summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-25 22:13:32 +0300
committerPaul Buetow <paul@buetow.org>2026-04-25 22:13:32 +0300
commita5cb9f7ad4b2bcb7ac367854147f661ab83c5ec1 (patch)
tree96db490ed72dec49c4bc2aba8539fd57f3246f47
parentcc9c69ce748416ad1d9b28592ec463434023d63c (diff)
can swap themes dynamicallyv0.12.0
-rw-r--r--PLAN.md85
-rw-r--r--integrationtests/integration_test.go33
-rw-r--r--internal/generator/doc.go33
-rw-r--r--internal/generator/favicon.go27
-rw-r--r--internal/generator/generator.go198
-rw-r--r--internal/generator/generator_test.go32
-rw-r--r--internal/generator/templates.go11
-rw-r--r--internal/generator/templates/embed.go65
-rw-r--r--internal/generator/templates/shared/favicon_head.tmpl2
-rw-r--r--internal/generator/templates/shared/nav.tmpl1772
-rw-r--r--internal/generator/templates/shared/shared.css925
-rw-r--r--internal/generator/templates/shared/shared.js1343
-rw-r--r--internal/generator/templates/shell.tmpl77
-rw-r--r--internal/generator/templates/themes/aurora.tmpl359
-rw-r--r--internal/generator/templates/themes/aurora/meta.json7
-rw-r--r--internal/generator/templates/themes/aurora/theme.css94
-rw-r--r--internal/generator/templates/themes/aurora/theme.js200
-rw-r--r--internal/generator/templates/themes/biomech.tmpl217
-rw-r--r--internal/generator/templates/themes/biomech/meta.json7
-rw-r--r--internal/generator/templates/themes/biomech/theme.css52
-rw-r--r--internal/generator/templates/themes/biomech/theme.js98
-rw-r--r--internal/generator/templates/themes/brutalist.tmpl260
-rw-r--r--internal/generator/templates/themes/brutalist/meta.json7
-rw-r--r--internal/generator/templates/themes/brutalist/theme.css65
-rw-r--r--internal/generator/templates/themes/brutalist/theme.js132
-rw-r--r--internal/generator/templates/themes/cathedral/meta.json7
-rw-r--r--internal/generator/templates/themes/cathedral/theme.css79
-rw-r--r--internal/generator/templates/themes/cathedral/theme.js (renamed from internal/generator/templates/themes/cathedral.tmpl)154
-rw-r--r--internal/generator/templates/themes/cosmos/meta.json7
-rw-r--r--internal/generator/templates/themes/cosmos/theme.css81
-rw-r--r--internal/generator/templates/themes/cosmos/theme.js (renamed from internal/generator/templates/themes/cosmos.tmpl)152
-rw-r--r--internal/generator/templates/themes/dos.tmpl302
-rw-r--r--internal/generator/templates/themes/dos/meta.json7
-rw-r--r--internal/generator/templates/themes/dos/theme.css79
-rw-r--r--internal/generator/templates/themes/dos/theme.js157
-rw-r--r--internal/generator/templates/themes/matrix/meta.json7
-rw-r--r--internal/generator/templates/themes/matrix/theme.css76
-rw-r--r--internal/generator/templates/themes/matrix/theme.js (renamed from internal/generator/templates/themes/matrix.tmpl)149
-rw-r--r--internal/generator/templates/themes/neon.tmpl348
-rw-r--r--internal/generator/templates/themes/neon/meta.json7
-rw-r--r--internal/generator/templates/themes/neon/theme.css94
-rw-r--r--internal/generator/templates/themes/neon/theme.js155
-rw-r--r--internal/generator/templates/themes/noir/meta.json7
-rw-r--r--internal/generator/templates/themes/noir/theme.css77
-rw-r--r--internal/generator/templates/themes/noir/theme.js (renamed from internal/generator/templates/themes/noir.tmpl)152
-rw-r--r--internal/generator/templates/themes/ocean/meta.json7
-rw-r--r--internal/generator/templates/themes/ocean/theme.css69
-rw-r--r--internal/generator/templates/themes/ocean/theme.js (renamed from internal/generator/templates/themes/ocean.tmpl)139
-rw-r--r--internal/generator/templates/themes/plasma/meta.json7
-rw-r--r--internal/generator/templates/themes/plasma/theme.css78
-rw-r--r--internal/generator/templates/themes/plasma/theme.js (renamed from internal/generator/templates/themes/plasma.tmpl)148
-rw-r--r--internal/generator/templates/themes/retro/meta.json7
-rw-r--r--internal/generator/templates/themes/retro/theme.css79
-rw-r--r--internal/generator/templates/themes/retro/theme.js (renamed from internal/generator/templates/themes/retro.tmpl)148
-rw-r--r--internal/generator/templates/themes/retrofuture.tmpl374
-rw-r--r--internal/generator/templates/themes/retrofuture/meta.json7
-rw-r--r--internal/generator/templates/themes/retrofuture/theme.css105
-rw-r--r--internal/generator/templates/themes/retrofuture/theme.js193
-rw-r--r--internal/generator/templates/themes/spaceage/meta.json7
-rw-r--r--internal/generator/templates/themes/spaceage/theme.css80
-rw-r--r--internal/generator/templates/themes/spaceage/theme.js (renamed from internal/generator/templates/themes/spaceage.tmpl)151
-rw-r--r--internal/generator/templates/themes/surveillance.tmpl224
-rw-r--r--internal/generator/templates/themes/surveillance/meta.json7
-rw-r--r--internal/generator/templates/themes/surveillance/theme.css50
-rw-r--r--internal/generator/templates/themes/surveillance/theme.js107
-rw-r--r--internal/generator/templates/themes/synthwave/meta.json7
-rw-r--r--internal/generator/templates/themes/synthwave/theme.css85
-rw-r--r--internal/generator/templates/themes/synthwave/theme.js (renamed from internal/generator/templates/themes/synthwave.tmpl)158
-rw-r--r--internal/generator/templates/themes/terminal.tmpl275
-rw-r--r--internal/generator/templates/themes/terminal/meta.json7
-rw-r--r--internal/generator/templates/themes/terminal/theme.css70
-rw-r--r--internal/generator/templates/themes/terminal/theme.js141
-rw-r--r--internal/generator/templates/themes/tropicale/meta.json7
-rw-r--r--internal/generator/templates/themes/tropicale/theme.css81
-rw-r--r--internal/generator/templates/themes/tropicale/theme.js (renamed from internal/generator/templates/themes/tropicale.tmpl)152
-rw-r--r--internal/generator/templates/themes/volcano/meta.json7
-rw-r--r--internal/generator/templates/themes/volcano/theme.css69
-rw-r--r--internal/generator/templates/themes/volcano/theme.js (renamed from internal/generator/templates/themes/volcano.tmpl)139
-rw-r--r--internal/generator/themes.go33
-rw-r--r--internal/version/version.go2
80 files changed, 5488 insertions, 5930 deletions
diff --git a/PLAN.md b/PLAN.md
deleted file mode 100644
index 3190323..0000000
--- a/PLAN.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Plan
-
-This is the plan for the microblog of snonux.foo
-
-## Style
-
-The style should be the same as in ./design.html
-
-* also link to foo.zone, my normal blog, at the top of every page of the microblog.
-* dont have an archive link like in the design.html example.
-* the "transmit to nexus" should just link to mailto:paul@nospan.buetow.org
-* every microblog entry should have date/timestamp linke in the design.html exampl
-* dont have the likes (hearts), comments, and rebroadcast buttons per blog post.
-* we want to be able to navigat between the blog posts with cursor up-down keys and jk (vi style up-down keys)
-* we also want to navigate with the left-right cursor keys to previous, next page (if any) and also hl(vi style up-down keys)
-* when navigating, we want to highlight the active entry (with a border or background), so it's clear which post is selected.and also play a sound.
-* when hit enter on a blog post, open it in a larger view. when hit ESC there, return.
-
-## Input dir
-
-I want to create a microblog. It should have an input directory, where i can put multiple source file formats:
-
-* Plain .txt
-* Markdown .md
-* Images (.png/.jpg/.gif)
-* Audio (.mp3 files)
-
-And the blog should automatically generate a post out of it. Furthermore, it should also support a Markdown with a reference to an image file in the same directory.
-
-Once we invoke the microblog generator, it should process all files in the input dir into blog assets.
-
-### Plain text input
-
-Just add this as a plain text as a blog post
-
-### Markdown .md
-
-Add this as a formatted HTML entry with styling etc. For this, we need to convert the Markdown to HTML.
-
-### Images
-
-Just have the image displayed as its own entry.
-
-### Audio
-
-Just have a playback button for the audio in the resulting entry
-
-### After being processed
-
-After an input was procssed, remove the files from the input dir.
-
-## Output dir
-
-The output dir should only contain static assets. That the directory to be published via a webbrowser.
-
-We expect one directory per blog post. The microblog generator then combines all of them together into multiple pages.
-
-So we need to keep the individual directories per blog post since the pages need to be re-generated according to the total blog post count and same for the atom.xml feed, so we need the directories as intermediate formats. We can link to images directly to them, though. So the output directory format will be like this:
-
-
-./outdir/index.html # Generated main page
-./outdir/pageN.html # Older pages
-./ourdir/posts/YYYY-MM-DD-HHmmss/ # Microblog post asset(s)
-
-Downscale any images to a maximum width of 1024px and compress to 80% JPEG quality
-
-## Page size limit
-
-Have max 42 entries on the single HTML page. Once more, allow paging (e.g. go to next 42 pages, etc).
-
-* Next page (if any) is only on the bottom of the page.
-* Previous page button (if any) is at the top of the page.
-
-## atom.xml feed
-
-We should have an atom.xml feed with the last 42 entries generated every run.
-
-## Comprehensive testing
-
-* All features should have integration tests.
-
-## Technologies used
-
-* Implemented in Google Go
-* Follow everything from the auditing-code-quality skill
diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go
index 91ae6af..4b96d4d 100644
--- a/integrationtests/integration_test.go
+++ b/integrationtests/integration_test.go
@@ -118,8 +118,16 @@ func TestTxtInput(t *testing.T) {
// index.html must contain the post text.
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, "Hello, Nexus!", "index.html")
+ // splash WebGL canvas is part of the per-theme splash markup baked in.
assertContains(t, index, "splash-gl-canvas", "index.html splash WebGL canvas")
assertContains(t, index, `href="atom.xml"`, "index.html atom feed link")
+ // shared bundles must also be written.
+ if _, err := os.Stat(filepath.Join(outputDir, "shared.css")); err != nil {
+ t.Fatalf("shared.css missing: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(outputDir, "shared.js")); err != nil {
+ t.Fatalf("shared.js missing: %v", err)
+ }
}
// TestMarkdownInput verifies Markdown files are converted to HTML.
@@ -356,8 +364,11 @@ func TestKeyboardNavJS(t *testing.T) {
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, `data-index="0"`, "index.html data-index attribute")
- assertContains(t, index, `.post-active`, "index.html .post-active CSS")
- assertContains(t, index, `playNavSound`, "index.html playNavSound function")
+ // Shared CSS and JS now live as separate files referenced from the shell.
+ sharedCSS := readFile(t, filepath.Join(outputDir, "shared.css"))
+ assertContains(t, sharedCSS, `.post-active`, "shared.css .post-active rule")
+ sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
+ assertContains(t, sharedJS, `playNavSound`, "shared.js playNavSound function")
}
// TestThemeSelection verifies that every registered theme renders a valid
@@ -395,8 +406,24 @@ func TestThemeSelection(t *testing.T) {
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, "theme test post", "post text")
- assertContains(t, index, "playNavSound", "nav JS")
assertContains(t, index, `data-index="0"`, "data-index attribute")
+ assertContains(t, index, `SNONUX_DEFAULT_THEME = "`+theme+`"`, "default theme baked in")
+
+ // Per-theme assets are written under themes/<theme>/.
+ for _, fname := range []string{"theme.css", "theme.js", "meta.json", "sounds.json"} {
+ path := filepath.Join(outputDir, "themes", theme, fname)
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("theme asset missing %s: %v", path, err)
+ }
+ if info.Size() == 0 {
+ t.Fatalf("theme asset %s is empty", path)
+ }
+ }
+
+ // shared.js holds the nav logic.
+ sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
+ assertContains(t, sharedJS, "playNavSound", "shared.js playNavSound")
})
}
}
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
index d7d4a53..be82bdc 100644
--- a/internal/generator/doc.go
+++ b/internal/generator/doc.go
@@ -4,21 +4,26 @@
// Responsibilities by area (file → role):
//
// - generator.go — Orchestration: load posts from disk, sort newest-first,
-// paginate, parse theme+nav templates, write index.html / pageN.html,
-// then call atom.Generate.
-// - themes.go — Theme registry (name → template string) and getTheme /
-// ListThemes for the CLI.
-// - theme_*.go — One file per visual theme: full-page HTML that invokes
-// {{template "navSharedCSSInner"}} inside <style>, then {{template "splashGate"}},
-// {{template "navhints" .}}, {{template "navmodal" .}},
-// {{template "navscript" .}}.
-// - shared.go — navDefs: shared {{define}} blocks merged at parse time with
-// the chosen theme so a single html/template parse sees every name.
-// - theme_sounds.go — Per-theme Web Audio parameters (splash arpeggio, nav blip,
-// modal open/close); embedded in pages as ThemeSoundsJSON for navscript.
-// - templates.go — Short pointer: where templates and registry live.
+// paginate, parse shell+nav templates, write index.html / pageN.html plus
+// shared.css/shared.js and per-theme assets, then call atom.Generate.
+// - themes.go — ListThemes / validThemeName helpers backed by the embedded FS.
+// - shared.go — navDefs: shared {{define}} blocks (splashGate, navhints,
+// navmodal) merged at parse time with shell.tmpl so a single html/template
+// parse sees every name.
+// - theme_sounds.go — Per-theme Web Audio parameters; one file is written per
+// theme to dist/themes/<name>/sounds.json and the default theme's preset is
+// also baked into shell.tmpl as window.SNONUX_SOUNDS so the splash chime
+// can fire instantly.
+// - favicon.go — Generates the 32×32 .ico binary written into the output dir.
+// - templates.go — Short pointer: where templates and helpers live.
//
-// Dependency direction: themes and shared nav templates are composed only for
+// Theme assets live as separate files under templates/themes/<name>/theme.css,
+// theme.js, and meta.json. The single shell.tmpl loads the active theme's CSS
+// synchronously in <head> (chosen from localStorage by a boot script) and
+// shared.js orchestrates the rest. Switching themes is a localStorage write
+// followed by location.reload().
+//
+// Dependency direction: shell and shared nav templates are composed only for
// the HTML path (generator.go). Package atom depends on config and post only,
// not on themes or html/template, so feed logic stays isolated from page chrome.
package generator
diff --git a/internal/generator/favicon.go b/internal/generator/favicon.go
index a4a8251..8664183 100644
--- a/internal/generator/favicon.go
+++ b/internal/generator/favicon.go
@@ -6,35 +6,12 @@ import (
"fmt"
"image"
"image/color"
- "log"
"os"
"path/filepath"
- "strings"
-
- "codeberg.org/snonux/snonux/internal/generator/templates"
)
-// faviconHeadHTML is the <link rel="icon"> fragment injected into every theme's
-// <head>. Loaded at startup from the embedded templates FS so no HTML lives in
-// the Go sources.
-var faviconHeadHTML = loadFaviconHeadHTML()
-
-func loadFaviconHeadHTML() string {
- s, err := templates.Shared("favicon_head")
- if err != nil {
- log.Printf("warning: could not load favicon_head template: %v", err)
- return ""
- }
- return s
-}
-
-func injectSharedHead(theme string) string {
- if strings.Contains(theme, `rel="icon"`) {
- return theme
- }
-
- return strings.Replace(theme, "</head>", faviconHeadHTML+"</head>", 1)
-}
+// shell.tmpl now embeds the favicon <link> directly, so no head-injection
+// helper is needed; favicon.go only generates and writes the .ico binary.
func writeFavicon(outputDir string) error {
data, err := generateFaviconICO()
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index dc1f236..079cc38 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -12,17 +12,29 @@ import (
"codeberg.org/snonux/snonux/internal/config"
"codeberg.org/snonux/snonux/internal/generator/atom"
+ "codeberg.org/snonux/snonux/internal/generator/templates"
"codeberg.org/snonux/snonux/internal/post"
)
// pageData holds the template variables for a single HTML page.
+// Theme-specific values come from the default theme's meta.json — they are
+// what the user sees on first paint. shared.js may swap them out at runtime
+// when a non-default theme is saved in localStorage.
type pageData struct {
- Posts []postView
- PrevPage string // URL of the newer page, empty if none
- NextPage string // URL of the older page, empty if none
- PrevPageJSON template.JS
- NextPageJSON template.JS
- ThemeSoundsJSON template.JS // Web Audio preset for this theme (splash + nav)
+ Posts []postView
+ PrevPage string // URL of the newer page, empty if none
+ NextPage string // URL of the older page, empty if none
+ PrevPageJSON template.JS
+ NextPageJSON template.JS
+
+ DefaultTheme string // baked into the shell as SNONUX_DEFAULT_THEME
+ AllThemesJSON template.JS // [...] of all available theme names, JS literal
+ DefaultTitle string // <title> contents for the default theme
+ DefaultHeaderHTML template.HTML // <header> innerHTML for the default theme
+ DefaultSplashHTML template.HTML // #splash-overlay innerHTML for the default theme
+ DefaultPrevText template.HTML // pagination prev anchor text (theme-styled)
+ DefaultNextText template.HTML // pagination next anchor text
+ DefaultSoundsJSON template.JS // default theme's Web Audio preset (literal)
}
// postView is a render-friendly representation of a post for the HTML template.
@@ -32,23 +44,62 @@ type postView struct {
ContentHTML template.HTML // pre-rendered; trusted — generated by this tool
}
-// Run loads all posts, generates all HTML pages, and writes atom.xml.
+// themeMeta mirrors the JSON structure written by the migration tool and used
+// at runtime by shared.js to swap theme markup in.
+type themeMeta struct {
+ Title string `json:"title"`
+ HeaderHTML string `json:"header_html"`
+ SplashInnerHTML string `json:"splash_inner_html"`
+ PrevPageText string `json:"prev_page_text"`
+ NextPageText string `json:"next_page_text"`
+}
+
+func loadThemeMeta(name string) (themeMeta, error) {
+ var m themeMeta
+ b, err := templates.ThemeMeta(name)
+ if err != nil {
+ return m, fmt.Errorf("read theme meta %q: %w", name, err)
+ }
+ if err := json.Unmarshal(b, &m); err != nil {
+ return m, fmt.Errorf("parse theme meta %q: %w", name, err)
+ }
+ return m, nil
+}
+
+// allThemesJSON returns a JS array literal of all theme names.
+func allThemesJSON() (template.JS, error) {
+ names, err := templates.ThemeNames()
+ if err != nil {
+ return "", err
+ }
+ b, err := json.Marshal(names)
+ if err != nil {
+ return "", err
+ }
+ return template.JS(b), nil //nolint:gosec // marshalled from a fixed string slice
+}
+
+// Run loads all posts, generates all HTML pages, and writes atom.xml plus the
+// shared CSS/JS bundles and per-theme asset files.
func Run(cfg *config.Config) error {
posts, err := loadAllPosts(cfg.OutputDir)
if err != nil {
return err
}
- // Sort newest-first so page 1 (index.html) has the latest content.
sort.Slice(posts, func(i, j int) bool {
return posts[i].Timestamp.After(posts[j].Timestamp)
})
pages := paginate(posts, config.PostsPerPage)
- // Combine the theme HTML (which uses {{template "navhints"}} etc.) with the
- // shared navDefs sub-templates so a single parse call resolves all references.
- combined := injectSharedHead(getTheme(cfg.Theme)) + "\n" + navDefs
+ // Combine shell.tmpl with nav.tmpl partials so a single parse call resolves
+ // references to splashGate, navhints, navmodal.
+ shellSrc, err := templates.Shell()
+ if err != nil {
+ return fmt.Errorf("load shell template: %w", err)
+ }
+ combined := shellSrc + "\n" + navDefs
tmpl, err := template.New("page").Parse(combined)
if err != nil {
return fmt.Errorf("parse page template: %w", err)
@@ -57,9 +108,25 @@ func Run(cfg *config.Config) error {
if err := writeFavicon(cfg.OutputDir); err != nil {
return err
}
+ if err := writeSharedAssets(cfg.OutputDir); err != nil {
+ return err
+ }
+ if err := writeAllThemeAssets(cfg.OutputDir); err != nil {
+ return err
+ }
+
+ defaultTheme := validThemeName(cfg.Theme)
+ defaultMeta, err := loadThemeMeta(defaultTheme)
+ if err != nil {
+ return err
+ }
+ all, err := allThemesJSON()
+ if err != nil {
+ return err
+ }
for i, page := range pages {
- if err := writePage(tmpl, page, i, len(pages), cfg); err != nil {
+ if err := writePage(tmpl, page, i, len(pages), cfg, defaultTheme, defaultMeta, all); err != nil {
return err
}
}
@@ -126,8 +193,8 @@ func pageFilename(index int) string {
const indexPageNavURL = "index.html?splash=0"
// writePage renders one HTML page and writes it to cfg.OutputDir.
-func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPages int, cfg *config.Config) error {
- data := buildPageData(posts, pageIndex, totalPages, cfg.Theme)
+func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPages int, cfg *config.Config, defaultTheme string, defaultMeta themeMeta, all template.JS) error {
+ data := buildPageData(posts, pageIndex, totalPages, defaultTheme, defaultMeta, all)
filename := pageFilename(pageIndex)
path := filepath.Join(cfg.OutputDir, filename)
@@ -146,7 +213,7 @@ func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPage
}
// buildPageData constructs the template data for a single page.
-func buildPageData(posts []*post.Post, pageIndex, totalPages int, theme string) pageData {
+func buildPageData(posts []*post.Post, pageIndex, totalPages int, defaultTheme string, meta themeMeta, all template.JS) pageData {
views := make([]postView, len(posts))
for i, p := range posts {
views[i] = postView{
@@ -173,12 +240,19 @@ func buildPageData(posts []*post.Post, pageIndex, totalPages int, theme string)
}
return pageData{
- Posts: views,
- PrevPage: prevPage,
- NextPage: nextPage,
- PrevPageJSON: jsonStringOrNull(prevPage),
- NextPageJSON: jsonStringOrNull(nextPage),
- ThemeSoundsJSON: themeSoundsJSON(theme),
+ Posts: views,
+ PrevPage: prevPage,
+ NextPage: nextPage,
+ PrevPageJSON: jsonStringOrNull(prevPage),
+ NextPageJSON: jsonStringOrNull(nextPage),
+ DefaultTheme: defaultTheme,
+ AllThemesJSON: all,
+ DefaultTitle: meta.Title,
+ DefaultHeaderHTML: template.HTML(meta.HeaderHTML), //nolint:gosec // source is in-tree theme metadata
+ DefaultSplashHTML: template.HTML(meta.SplashInnerHTML), //nolint:gosec // same
+ DefaultPrevText: template.HTML(meta.PrevPageText), //nolint:gosec // same
+ DefaultNextText: template.HTML(meta.NextPageText), //nolint:gosec // same
+ DefaultSoundsJSON: themeSoundsJSON(defaultTheme),
}
}
@@ -202,3 +276,85 @@ func jsonStringOrNull(s string) template.JS {
return template.JS(strings.TrimSpace(string(b))) //nolint:gosec // filename is tool-generated
}
+
+// writeSharedAssets dumps shared.css and shared.js to the output dir. They are
+// linked from every page and cached by browsers across navigations.
+func writeSharedAssets(outputDir string) error {
+ css, err := templates.SharedCSS()
+ if err != nil {
+ return fmt.Errorf("read shared.css: %w", err)
+ }
+ if err := os.WriteFile(filepath.Join(outputDir, "shared.css"), css, 0o644); err != nil {
+ return fmt.Errorf("write shared.css: %w", err)
+ }
+
+ js, err := templates.SharedJS()
+ if err != nil {
+ return fmt.Errorf("read shared.js: %w", err)
+ }
+ if err := os.WriteFile(filepath.Join(outputDir, "shared.js"), js, 0o644); err != nil {
+ return fmt.Errorf("write shared.js: %w", err)
+ }
+
+ return nil
+}
+
+// writeAllThemeAssets copies each theme's CSS, JS, meta.json, and writes its
+// sounds.json to dist/themes/<name>/. shared.js fetches these on theme switch.
+func writeAllThemeAssets(outputDir string) error {
+ names, err := templates.ThemeNames()
+ if err != nil {
+ return err
+ }
+
+ root := filepath.Join(outputDir, "themes")
+ if err := os.MkdirAll(root, 0o755); err != nil {
+ return fmt.Errorf("create themes dir: %w", err)
+ }
+
+ for _, name := range names {
+ dir := filepath.Join(root, name)
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return fmt.Errorf("create theme dir %q: %w", name, err)
+ }
+ if err := writeThemeAsset(dir, name); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// writeThemeAsset copies the four per-theme files into dir.
+func writeThemeAsset(dir, name string) error {
+ css, err := templates.ThemeCSS(name)
+ if err != nil {
+ return fmt.Errorf("read %s/theme.css: %w", name, err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "theme.css"), css, 0o644); err != nil {
+ return fmt.Errorf("write %s/theme.css: %w", name, err)
+ }
+
+ js, err := templates.ThemeJS(name)
+ if err != nil {
+ return fmt.Errorf("read %s/theme.js: %w", name, err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "theme.js"), js, 0o644); err != nil {
+ return fmt.Errorf("write %s/theme.js: %w", name, err)
+ }
+
+ meta, err := templates.ThemeMeta(name)
+ if err != nil {
+ return fmt.Errorf("read %s/meta.json: %w", name, err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "meta.json"), meta, 0o644); err != nil {
+ return fmt.Errorf("write %s/meta.json: %w", name, err)
+ }
+
+ soundsJSON := themeSoundsJSON(name)
+ if err := os.WriteFile(filepath.Join(dir, "sounds.json"), []byte(soundsJSON), 0o644); err != nil {
+ return fmt.Errorf("write %s/sounds.json: %w", name, err)
+ }
+
+ return nil
+}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 7b0009c..d477275 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -179,10 +179,19 @@ func TestBuildPageData_navLinks(t *testing.T) {
},
}
+ meta, err := loadThemeMeta("neon")
+ if err != nil {
+ t.Fatalf("loadThemeMeta: %v", err)
+ }
+ all, err := allThemesJSON()
+ if err != nil {
+ t.Fatalf("allThemesJSON: %v", err)
+ }
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
- data := buildPageData([]*post.Post{p}, tt.pageIndex, tt.totalPages, "neon")
+ data := buildPageData([]*post.Post{p}, tt.pageIndex, tt.totalPages, "neon", meta, all)
if data.PrevPage != tt.wantPrev {
t.Fatalf("PrevPage=%q; want %q", data.PrevPage, tt.wantPrev)
}
@@ -202,19 +211,24 @@ func TestBuildPageData_navLinks(t *testing.T) {
}
}
-func TestGetTheme_unknownFallsBackToNeon(t *testing.T) {
+func TestValidThemeName_unknownFallsBackToNeon(t *testing.T) {
t.Parallel()
- if got, want := getTheme("no-such-theme-"), getTheme("neon"); got != want {
- t.Fatal("expected neon fallback")
+ if got := validThemeName("no-such-theme-"); got != "neon" {
+ t.Fatalf("validThemeName(\"no-such-theme-\") = %q; want \"neon\"", got)
+ }
+ if got := validThemeName("matrix"); got != "matrix" {
+ t.Fatalf("validThemeName(\"matrix\") = %q; want \"matrix\"", got)
}
}
-func TestInjectSharedHead_addsFaviconLink(t *testing.T) {
+func TestLoadThemeMeta_neonHasFields(t *testing.T) {
t.Parallel()
-
- got := injectSharedHead(getTheme("neon"))
- if !strings.Contains(got, `rel="icon" href="favicon.ico"`) {
- t.Fatalf("favicon link missing from theme head")
+ m, err := loadThemeMeta("neon")
+ if err != nil {
+ t.Fatalf("loadThemeMeta(neon): %v", err)
+ }
+ if m.Title == "" || m.HeaderHTML == "" || m.SplashInnerHTML == "" {
+ t.Fatalf("neon meta missing required fields: %+v", m)
}
}
diff --git a/internal/generator/templates.go b/internal/generator/templates.go
index f50601a..df854ab 100644
--- a/internal/generator/templates.go
+++ b/internal/generator/templates.go
@@ -1,9 +1,10 @@
package generator
-// HTML theme templates and shared sub-templates live as separate files under
-// internal/generator/templates/{themes,shared}/*.tmpl and are loaded into the
+// The unified shell template, shared CSS/JS bundles, and per-theme asset
+// triples live under internal/generator/templates/ and are loaded into the
// binary via embed.FS; see internal/generator/templates/embed.go.
//
-// themes.go exposes the theme registry and getTheme()/ListThemes() helpers.
-// shared.go loads templates/shared/nav.tmpl into navDefs.
-// favicon.go loads templates/shared/favicon_head.tmpl into faviconHeadHTML.
+// themes.go exposes ListThemes()/validThemeName().
+// shared.go loads templates/shared/nav.tmpl into navDefs (splashGate, navhints,
+// navmodal partials called from shell.tmpl).
+// favicon.go generates the favicon.ico binary written into each output dir.
diff --git a/internal/generator/templates/embed.go b/internal/generator/templates/embed.go
index 91ba07f..ac3efbd 100644
--- a/internal/generator/templates/embed.go
+++ b/internal/generator/templates/embed.go
@@ -1,8 +1,8 @@
-// Package templates exposes the embedded HTML theme and shared sub-templates
-// used by the generator. Themes and nav/HTML fragments live as separate files
-// under templates/themes/*.tmpl and templates/shared/*.tmpl so they can be
-// edited without recompiling logic; they are compiled into the binary via
-// //go:embed so snonux still ships as a single self-contained executable.
+// Package templates exposes the embedded HTML shell, shared CSS/JS bundles,
+// and per-theme assets used by the generator. Themes live as directories
+// under templates/themes/<name>/ containing theme.css, theme.js, and meta.json.
+// All assets are compiled into the binary via //go:embed so snonux still ships
+// as a single self-contained executable.
package templates
import (
@@ -11,24 +11,24 @@ import (
"io/fs"
"path"
"sort"
- "strings"
)
-//go:embed themes/*.tmpl shared/*.tmpl
+//go:embed shell.tmpl shared/*.tmpl shared/shared.css shared/shared.js themes/*/theme.css themes/*/theme.js themes/*/meta.json
var FS embed.FS
-// Theme reads the raw HTML template body for a named theme.
-// The returned string is the outer page template; shared sub-templates
-// (navhints, navmodal, navscript, etc.) are obtained via Shared().
-func Theme(name string) (string, error) {
- b, err := FS.ReadFile(path.Join("themes", name+".tmpl"))
+// Shell returns the body of shell.tmpl — the single page template used for
+// every generated HTML page. Theme-specific markup is injected at gen time
+// from the default theme's meta.json (and at runtime by shared.js for any
+// other selected theme).
+func Shell() (string, error) {
+ b, err := FS.ReadFile("shell.tmpl")
if err != nil {
- return "", fmt.Errorf("read theme %q: %w", name, err)
+ return "", fmt.Errorf("read shell.tmpl: %w", err)
}
return string(b), nil
}
-// Shared reads a named shared sub-template file from shared/*.tmpl.
+// Shared reads a named shared sub-template (currently only "nav").
func Shared(name string) (string, error) {
b, err := FS.ReadFile(path.Join("shared", name+".tmpl"))
if err != nil {
@@ -37,8 +37,35 @@ func Shared(name string) (string, error) {
return string(b), nil
}
+// SharedCSS returns the bundled CSS that every page links via shared.css.
+// Used by the generator to write dist/shared.css.
+func SharedCSS() ([]byte, error) {
+ return FS.ReadFile("shared/shared.css")
+}
+
+// SharedJS returns the bundled JS that every page references via shared.js.
+// Used by the generator to write dist/shared.js.
+func SharedJS() ([]byte, error) {
+ return FS.ReadFile("shared/shared.js")
+}
+
+// ThemeCSS returns the per-theme stylesheet bytes for the named theme.
+func ThemeCSS(name string) ([]byte, error) {
+ return FS.ReadFile(path.Join("themes", name, "theme.css"))
+}
+
+// ThemeJS returns the per-theme script bytes for the named theme.
+func ThemeJS(name string) ([]byte, error) {
+ return FS.ReadFile(path.Join("themes", name, "theme.js"))
+}
+
+// ThemeMeta returns the per-theme meta.json bytes for the named theme.
+func ThemeMeta(name string) ([]byte, error) {
+ return FS.ReadFile(path.Join("themes", name, "meta.json"))
+}
+
// ThemeNames returns a sorted list of available theme names derived from the
-// files present under templates/themes/.
+// directories present under templates/themes/.
func ThemeNames() ([]string, error) {
entries, err := fs.ReadDir(FS, "themes")
if err != nil {
@@ -47,14 +74,10 @@ func ThemeNames() ([]string, error) {
names := make([]string, 0, len(entries))
for _, e := range entries {
- if e.IsDir() {
- continue
- }
- n := e.Name()
- if !strings.HasSuffix(n, ".tmpl") {
+ if !e.IsDir() {
continue
}
- names = append(names, strings.TrimSuffix(n, ".tmpl"))
+ names = append(names, e.Name())
}
sort.Strings(names)
diff --git a/internal/generator/templates/shared/favicon_head.tmpl b/internal/generator/templates/shared/favicon_head.tmpl
deleted file mode 100644
index e35bd5d..0000000
--- a/internal/generator/templates/shared/favicon_head.tmpl
+++ /dev/null
@@ -1,2 +0,0 @@
-
- <link rel="icon" href="favicon.ico" sizes="any">
diff --git a/internal/generator/templates/shared/nav.tmpl b/internal/generator/templates/shared/nav.tmpl
index 346294a..9794f2f 100644
--- a/internal/generator/templates/shared/nav.tmpl
+++ b/internal/generator/templates/shared/nav.tmpl
@@ -45,852 +45,27 @@
{{define "navhints"}}
<div class="nav-hints" role="region" aria-label="Keyboard shortcuts">
- <span><kbd>j</kbd><kbd>k</kbd> or <kbd>↑</kbd><kbd>↓</kbd> select post</span>
- <span><kbd>PgUp</kbd><kbd>PgDn</kbd> scroll</span>
- <span><kbd>Enter</kbd> or click post to expand</span>
- <span><kbd>Esc</kbd> close</span>
- <span><kbd>h</kbd><kbd>l</kbd> or <kbd>←</kbd><kbd>→</kbd> change page</span>
- <span><kbd>w</kbd> wild mode</span>
+ <div class="nav-hints-row nav-hints-nav">
+ <span class="nav-hints-label">nav</span>
+ <span><kbd>j</kbd><kbd>k</kbd><kbd>↑</kbd><kbd>↓</kbd> post</span>
+ <span><kbd>h</kbd><kbd>l</kbd><kbd>←</kbd><kbd>→</kbd> page</span>
+ <span><kbd>PgUp</kbd><kbd>PgDn</kbd> scroll</span>
+ <span><kbd>Enter</kbd> open</span>
+ <span><kbd>Esc</kbd> close</span>
+ </div>
+ <div class="nav-hints-row nav-hints-fx">
+ <span class="nav-hints-label">fx</span>
+ <button type="button" class="nav-fx-button" data-sno-fx="wild" aria-pressed="false" aria-label="Toggle wild mode"><kbd>w</kbd> wild</button>
+ <button type="button" class="nav-fx-button" data-sno-fx="crt" aria-pressed="false" aria-label="Toggle CRT effect"><kbd>c</kbd> crt</button>
+ <button type="button" class="nav-fx-button" data-sno-fx="ghost" aria-pressed="false" aria-label="Toggle ghost mode"><kbd>g</kbd> ghost</button>
+ <button type="button" class="nav-fx-button" data-sno-fx="flash" aria-label="Trigger flash effect"><kbd>p</kbd> flash</button>
+ <button type="button" class="nav-fx-button" data-sno-fx="scatter" aria-label="Trigger scatter effect"><kbd>x</kbd> scatter</button>
+ <span class="nav-hints-spacer"><kbd>t</kbd> theme</span>
+ <select id="sno-theme-select" class="nav-fx-button" aria-label="Theme"></select>
+ </div>
</div>
{{end}}
-{{define "navSharedCSSInner"}}
-/* Non-active posts are translucent so the WebGL background shows through.
- The active/highlighted post snaps to full opacity for clear reading.
- Hover on a non-active post partially reveals it before selection. */
-.post { position:relative; }
-.post:not(.post-active) { opacity: 0.55; transition: opacity 0.25s ease; }
-.post:not(.post-active):hover { opacity: 0.85; }
-.post.post-active { opacity: 1 !important; transition: opacity 0.15s ease; }
-/* Thumbnail sizing in list view; modal overrides to full width. */
-.post-image { max-height:220px; max-width:100%; object-fit:cover; cursor:pointer; }
-#post-modal .post-image { max-height:none; width:100%; max-width:100%; object-fit:contain; cursor:default; }
-/* Semi-transparent modal backdrop so the WebGL scene stays visible behind
- the expanded post. Theme-specific modal-inner keeps its own background. */
-.post-modal { background:rgba(0,0,0,0.55) !important; backdrop-filter:blur(6px) !important; }
-#post-modal.active { display:flex !important; align-items:center; justify-content:center; }
-#post-modal .modal-inner { width:fit-content; max-width:min(100%, 90vw); max-height:calc(100vh - 80px); overflow:auto; margin:0 auto !important; will-change:transform; }
-/* Content area max-width across all themes */
-.overlay { max-width:1200px; margin-left:auto; margin-right:auto; }
-/* Pagination: newer + older in a footer bar (below scrollable posts, like the header) */
-.page-nav-dual { display:flex; justify-content:center; align-items:center; flex-wrap:wrap;
- gap:clamp(16px,4vw,48px); }
-/* Flex column layout: let #post-content shrink so overflow-y scrolls; footer stays visible */
-#post-content.content { min-height:0; }
-.page-nav-footer { flex-shrink:0; width:100%; box-sizing:border-box; }
-.page-nav-footer .page-nav { margin:0; }
-/* ~Half-height footer bar vs default .page-nav padding */
-.page-nav-footer .page-nav a { padding-top:4px; padding-bottom:4px; }
-/* Shared nav FX keyframes — themes apply these classes for brief effects */
-/* Modal open: post zooms/flies into the modal overlay */
-@keyframes sno-modal-zoom { 0%{transform:scale(0.82) translateY(18px);opacity:0} 60%{transform:scale(1.02) translateY(-3px);opacity:1} 100%{transform:scale(1) translateY(0);opacity:1} }
-@keyframes sno-modal-fly-up { 0%{transform:translateY(60px) scale(0.92);opacity:0} 70%{transform:translateY(-4px) scale(1.01);opacity:1} 100%{transform:translateY(0) scale(1);opacity:1} }
-@keyframes sno-modal-slide-in { 0%{transform:translateX(-40px) scale(0.96);opacity:0} 65%{transform:translateX(4px) scale(1.005);opacity:1} 100%{transform:translateX(0) scale(1);opacity:1} }
-@keyframes sno-modal-expand { 0%{transform:scale(0.05) rotate(-4deg);opacity:0} 55%{transform:scale(1.04) rotate(0.5deg);opacity:1} 100%{transform:scale(1) rotate(0);opacity:1} }
-.sno-modal-zoom .modal-inner { animation:sno-modal-zoom 0.36s cubic-bezier(0.22,1,0.36,1) both !important; }
-.sno-modal-fly .modal-inner { animation:sno-modal-fly-up 0.34s cubic-bezier(0.22,1,0.36,1) both !important; }
-.sno-modal-slide .modal-inner { animation:sno-modal-slide-in 0.32s cubic-bezier(0.22,1,0.36,1) both !important; }
-.sno-modal-expand .modal-inner { animation:sno-modal-expand 0.4s cubic-bezier(0.22,1,0.36,1) both !important; }
-@keyframes sno-shake { 0%,100%{transform:translate(0)} 14%{transform:translate(-7px,4px)} 28%{transform:translate(7px,-5px)} 42%{transform:translate(-5px,6px)} 56%{transform:translate(6px,-4px)} 70%{transform:translate(-4px,3px)} 86%{transform:translate(4px,-2px)} }
-@keyframes sno-zoom-fwd { 0%{transform:scale(1)} 40%{transform:scale(1.05)} 100%{transform:scale(1)} }
-@keyframes sno-glitch { 0%,100%{transform:translate(0) skewX(0)} 20%{transform:translate(-5px,0) skewX(-4deg)} 40%{transform:translate(5px,0) skewX(4deg)} 60%{transform:translate(-3px,0)} 80%{transform:translate(3px,0)} }
-@keyframes sno-bounce-left { 0%{transform:translateX(0)} 25%{transform:translateX(-18px)} 50%{transform:translateX(6px)} 75%{transform:translateX(-3px)} 100%{transform:translateX(0)} }
-@keyframes sno-bounce-right { 0%{transform:translateX(0)} 25%{transform:translateX(18px)} 50%{transform:translateX(-6px)} 75%{transform:translateX(3px)} 100%{transform:translateX(0)} }
-@keyframes sno-bounce-left-wild { 0%{transform:translateX(0) rotate(0)} 15%{transform:translateX(-48px) rotate(-3deg)} 35%{transform:translateX(22px) rotate(2deg)} 55%{transform:translateX(-12px) rotate(-1.2deg)} 75%{transform:translateX(6px) rotate(0.5deg)} 100%{transform:translateX(0) rotate(0)} }
-@keyframes sno-bounce-right-wild { 0%{transform:translateX(0) rotate(0)} 15%{transform:translateX(48px) rotate(3deg)} 35%{transform:translateX(-22px) rotate(-2deg)} 55%{transform:translateX(12px) rotate(1.2deg)} 75%{transform:translateX(-6px) rotate(-0.5deg)} 100%{transform:translateX(0) rotate(0)} }
-.sno-fx-bounce-left { animation:sno-bounce-left 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-right { animation:sno-bounce-right 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-left-wild { animation:sno-bounce-left-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-right-wild { animation:sno-bounce-right-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
-/* Vertical boundary bounce (top/bottom post list) */
-@keyframes sno-bounce-up { 0%{transform:translateY(0)} 25%{transform:translateY(-14px)} 50%{transform:translateY(5px)} 75%{transform:translateY(-2px)} 100%{transform:translateY(0)} }
-@keyframes sno-bounce-down { 0%{transform:translateY(0)} 25%{transform:translateY(14px)} 50%{transform:translateY(-5px)} 75%{transform:translateY(2px)} 100%{transform:translateY(0)} }
-@keyframes sno-bounce-up-wild { 0%{transform:translateY(0) rotate(0)} 15%{transform:translateY(-40px) rotate(-2.5deg)} 35%{transform:translateY(18px) rotate(1.5deg)} 55%{transform:translateY(-9px) rotate(-0.8deg)} 75%{transform:translateY(4px) rotate(0.3deg)} 100%{transform:translateY(0) rotate(0)} }
-@keyframes sno-bounce-down-wild { 0%{transform:translateY(0) rotate(0)} 15%{transform:translateY(40px) rotate(2.5deg)} 35%{transform:translateY(-18px) rotate(-1.5deg)} 55%{transform:translateY(9px) rotate(0.8deg)} 75%{transform:translateY(-4px) rotate(-0.3deg)} 100%{transform:translateY(0) rotate(0)} }
-.sno-fx-bounce-up { animation:sno-bounce-up 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-down { animation:sno-bounce-down 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-up-wild { animation:sno-bounce-up-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
-.sno-fx-bounce-down-wild { animation:sno-bounce-down-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
-/* Post hover ripple — radial ring emanates from center on hover */
-@keyframes sno-hover-ripple { 0%{transform:translate(-50%,-50%) scale(0);opacity:0.5} 100%{transform:translate(-50%,-50%) scale(1);opacity:0} }
-.post:not(.post-active)::before { content:''; position:absolute; top:50%; left:50%; width:120%; height:120%;
- border-radius:50%; border:2px solid currentColor; pointer-events:none;
- transform:translate(-50%,-50%) scale(0); opacity:0; z-index:0; }
-.post:not(.post-active):hover::before { animation:sno-hover-ripple 0.6s ease-out forwards; }
-/* Modal scroll-end flash — brief glow at bottom of modal-inner when scrolled to end */
-@keyframes sno-scroll-end-flash { 0%{opacity:0.7} 100%{opacity:0} }
-.modal-inner .sno-scroll-end { position:sticky; bottom:0; left:0; right:0; height:3px; pointer-events:none;
- background:linear-gradient(90deg, transparent, currentColor, transparent); opacity:0; }
-.modal-inner .sno-scroll-end.sno-scroll-end-active { animation:sno-scroll-end-flash 0.5s ease-out forwards; }
-/* Idle breathing — gentle glow pulse on active post after inactivity */
-@keyframes sno-idle-breathe { 0%,100%{box-shadow:inherit} 50%{box-shadow:0 0 18px 4px currentColor} }
-.post-active.sno-idle-breathe { animation:sno-idle-breathe 3s ease-in-out infinite; }
-/* First-visit particle burst */
-@keyframes sno-particle-fly { 0%{transform:translate(0,0) scale(1);opacity:1} 100%{transform:translate(var(--px),var(--py)) scale(0);opacity:0} }
-#sno-burst { position:fixed; inset:0; z-index:9999; pointer-events:none; }
-#sno-burst span { position:absolute; width:6px; height:6px; border-radius:50%; background:currentColor;
- animation:sno-particle-fly var(--pdur) ease-out forwards; animation-delay:var(--pdel); opacity:0; }
-/* Cursor sparkle trail (normal mode) */
-@keyframes sno-sparkle { 0%{transform:scale(1);opacity:0.8} 100%{transform:scale(0);opacity:0} }
-.sno-sparkle { position:fixed; pointer-events:none; z-index:9990; border-radius:50%; }
-/* Post afterimage — ghost of previously selected post */
-@keyframes sno-afterimage { 0%{opacity:0.4;transform:scale(1)} 100%{opacity:0;transform:scale(0.97)} }
-.sno-afterimage { position:absolute; inset:0; pointer-events:none; z-index:0;
- border:1px solid currentColor; border-radius:inherit; opacity:0; }
-.sno-afterimage-active { animation:sno-afterimage 0.5s ease-out forwards; }
-/* Wild flying emoji */
-@keyframes sno-fly-lr { 0%{transform:translateX(-60px) translateY(var(--fy,0)) rotate(0)} 100%{transform:translateX(calc(100vw + 60px)) translateY(var(--fy,0)) rotate(var(--frot,360deg))} }
-@keyframes sno-fly-rl { 0%{transform:translateX(calc(100vw + 60px)) translateY(var(--fy,0)) rotate(0)} 100%{transform:translateX(-60px) translateY(var(--fy,0)) rotate(var(--frot,-360deg))} }
-#sno-flyzone { position:fixed; inset:0; z-index:9985; pointer-events:none; overflow:hidden; }
-#sno-flyzone span { position:absolute; top:var(--ftop,50%); font-size:clamp(1.2rem,2.5vw,2rem);
- animation-timing-function:linear; animation-fill-mode:forwards; }
-/* Wild random post flip */
-@keyframes sno-flip-post { 0%{transform:scaleY(1)} 25%{transform:scaleY(-1)} 75%{transform:scaleY(-1)} 100%{transform:scaleY(1)} }
-.sno-fx-flip { animation:sno-flip-post 1.4s ease-in-out both !important; transform-origin:center; }
-/* Wild hue drift on body */
-@keyframes sno-hue-drift { 0%{filter:hue-rotate(0)} 100%{filter:hue-rotate(360deg)} }
-body.sno-wild-hue { animation:sno-hue-drift 12s linear infinite; }
-@keyframes sno-wild-pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
-/* Storm overlay that flickers like distant lightning while wild mode is on */
-@keyframes sno-wild-flicker { 0%,84%,87%,91%,94%,100%{opacity:0} 85%,90%{opacity:0.75} 86%,92%{opacity:0.35} }
-.sno-fx-shake { animation:sno-shake 0.38s cubic-bezier(.36,.07,.19,.97) both !important; transform-origin:center; }
-.sno-fx-zoom { animation:sno-zoom-fwd 0.32s ease both !important; }
-.sno-fx-glitch { animation:sno-glitch 0.3s ease both !important; }
-/* Wild mode badge — only visible (opacity:1) when .sno-wild-on is present */
-#sno-wild-badge { position:fixed; top:10px; right:12px; z-index:5000; padding:3px 12px;
- font-size:0.64rem; letter-spacing:0.2em; text-transform:uppercase; border-radius:2px;
- pointer-events:none; opacity:0; transition:opacity 0.4s;
- background:rgba(220,0,0,0.92); color:#fff; border:1px solid rgba(255,100,100,0.8);
- font-family:monospace; }
-/* Animation only runs when wild is actually on, preventing invisible badge pulse */
-#sno-wild-badge.sno-wild-on { opacity:1; animation:sno-wild-pulse 0.9s ease-in-out infinite; }
-#sno-wild-root { position:fixed; inset:0; z-index:18; pointer-events:none; opacity:0;
- transition:opacity 0.35s ease; overflow:hidden; isolation:isolate; }
-body.sno-wild-active #sno-wild-root { opacity:1; }
-#sno-wild-root .sno-wild-layer,
-#sno-wild-root #sno-wild-scraps,
-#sno-wild-root #sno-wild-banner,
-#sno-wild-root #sno-wild-ticker { position:absolute; inset:0; }
-#sno-wild-colorwash {
- background-image:var(--sno-wild-colorwash, none);
- background-size:var(--sno-wild-colorwash-size, cover);
- background-position:center;
- mix-blend-mode:screen;
- opacity:var(--sno-wild-colorwash-opacity, 0.55);
- animation:sno-wild-drift 9s ease-in-out infinite;
-}
-#sno-wild-rain {
- background-image:var(--sno-wild-rain, none);
- background-size:var(--sno-wild-rain-size, 220px 220px);
- background-repeat:repeat;
- background-position:0 0;
- mix-blend-mode:screen;
- opacity:var(--sno-wild-rain-opacity, 0.18);
- animation:sno-wild-rainfall var(--sno-wild-rain-speed, 1.2s) linear infinite;
-}
-#sno-wild-wave {
- inset:auto 0 -8vh 0;
- height:70vh;
- background:var(--sno-wild-wave, none);
- opacity:var(--sno-wild-wave-opacity, 0.32);
- transform-origin:50% 100%;
- animation:sno-wild-surge 6.4s ease-in-out infinite;
-}
-#sno-wild-beacon {
- background:var(--sno-wild-beacon, none);
- opacity:var(--sno-wild-beacon-opacity, 0.38);
- filter:blur(var(--sno-wild-beacon-blur, 0px));
- mix-blend-mode:screen;
- animation:sno-wild-lens 4.5s ease-in-out infinite;
-}
-#sno-wild-noise {
- background-image:
- linear-gradient(90deg, rgba(255,255,255,0.08) 0 1px, transparent 1px 100%),
- linear-gradient(180deg, rgba(255,255,255,0.07) 0 1px, transparent 1px 100%),
- repeating-linear-gradient(180deg,
- rgba(255,255,255,0.08) 0 2px,
- rgba(0,0,0,0.02) 2px 4px,
- transparent 4px 8px);
- background-size:3px 3px, 100% 3px, 100% 8px;
- mix-blend-mode:overlay;
- opacity:var(--sno-wild-noise-opacity, 0.12);
- animation:sno-wild-static-shift 0.22s steps(2) infinite;
-}
-#sno-wild-banner {
- inset:18px auto auto 50%;
- transform:translateX(-50%);
- width:max-content;
- max-width:min(92vw, 920px);
- height:auto;
- padding:10px 18px;
- border:1px solid var(--sno-wild-accent, rgba(255,255,255,0.8));
- background:var(--sno-wild-banner-bg, rgba(0,0,0,0.82));
- color:var(--sno-wild-banner-color, #fff);
- font:700 clamp(0.75rem, 1vw + 0.55rem, 1.15rem)/1.1 monospace;
- letter-spacing:0.38em;
- text-transform:uppercase;
- text-align:center;
- box-shadow:0 0 28px var(--sno-wild-banner-glow, rgba(255,255,255,0.25));
- text-shadow:0 0 18px var(--sno-wild-banner-glow, rgba(255,255,255,0.25));
- white-space:nowrap;
- animation:sno-wild-banner-flicker 1.2s steps(2) infinite;
-}
-#sno-wild-ticker {
- inset:auto 0 12px 0;
- height:30px;
- overflow:hidden;
- opacity:0.92;
- color:var(--sno-wild-accent, #fff);
- font:700 0.76rem/30px monospace;
- letter-spacing:0.32em;
- text-transform:uppercase;
- text-shadow:0 0 14px var(--sno-wild-banner-glow, rgba(255,255,255,0.28));
-}
-#sno-wild-ticker span {
- position:absolute;
- left:0;
- top:0;
- display:inline-block;
- white-space:nowrap;
- padding-left:100%;
- animation:sno-wild-marquee 13s linear infinite;
-}
-#sno-wild-scraps span {
- position:absolute;
- left:var(--x);
- top:var(--y);
- transform:rotate(var(--rot));
- color:var(--sno-wild-scrap-color, rgba(255,255,255,0.82));
- font:700 clamp(0.6rem, 0.45rem + 0.45vw, 0.94rem)/1 monospace;
- letter-spacing:0.24em;
- text-transform:uppercase;
- white-space:nowrap;
- opacity:0;
- text-shadow:0 0 10px var(--sno-wild-banner-glow, rgba(255,255,255,0.18));
- animation:sno-wild-scrap var(--dur, 6s) linear infinite;
- animation-delay:var(--delay, 0s);
-}
-body.sno-wild-active .overlay,
-body.sno-wild-active header,
-body.sno-wild-active .post,
-body.sno-wild-active .page-nav-footer { will-change:transform, filter, opacity; }
-body.sno-wild-active .overlay { position:relative; z-index:24 !important; isolation:isolate; }
-body.sno-wild-active .post:not(.post-active) {
- opacity:0.32 !important;
- filter:saturate(0.78) brightness(0.82);
-}
-body.sno-wild-active .post.post-active {
- z-index:2;
- opacity:1 !important;
- background:rgba(0,0,0,0.84) !important;
- backdrop-filter:blur(14px) saturate(0.94);
- -webkit-backdrop-filter:blur(14px) saturate(0.94);
- box-shadow:
- 0 0 0 2px var(--sno-wild-accent, rgba(255,255,255,0.82)),
- 0 18px 44px rgba(0,0,0,0.55),
- inset 0 0 0 1px rgba(255,255,255,0.06) !important;
- text-shadow:none !important;
- filter:none !important;
- animation:none !important;
- transform:none !important;
-}
-body.sno-wild-active .post.post-active::after {
- content:"";
- position:absolute;
- inset:0;
- border-radius:inherit;
- pointer-events:none;
- background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.01) 38%, rgba(0,0,0,0.08));
-}
-body.sno-wild-active .post.post-active > * { position:relative; z-index:1; }
-@keyframes sno-wild-drift {
- 0%,100% { transform:translate3d(0,0,0) scale(1); }
- 50% { transform:translate3d(-2%,1.5%,0) scale(1.08); }
-}
-@keyframes sno-wild-rainfall {
- from { background-position:0 0, 0 0, 0 0; }
- to { background-position:0 280px, 160px 420px, -140px 640px; }
-}
-@keyframes sno-wild-surge {
- 0%,100% { transform:translateY(16%) scaleY(0.92) skewX(0deg); }
- 50% { transform:translateY(-8%) scaleY(1.12) skewX(-4deg); }
-}
-@keyframes sno-wild-lens {
- 0%,100% { transform:scale(0.9); opacity:0.18; }
- 50% { transform:scale(1.24); opacity:0.52; }
-}
-@keyframes sno-wild-static-shift {
- 0% { transform:translate3d(0,0,0); }
- 25% { transform:translate3d(-1.2%,0.6%,0); }
- 50% { transform:translate3d(1%, -0.6%,0); }
- 75% { transform:translate3d(0.7%,1.1%,0); }
- 100% { transform:translate3d(0,0,0); }
-}
-@keyframes sno-wild-banner-flicker {
- 0%,100% { opacity:0.95; }
- 12% { opacity:0.4; }
- 13% { opacity:1; }
- 32% { opacity:0.6; }
- 33% { opacity:1; }
- 61% { opacity:0.48; }
- 62% { opacity:0.98; }
-}
-@keyframes sno-wild-marquee {
- from { transform:translateX(0); }
- to { transform:translateX(-46%); }
-}
-@keyframes sno-wild-scrap {
- 0% { transform:translate3d(0,0,0) rotate(var(--rot)); opacity:0; }
- 12% { opacity:0.78; }
- 45% { transform:translate3d(var(--dx), var(--dy), 0) rotate(calc(var(--rot) + 10deg)); opacity:0.92; }
- 100% { transform:translate3d(calc(var(--dx) * -0.75), calc(var(--dy) * -0.75), 0) rotate(calc(var(--rot) - 8deg)); opacity:0.18; }
-}
-@keyframes sno-wild-hard-blink {
- 0%,44%,100% { filter:invert(0) grayscale(0) contrast(1.04); }
- 45%,49% { filter:invert(1) grayscale(1) contrast(1.8); }
- 50%,53% { filter:invert(0) grayscale(0) contrast(1.15); }
- 54%,58% { filter:invert(1) grayscale(1) contrast(1.9); }
-}
-@keyframes sno-wild-jitter {
- 0%,100% { transform:translate(0,0); }
- 20% { transform:translate(-4px,2px); }
- 40% { transform:translate(5px,-4px); }
- 60% { transform:translate(-3px,4px); }
- 80% { transform:translate(3px,-1px); }
-}
-@keyframes sno-wild-roll {
- 0%,100% { transform:translateY(0); }
- 28% { transform:translateY(-12px); }
- 29% { transform:translateY(16px); }
- 58% { transform:translateY(-8px); }
- 59% { transform:translateY(6px); }
-}
-@keyframes sno-wild-collapse {
- 0%,100% { transform:perspective(1200px) rotateX(0deg) scale(1); }
- 50% { transform:perspective(1200px) rotateX(10deg) scale(1.04) translateY(1.2%); }
-}
-@keyframes sno-wild-compress {
- 0%,100% { transform:scaleY(1); }
- 40% { transform:scaleY(0.9); }
- 70% { transform:scaleY(1.08); }
-}
-@keyframes sno-wild-text-burn {
- 0%,100% { text-shadow:0 0 0 transparent; }
- 33% { text-shadow:-2px 0 rgba(255,60,60,0.75), 2px 0 rgba(100,255,255,0.7); }
- 66% { text-shadow:2px 0 rgba(255,240,80,0.75), -2px 0 rgba(90,90,255,0.7); }
-}
-body[data-sno-theme="aurora"] {
- --sno-wild-colorwash:
- linear-gradient(112deg, transparent 0 16%, rgba(0,255,179,0.2) 26%, transparent 38%, rgba(0,207,232,0.24) 48%, transparent 60%, rgba(192,132,252,0.24) 70%, transparent 84%),
- radial-gradient(circle at 50% 28%, rgba(255,255,255,0.18) 0%, rgba(0,255,179,0.08) 20%, transparent 48%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 68%, rgba(236,255,255,0.9) 76% 80%, transparent 88%),
- linear-gradient(180deg, transparent 0 72%, rgba(0,255,179,0.6) 80% 84%, transparent 92%),
- linear-gradient(180deg, transparent 0 64%, rgba(192,132,252,0.55) 72% 78%, transparent 86%);
- --sno-wild-rain-size:160px 240px, 220px 280px, 290px 330px;
- --sno-wild-rain-opacity:0.3;
- --sno-wild-rain-speed:1.05s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,255,179,0.08) 38%, rgba(192,132,252,0.22) 76%, rgba(3,8,17,0.88) 100%);
- --sno-wild-wave-opacity:0.62;
- --sno-wild-beacon:radial-gradient(circle at 50% 24%, rgba(255,255,255,0.3) 0%, rgba(0,255,179,0.14) 18%, transparent 54%);
- --sno-wild-accent:#8fffe7;
- --sno-wild-banner-bg:rgba(3,16,28,0.74);
- --sno-wild-banner-color:#e9fff8;
- --sno-wild-banner-glow:rgba(0,255,179,0.34);
- --sno-wild-scrap-color:rgba(224,248,240,0.8);
-}
-body.sno-wild-active[data-sno-theme="aurora"] header,
-body.sno-wild-active[data-sno-theme="aurora"] .nav-hints,
-body.sno-wild-active[data-sno-theme="aurora"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="aurora"] .page-nav-footer { animation:sno-wild-text-burn 1.9s steps(1) infinite; }
-body[data-sno-theme="brutalist"] {
- --sno-wild-colorwash:
- linear-gradient(134deg, transparent 0 24%, rgba(255,255,255,0.08) 24.5% 25.2%, transparent 26% 42%, rgba(255,255,255,0.06) 42.5% 43.2%, transparent 44%),
- linear-gradient(28deg, transparent 0 30%, rgba(195,32,32,0.22) 30.5% 31.5%, transparent 32% 65%, rgba(255,255,255,0.07) 65.4% 66.2%, transparent 67%),
- radial-gradient(circle at 50% 10%, rgba(255,130,80,0.18) 0%, transparent 44%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 72%, rgba(133,65,22,0.9) 82% 84%, transparent 90%),
- linear-gradient(180deg, transparent 0 60%, rgba(255,255,255,0.35) 70% 72%, transparent 80%);
- --sno-wild-rain-size:240px 300px, 340px 360px;
- --sno-wild-rain-opacity:0.22;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(24,24,24,0.22) 38%, rgba(92,16,16,0.4) 78%, rgba(0,0,0,0.88) 100%);
- --sno-wild-accent:#ff5d52;
- --sno-wild-banner-bg:rgba(12,12,12,0.84);
- --sno-wild-banner-color:#fff3ef;
- --sno-wild-banner-glow:rgba(255,93,82,0.28);
- --sno-wild-scrap-color:rgba(255,214,204,0.82);
-}
-body.sno-wild-active[data-sno-theme="brutalist"] header,
-body.sno-wild-active[data-sno-theme="brutalist"] .nav-hints,
-body.sno-wild-active[data-sno-theme="brutalist"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="brutalist"] .page-nav-footer { animation:sno-wild-jitter 0.22s steps(2) infinite; }
-body[data-sno-theme="cosmos"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 48%, rgba(255,255,255,0.5) 0 2%, rgba(99,0,164,0.5) 6%, rgba(12,4,33,0.05) 16%, transparent 26%),
- radial-gradient(circle at 50% 48%, transparent 0 22%, rgba(255,255,255,0.18) 26% 27%, transparent 31%),
- conic-gradient(from 0deg at 50% 48%, rgba(255,255,255,0.28), transparent 18%, rgba(135,206,250,0.2) 32%, transparent 52%, rgba(255,240,150,0.24) 68%, transparent 84%, rgba(255,255,255,0.28));
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 70%, rgba(255,255,255,0.92) 80% 82%, transparent 90%),
- linear-gradient(180deg, transparent 0 72%, rgba(180,128,255,0.7) 82% 84%, transparent 92%);
- --sno-wild-rain-size:260px 280px, 320px 360px;
- --sno-wild-rain-opacity:0.26;
- --sno-wild-beacon:radial-gradient(circle at 50% 48%, rgba(255,255,255,0.75) 0%, rgba(155,115,255,0.24) 14%, transparent 36%);
- --sno-wild-wave:radial-gradient(circle at 50% 110%, rgba(255,255,255,0.12) 0%, rgba(20,6,44,0.52) 45%, rgba(2,2,12,0.94) 100%);
- --sno-wild-accent:#f8d9ff;
- --sno-wild-banner-bg:rgba(8,6,24,0.82);
- --sno-wild-banner-color:#ffffff;
- --sno-wild-banner-glow:rgba(238,214,255,0.34);
- --sno-wild-scrap-color:rgba(240,228,255,0.82);
-}
-body.sno-wild-active[data-sno-theme="cosmos"] header,
-body.sno-wild-active[data-sno-theme="cosmos"] .nav-hints,
-body.sno-wild-active[data-sno-theme="cosmos"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="cosmos"] .page-nav-footer { animation:sno-wild-collapse 4.2s ease-in-out infinite; }
-body[data-sno-theme="dos"] {
- --sno-wild-colorwash:
- linear-gradient(90deg, rgba(255,255,255,0.12) 0 50%, rgba(0,0,0,0.16) 50% 100%),
- radial-gradient(circle at 50% 14%, rgba(85,255,255,0.18) 0%, transparent 38%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 62%, rgba(255,255,255,0.95) 68% 72%, transparent 78%),
- linear-gradient(180deg, transparent 0 54%, rgba(85,255,255,0.85) 60% 64%, transparent 70%),
- linear-gradient(180deg, transparent 0 70%, rgba(255,255,85,0.75) 76% 80%, transparent 86%);
- --sno-wild-rain-size:120px 180px, 190px 220px, 280px 260px;
- --sno-wild-rain-opacity:0.34;
- --sno-wild-rain-speed:0.62s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,170,0.18) 42%, rgba(0,0,0,0.82) 100%);
- --sno-wild-accent:#ffff55;
- --sno-wild-banner-bg:#0000aa;
- --sno-wild-banner-color:#ffffff;
- --sno-wild-banner-glow:rgba(85,255,255,0.28);
- --sno-wild-scrap-color:#ffff55;
-}
-body.sno-wild-active[data-sno-theme="dos"] header,
-body.sno-wild-active[data-sno-theme="dos"] .nav-hints,
-body.sno-wild-active[data-sno-theme="dos"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="dos"] .page-nav-footer {
- animation:sno-wild-hard-blink 0.72s steps(1) infinite, sno-wild-jitter 0.14s steps(2) infinite;
-}
-body[data-sno-theme="matrix"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 0%, rgba(0,255,65,0.18) 0%, transparent 42%),
- linear-gradient(180deg, rgba(0,40,0,0) 0%, rgba(0,255,65,0.09) 60%, rgba(0,0,0,0.62) 100%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 54%, rgba(187,255,210,1) 62% 68%, transparent 76%),
- linear-gradient(180deg, transparent 0 44%, rgba(0,255,65,0.9) 52% 58%, transparent 66%),
- linear-gradient(180deg, transparent 0 68%, rgba(140,255,180,0.72) 76% 82%, transparent 90%);
- --sno-wild-rain-size:90px 180px, 130px 210px, 200px 280px;
- --sno-wild-rain-opacity:0.5;
- --sno-wild-rain-speed:0.38s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,255,65,0.04) 40%, rgba(0,0,0,0.92) 100%);
- --sno-wild-accent:#98ffac;
- --sno-wild-banner-bg:rgba(0,10,0,0.78);
- --sno-wild-banner-color:#d8ffe2;
- --sno-wild-banner-glow:rgba(0,255,65,0.32);
- --sno-wild-scrap-color:rgba(152,255,172,0.84);
-}
-body.sno-wild-active[data-sno-theme="matrix"] header,
-body.sno-wild-active[data-sno-theme="matrix"] .nav-hints,
-body.sno-wild-active[data-sno-theme="matrix"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="matrix"] .page-nav-footer { animation:sno-wild-text-burn 1.1s steps(1) infinite; }
-body[data-sno-theme="neon"] {
- --sno-wild-colorwash:
- conic-gradient(from 90deg at 50% 20%, rgba(255,231,0,0.16), transparent 18%, rgba(255,0,204,0.22) 32%, transparent 50%, rgba(0,245,255,0.24) 66%, transparent 84%, rgba(255,231,0,0.16)),
- radial-gradient(circle at 50% 15%, rgba(255,255,255,0.22) 0%, rgba(0,245,255,0.08) 26%, transparent 56%);
- --sno-wild-rain:
- linear-gradient(118deg, transparent 0 46%, rgba(255,255,255,0.95) 49% 50%, transparent 53%),
- linear-gradient(62deg, transparent 0 45%, rgba(0,245,255,0.75) 48% 49%, transparent 52%),
- linear-gradient(132deg, transparent 0 42%, rgba(255,0,204,0.82) 45% 46%, transparent 49%);
- --sno-wild-rain-size:280px 280px, 360px 360px, 420px 420px;
- --sno-wild-rain-opacity:0.34;
- --sno-wild-rain-speed:0.72s;
- --sno-wild-beacon:radial-gradient(circle at 50% 20%, rgba(255,255,255,0.44) 0%, rgba(255,0,204,0.18) 14%, transparent 42%);
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,0,204,0.08) 44%, rgba(11,0,26,0.88) 100%);
- --sno-wild-accent:#ffe700;
- --sno-wild-banner-bg:rgba(11,0,26,0.8);
- --sno-wild-banner-color:#faff9c;
- --sno-wild-banner-glow:rgba(255,231,0,0.42);
- --sno-wild-scrap-color:rgba(255,245,174,0.88);
-}
-body.sno-wild-active[data-sno-theme="neon"] header,
-body.sno-wild-active[data-sno-theme="neon"] .nav-hints,
-body.sno-wild-active[data-sno-theme="neon"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="neon"] .page-nav-footer { animation:sno-wild-text-burn 1.3s steps(1) infinite; }
-body[data-sno-theme="ocean"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 120%, rgba(0,180,255,0.22) 0%, rgba(0,31,63,0.08) 34%, transparent 56%),
- radial-gradient(circle at 20% 28%, rgba(147,255,227,0.16) 0%, transparent 32%),
- radial-gradient(circle at 78% 22%, rgba(120,255,235,0.14) 0%, transparent 30%);
- --sno-wild-rain:
- linear-gradient(165deg, transparent 0 58%, rgba(220,255,255,0.95) 62% 66%, transparent 72%),
- linear-gradient(175deg, transparent 0 48%, rgba(98,255,228,0.72) 52% 56%, transparent 62%),
- linear-gradient(170deg, transparent 0 68%, rgba(170,255,240,0.78) 72% 76%, transparent 82%);
- --sno-wild-rain-size:170px 260px, 240px 320px, 310px 360px;
- --sno-wild-rain-opacity:0.26;
- --sno-wild-rain-speed:0.82s;
- --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(0,255,220,0.3) 0%, rgba(0,80,120,0.32) 28%, rgba(1,20,40,0.92) 74%);
- --sno-wild-wave-opacity:0.86;
- --sno-wild-beacon:radial-gradient(circle at 50% 88%, rgba(147,255,227,0.18) 0%, transparent 38%);
- --sno-wild-accent:#bafcff;
- --sno-wild-banner-bg:rgba(0,24,42,0.8);
- --sno-wild-banner-color:#e1ffff;
- --sno-wild-banner-glow:rgba(120,255,235,0.34);
- --sno-wild-scrap-color:rgba(202,240,248,0.86);
-}
-body.sno-wild-active[data-sno-theme="ocean"] header,
-body.sno-wild-active[data-sno-theme="ocean"] .nav-hints,
-body.sno-wild-active[data-sno-theme="ocean"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="ocean"] .page-nav-footer { animation:sno-wild-collapse 5.2s ease-in-out infinite; }
-body[data-sno-theme="plasma"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 48%, rgba(255,255,255,0.66) 0%, rgba(165,230,255,0.18) 10%, transparent 24%),
- conic-gradient(from 0deg at 50% 48%, rgba(255,255,255,0.28), rgba(157,255,255,0.16), rgba(251,146,255,0.28), rgba(255,255,255,0.28)),
- radial-gradient(circle at 50% 110%, rgba(65,14,90,0.34) 0%, transparent 52%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 70%, rgba(255,255,255,0.92) 78% 82%, transparent 90%),
- linear-gradient(180deg, transparent 0 60%, rgba(132,255,255,0.72) 68% 72%, transparent 80%);
- --sno-wild-rain-size:220px 260px, 320px 360px;
- --sno-wild-rain-opacity:0.28;
- --sno-wild-beacon:radial-gradient(circle at 50% 48%, rgba(255,255,255,0.78) 0%, rgba(128,224,255,0.2) 12%, transparent 36%);
- --sno-wild-wave:linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(144,220,255,0.12) 44%, rgba(8,2,20,0.9) 100%);
- --sno-wild-accent:#fef3ff;
- --sno-wild-banner-bg:rgba(18,6,36,0.8);
- --sno-wild-banner-color:#ffffff;
- --sno-wild-banner-glow:rgba(160,224,255,0.42);
- --sno-wild-scrap-color:rgba(248,240,255,0.88);
-}
-body.sno-wild-active[data-sno-theme="plasma"] header,
-body.sno-wild-active[data-sno-theme="plasma"] .nav-hints,
-body.sno-wild-active[data-sno-theme="plasma"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="plasma"] .page-nav-footer { animation:sno-wild-text-burn 1.5s steps(1) infinite; }
-body[data-sno-theme="retro"] {
- --sno-wild-colorwash:
- linear-gradient(90deg, rgba(255,80,0,0.18) 0 18%, transparent 24% 76%, rgba(255,220,80,0.14) 82% 100%),
- linear-gradient(180deg, rgba(255,255,255,0.08), transparent 16%, rgba(255,255,255,0.06) 34%, transparent 54%, rgba(255,255,255,0.05) 78%, transparent 100%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 46%, rgba(255,255,255,0.88) 52% 56%, transparent 62%),
- linear-gradient(180deg, transparent 0 64%, rgba(255,190,50,0.82) 70% 74%, transparent 80%);
- --sno-wild-rain-size:160px 220px, 260px 300px;
- --sno-wild-rain-opacity:0.24;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,190,50,0.08) 42%, rgba(10,4,0,0.9) 100%);
- --sno-wild-accent:#ffcf63;
- --sno-wild-banner-bg:rgba(24,10,0,0.82);
- --sno-wild-banner-color:#ffe7a5;
- --sno-wild-banner-glow:rgba(255,190,50,0.34);
- --sno-wild-scrap-color:rgba(255,215,120,0.86);
-}
-body.sno-wild-active[data-sno-theme="retro"] header,
-body.sno-wild-active[data-sno-theme="retro"] .nav-hints,
-body.sno-wild-active[data-sno-theme="retro"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="retro"] .page-nav-footer {
- animation:sno-wild-roll 0.85s linear infinite, sno-wild-jitter 0.18s steps(2) infinite;
-}
-body[data-sno-theme="retrofuture"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 20%, rgba(255,220,140,0.24) 0%, transparent 38%),
- linear-gradient(180deg, rgba(169,111,46,0.08) 0%, rgba(133,86,33,0.16) 60%, rgba(50,29,12,0.52) 100%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 66%, rgba(255,232,180,0.82) 74% 78%, transparent 86%),
- linear-gradient(180deg, transparent 0 58%, rgba(170,120,60,0.7) 66% 70%, transparent 78%);
- --sno-wild-rain-size:180px 240px, 260px 320px;
- --sno-wild-rain-opacity:0.22;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,206,128,0.07) 42%, rgba(28,18,9,0.92) 100%);
- --sno-wild-accent:#ffd793;
- --sno-wild-banner-bg:rgba(26,16,8,0.82);
- --sno-wild-banner-color:#fff4d6;
- --sno-wild-banner-glow:rgba(255,215,147,0.3);
- --sno-wild-scrap-color:rgba(255,233,190,0.86);
-}
-body.sno-wild-active[data-sno-theme="retrofuture"] header,
-body.sno-wild-active[data-sno-theme="retrofuture"] .nav-hints,
-body.sno-wild-active[data-sno-theme="retrofuture"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="retrofuture"] .page-nav-footer { animation:sno-wild-text-burn 1.6s steps(1) infinite; }
-body[data-sno-theme="spaceage"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(255,170,90,0.16) 0%, transparent 24%, rgba(80,220,255,0.12) 54%, transparent 100%),
- radial-gradient(circle at 50% -10%, rgba(255,255,255,0.46) 0%, rgba(255,180,120,0.16) 18%, transparent 42%);
- --sno-wild-rain:
- linear-gradient(160deg, transparent 0 52%, rgba(255,220,190,0.95) 58% 62%, transparent 68%),
- linear-gradient(170deg, transparent 0 42%, rgba(255,138,84,0.74) 48% 52%, transparent 58%),
- linear-gradient(180deg, transparent 0 62%, rgba(120,220,255,0.68) 68% 72%, transparent 78%);
- --sno-wild-rain-size:180px 260px, 260px 320px, 360px 420px;
- --sno-wild-rain-opacity:0.28;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,149,90,0.08) 44%, rgba(0,10,18,0.9) 100%);
- --sno-wild-accent:#ffd0a8;
- --sno-wild-banner-bg:rgba(3,18,26,0.82);
- --sno-wild-banner-color:#fff1e0;
- --sno-wild-banner-glow:rgba(255,172,124,0.34);
- --sno-wild-scrap-color:rgba(255,225,206,0.82);
-}
-body.sno-wild-active[data-sno-theme="spaceage"] header,
-body.sno-wild-active[data-sno-theme="spaceage"] .nav-hints,
-body.sno-wild-active[data-sno-theme="spaceage"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="spaceage"] .page-nav-footer {
- animation:sno-wild-compress 1.8s ease-in-out infinite, sno-wild-jitter 0.42s steps(2) infinite;
-}
-body[data-sno-theme="synthwave"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(255,20,147,0.14) 0%, transparent 20%, rgba(0,255,255,0.12) 52%, transparent 74%),
- radial-gradient(circle at 50% 18%, rgba(255,255,255,0.34) 0%, rgba(255,20,147,0.14) 14%, transparent 36%),
- linear-gradient(90deg, rgba(255,20,147,0.14) 0 1px, transparent 1px 11%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 68%, rgba(255,255,255,0.95) 76% 80%, transparent 88%),
- linear-gradient(180deg, transparent 0 60%, rgba(255,20,147,0.82) 68% 72%, transparent 80%),
- linear-gradient(180deg, transparent 0 50%, rgba(0,255,255,0.74) 58% 62%, transparent 70%);
- --sno-wild-rain-size:160px 220px, 260px 320px, 360px 400px;
- --sno-wild-rain-opacity:0.32;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,20,147,0.08) 42%, rgba(5,1,22,0.92) 100%);
- --sno-wild-accent:#ff7de9;
- --sno-wild-banner-bg:rgba(18,0,34,0.84);
- --sno-wild-banner-color:#ffe9ff;
- --sno-wild-banner-glow:rgba(255,125,233,0.4);
- --sno-wild-scrap-color:rgba(255,225,255,0.86);
-}
-body.sno-wild-active[data-sno-theme="synthwave"] header,
-body.sno-wild-active[data-sno-theme="synthwave"] .nav-hints,
-body.sno-wild-active[data-sno-theme="synthwave"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="synthwave"] .page-nav-footer { animation:sno-wild-collapse 3.8s ease-in-out infinite; }
-body[data-sno-theme="terminal"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(51,255,51,0.14) 0%, transparent 24%, rgba(0,0,0,0.14) 46%, transparent 68%, rgba(51,255,51,0.1) 100%),
- radial-gradient(circle at 50% 10%, rgba(255,255,255,0.18) 0%, transparent 38%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 52%, rgba(255,255,255,0.94) 58% 62%, transparent 68%),
- linear-gradient(180deg, transparent 0 44%, rgba(51,255,51,0.9) 50% 54%, transparent 60%),
- linear-gradient(180deg, transparent 0 64%, rgba(140,255,140,0.7) 70% 74%, transparent 80%);
- --sno-wild-rain-size:100px 180px, 140px 210px, 220px 260px;
- --sno-wild-rain-opacity:0.4;
- --sno-wild-rain-speed:0.42s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(51,255,51,0.06) 42%, rgba(0,0,0,0.95) 100%);
- --sno-wild-accent:#8dff8d;
- --sno-wild-banner-bg:rgba(0,10,0,0.8);
- --sno-wild-banner-color:#d7ffd7;
- --sno-wild-banner-glow:rgba(51,255,51,0.34);
- --sno-wild-scrap-color:rgba(141,255,141,0.84);
-}
-body.sno-wild-active[data-sno-theme="terminal"] header,
-body.sno-wild-active[data-sno-theme="terminal"] .nav-hints,
-body.sno-wild-active[data-sno-theme="terminal"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="terminal"] .page-nav-footer {
- animation:sno-wild-hard-blink 0.82s steps(1) infinite, sno-wild-jitter 0.16s steps(2) infinite;
-}
-body.sno-wild-active[data-sno-theme="terminal"] .post.post-active {
- filter:invert(1) grayscale(1) contrast(1.28) !important;
- background:#fff !important;
- color:#000 !important;
- text-shadow:none !important;
-}
-body.sno-wild-active[data-sno-theme="terminal"] .post.post-active a { color:#000 !important; }
-body.sno-wild-active[data-sno-theme="terminal"] .post.post-active .post-time,
-body.sno-wild-active[data-sno-theme="terminal"] .post.post-active .post-header strong { color:#000 !important; }
-body[data-sno-theme="tropicale"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(255,255,255,0.12) 0%, transparent 18%, rgba(0,110,160,0.1) 46%, rgba(0,40,70,0.38) 100%),
- radial-gradient(circle at 50% 110%, rgba(0,210,255,0.24) 0%, transparent 44%);
- --sno-wild-rain:
- linear-gradient(168deg, transparent 0 54%, rgba(255,255,255,0.98) 60% 64%, transparent 70%),
- linear-gradient(176deg, transparent 0 42%, rgba(200,255,255,0.72) 48% 52%, transparent 58%),
- linear-gradient(172deg, transparent 0 64%, rgba(255,230,180,0.52) 70% 74%, transparent 80%);
- --sno-wild-rain-size:150px 260px, 230px 320px, 320px 400px;
- --sno-wild-rain-opacity:0.44;
- --sno-wild-rain-speed:0.5s;
- --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(175,255,255,0.28) 0%, rgba(0,135,190,0.32) 26%, rgba(0,24,44,0.92) 74%);
- --sno-wild-wave-opacity:0.88;
- --sno-wild-accent:#fff2bf;
- --sno-wild-banner-bg:rgba(0,34,56,0.82);
- --sno-wild-banner-color:#fff7db;
- --sno-wild-banner-glow:rgba(255,242,191,0.34);
- --sno-wild-scrap-color:rgba(255,244,213,0.84);
-}
-body.sno-wild-active[data-sno-theme="tropicale"] header,
-body.sno-wild-active[data-sno-theme="tropicale"] .nav-hints,
-body.sno-wild-active[data-sno-theme="tropicale"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="tropicale"] .page-nav-footer { animation:sno-wild-jitter 0.3s steps(2) infinite; }
-body[data-sno-theme="volcano"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(60,60,60,0.28) 0%, rgba(40,40,40,0.18) 26%, transparent 48%, rgba(255,140,0,0.18) 72%, rgba(255,214,102,0.14) 100%),
- radial-gradient(circle at 50% 8%, rgba(255,255,255,0.24) 0%, rgba(255,150,60,0.1) 18%, transparent 42%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 42%, rgba(110,110,110,0.92) 48% 54%, transparent 60%),
- linear-gradient(180deg, transparent 0 54%, rgba(255,170,80,0.82) 60% 66%, transparent 72%),
- linear-gradient(180deg, transparent 0 64%, rgba(255,235,140,0.54) 70% 76%, transparent 82%);
- --sno-wild-rain-size:150px 230px, 220px 300px, 320px 360px;
- --sno-wild-rain-opacity:0.38;
- --sno-wild-rain-speed:0.68s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,120,20,0.1) 42%, rgba(55,20,0,0.9) 100%);
- --sno-wild-accent:#ffdf76;
- --sno-wild-banner-bg:rgba(22,8,0,0.84);
- --sno-wild-banner-color:#fff0c2;
- --sno-wild-banner-glow:rgba(255,190,70,0.34);
- --sno-wild-scrap-color:rgba(255,230,170,0.86);
-}
-body.sno-wild-active[data-sno-theme="volcano"] header,
-body.sno-wild-active[data-sno-theme="volcano"] .nav-hints,
-body.sno-wild-active[data-sno-theme="volcano"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="volcano"] .page-nav-footer { animation:sno-wild-jitter 0.24s steps(2) infinite; }
-body[data-sno-theme="noir"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 18% 18%, rgba(36,65,130,0.2) 0%, transparent 24%),
- radial-gradient(circle at 82% 24%, rgba(169,55,43,0.24) 0%, transparent 24%),
- linear-gradient(180deg, rgba(240,234,214,0.08) 0%, transparent 26%, rgba(0,0,0,0.14) 48%, transparent 72%, rgba(240,234,214,0.06) 100%);
- --sno-wild-rain:
- linear-gradient(168deg, transparent 0 56%, rgba(255,255,255,0.96) 62% 66%, transparent 72%),
- linear-gradient(174deg, transparent 0 48%, rgba(210,220,255,0.72) 54% 58%, transparent 64%),
- linear-gradient(170deg, transparent 0 64%, rgba(255,170,170,0.68) 70% 74%, transparent 80%);
- --sno-wild-rain-size:170px 260px, 250px 340px, 340px 420px;
- --sno-wild-rain-opacity:0.34;
- --sno-wild-rain-speed:0.58s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,255,255,0.04) 44%, rgba(0,0,0,0.96) 100%);
- --sno-wild-beacon:linear-gradient(90deg, rgba(36,65,130,0.12), transparent 28%, rgba(169,55,43,0.14) 72%, transparent);
- --sno-wild-accent:#f0ead6;
- --sno-wild-banner-bg:rgba(5,5,5,0.84);
- --sno-wild-banner-color:#fff6e2;
- --sno-wild-banner-glow:rgba(240,234,214,0.28);
- --sno-wild-scrap-color:rgba(255,242,224,0.84);
-}
-body.sno-wild-active[data-sno-theme="noir"] header,
-body.sno-wild-active[data-sno-theme="noir"] .nav-hints,
-body.sno-wild-active[data-sno-theme="noir"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="noir"] .page-nav-footer { animation:sno-wild-roll 1.1s linear infinite, sno-wild-text-burn 1.8s steps(1) infinite; }
-body[data-sno-theme="cathedral"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 10%, rgba(217,191,120,0.22) 0%, transparent 26%),
- conic-gradient(from 0deg at 50% 18%, rgba(79,127,209,0.18), rgba(113,35,61,0.22), rgba(217,191,120,0.18), rgba(79,127,209,0.18)),
- linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 26%, rgba(0,0,0,0.18) 100%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 64%, rgba(255,220,140,0.92) 72% 76%, transparent 84%),
- linear-gradient(180deg, transparent 0 58%, rgba(255,140,90,0.72) 66% 70%, transparent 78%),
- linear-gradient(180deg, transparent 0 52%, rgba(120,160,255,0.66) 60% 64%, transparent 72%);
- --sno-wild-rain-size:180px 220px, 260px 320px, 320px 420px;
- --sno-wild-rain-opacity:0.26;
- --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(217,191,120,0.18) 0%, rgba(113,35,61,0.24) 34%, rgba(8,6,10,0.94) 78%);
- --sno-wild-beacon:radial-gradient(circle at 50% 18%, rgba(255,244,210,0.36) 0%, rgba(217,191,120,0.16) 18%, transparent 40%);
- --sno-wild-accent:#f5d89a;
- --sno-wild-banner-bg:rgba(18,11,20,0.84);
- --sno-wild-banner-color:#fff4d8;
- --sno-wild-banner-glow:rgba(217,191,120,0.34);
- --sno-wild-scrap-color:rgba(255,239,206,0.86);
-}
-body.sno-wild-active[data-sno-theme="cathedral"] header,
-body.sno-wild-active[data-sno-theme="cathedral"] .nav-hints,
-body.sno-wild-active[data-sno-theme="cathedral"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="cathedral"] .page-nav-footer { animation:sno-wild-collapse 4.4s ease-in-out infinite, sno-wild-text-burn 1.7s steps(1) infinite; }
-body[data-sno-theme="surveillance"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(99,243,168,0.12) 0%, transparent 22%, rgba(255,77,92,0.08) 56%, transparent 100%),
- linear-gradient(90deg, rgba(99,243,168,0.08) 0 1px, transparent 1px 8%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 52%, rgba(188,255,212,0.96) 58% 62%, transparent 68%),
- linear-gradient(180deg, transparent 0 44%, rgba(99,243,168,0.82) 50% 54%, transparent 60%),
- linear-gradient(180deg, transparent 0 62%, rgba(255,77,92,0.68) 68% 72%, transparent 78%);
- --sno-wild-rain-size:100px 180px, 160px 220px, 260px 320px;
- --sno-wild-rain-opacity:0.4;
- --sno-wild-rain-speed:0.38s;
- --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(99,243,168,0.06) 42%, rgba(0,0,0,0.95) 100%);
- --sno-wild-accent:#bcffd4;
- --sno-wild-banner-bg:rgba(6,12,9,0.84);
- --sno-wild-banner-color:#ecfff5;
- --sno-wild-banner-glow:rgba(99,243,168,0.34);
- --sno-wild-scrap-color:rgba(188,255,212,0.86);
-}
-body.sno-wild-active[data-sno-theme="surveillance"] header,
-body.sno-wild-active[data-sno-theme="surveillance"] .nav-hints,
-body.sno-wild-active[data-sno-theme="surveillance"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="surveillance"] .page-nav-footer { animation:sno-wild-hard-blink 0.94s steps(1) infinite, sno-wild-jitter 0.18s steps(2) infinite; }
-body[data-sno-theme="biomech"] {
- --sno-wild-colorwash:
- radial-gradient(circle at 50% 44%, rgba(245,91,125,0.22) 0%, transparent 24%),
- conic-gradient(from 0deg at 50% 44%, rgba(147,255,216,0.2), rgba(128,63,93,0.28), rgba(208,199,187,0.14), rgba(147,255,216,0.2)),
- linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(147,255,216,0.04) 40%, rgba(0,0,0,0.24) 100%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 64%, rgba(245,91,125,0.86) 72% 76%, transparent 84%),
- linear-gradient(180deg, transparent 0 56%, rgba(147,255,216,0.84) 64% 68%, transparent 76%),
- linear-gradient(180deg, transparent 0 48%, rgba(208,199,187,0.62) 56% 60%, transparent 68%);
- --sno-wild-rain-size:150px 220px, 240px 340px, 320px 420px;
- --sno-wild-rain-opacity:0.3;
- --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(245,91,125,0.18) 0%, rgba(128,63,93,0.3) 32%, rgba(5,4,8,0.96) 78%);
- --sno-wild-beacon:radial-gradient(circle at 50% 44%, rgba(255,210,220,0.28) 0%, rgba(147,255,216,0.12) 14%, transparent 36%);
- --sno-wild-accent:#93ffd8;
- --sno-wild-banner-bg:rgba(10,8,14,0.84);
- --sno-wild-banner-color:#e7fff6;
- --sno-wild-banner-glow:rgba(147,255,216,0.34);
- --sno-wild-scrap-color:rgba(216,255,242,0.88);
-}
-body.sno-wild-active[data-sno-theme="biomech"] header,
-body.sno-wild-active[data-sno-theme="biomech"] .nav-hints,
-body.sno-wild-active[data-sno-theme="biomech"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="biomech"] .page-nav-footer { animation:sno-wild-collapse 3.6s ease-in-out infinite, sno-wild-jitter 0.22s steps(2) infinite; }
-body[data-sno-theme="paper"] {
- --sno-wild-colorwash:
- linear-gradient(180deg, rgba(184,58,46,0.1) 0%, transparent 18%, rgba(62,93,138,0.06) 52%, rgba(0,0,0,0.04) 100%),
- radial-gradient(circle at 22% 24%, rgba(0,0,0,0.05) 0%, transparent 18%),
- radial-gradient(circle at 78% 72%, rgba(0,0,0,0.05) 0%, transparent 18%);
- --sno-wild-rain:
- linear-gradient(180deg, transparent 0 54%, rgba(0,0,0,0.34) 60% 64%, transparent 70%),
- linear-gradient(180deg, transparent 0 62%, rgba(184,58,46,0.46) 68% 72%, transparent 78%),
- linear-gradient(180deg, transparent 0 48%, rgba(62,93,138,0.42) 54% 58%, transparent 64%);
- --sno-wild-rain-size:160px 220px, 260px 320px, 360px 420px;
- --sno-wild-rain-opacity:0.24;
- --sno-wild-rain-speed:0.74s;
- --sno-wild-wave:linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(0,0,0,0.03) 44%, rgba(215,199,167,0.2) 100%);
- --sno-wild-accent:#b83a2e;
- --sno-wild-banner-bg:rgba(250,244,231,0.88);
- --sno-wild-banner-color:#16120f;
- --sno-wild-banner-glow:rgba(184,58,46,0.18);
- --sno-wild-scrap-color:rgba(22,18,15,0.76);
-}
-body.sno-wild-active[data-sno-theme="paper"] header,
-body.sno-wild-active[data-sno-theme="paper"] .nav-hints,
-body.sno-wild-active[data-sno-theme="paper"] .post:not(.post-active),
-body.sno-wild-active[data-sno-theme="paper"] .page-nav-footer { animation:sno-wild-roll 0.9s linear infinite, sno-wild-text-burn 2.2s steps(1) infinite; }
-/* Host note under the site subtitle (all themes) */
-.logo-host { font-size:0.65rem; opacity:0.55; margin-top:4px; letter-spacing:0.3px; line-height:1.3; }
-/* Atom feed link in header (paired with transmit in .nav) */
-.nav { display:flex; align-items:center; gap:clamp(10px,2.2vw,20px); flex-wrap:wrap; justify-content:flex-end; }
-a.header-feed-link { font-size:0.8rem; text-decoration:none; opacity:0.82; letter-spacing:0.04em; white-space:nowrap; }
-a.header-feed-link:hover { opacity:1; text-decoration:underline; }
-/* Header logo/title can reopen the splash overlay. */
-.logo-mark, .logo-title h1, #sn-logo { cursor:pointer; }
-/* Full-viewport splash (theme-specific colours/animation on each .splash-THEMENAME) */
-#splash-overlay { position:fixed; inset:0; z-index:2000; display:flex; flex-direction:column; align-items:center;
- justify-content:center; text-align:center; padding:max(16px,4vw); box-sizing:border-box; cursor:pointer;
- transition:opacity .55s ease, visibility .55s ease, transform .55s ease; }
-#splash-overlay.splash--dismissed { opacity:0 !important; visibility:hidden !important;
- pointer-events:none !important; transform:scale(1.02); }
-#splash-overlay:focus { outline:2px solid rgba(255,255,255,0.35); outline-offset:4px; }
-/* Vignette over WebGL so 3D motion does not overpower the edges */
-#splash-overlay::before { content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
- background: radial-gradient(ellipse 92% 82% at 50% 42%, rgba(0,0,0,0) 32%, rgba(0,0,0,0.26) 68%, rgba(0,0,0,0.48) 100%); }
-.splash-title { font-weight:700; letter-spacing:0.06em; line-height:1.15; }
-.splash-tag { margin-top:0.35rem; font-size:0.76rem; letter-spacing:0.2em; text-transform:uppercase; }
-.splash-hint { margin-top:1.25rem; font-size:0.72rem; letter-spacing:0.12em; }
-#splash-overlay .splash-gl-canvas { position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0; pointer-events:none; }
-/* Frosted panel so title/tag/hint stay readable over busy shaders */
-#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);
- 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; }
-/* Images embedded in markdown posts */
-.post-text img { max-width:100%; max-height:320px; object-fit:contain; border-radius:6px; cursor:pointer; }
-#post-modal .post-text img, #modal-content img { max-height:none; cursor:default; }
-/* Markdown post typography: restore spacing stripped by the global reset.
- Rules apply to both the list-view .post-text and the zoomed-in #modal-content
- so the formatting is identical in both views. */
-.post-text p, #modal-content p { margin:0.65em 0; }
-.post-text ul, .post-text ol, #modal-content ul, #modal-content ol { margin:0.65em 0; padding-left:1.8em; }
-.post-text li, #modal-content li { margin:0.3em 0; }
-.post-text p:first-child, #modal-content p:first-child { margin-top:0; }
-.post-text p:last-child, .post-text ul:last-child, .post-text ol:last-child,
-#modal-content p:last-child, #modal-content ul:last-child, #modal-content ol:last-child { margin-bottom:0; }
-{{end}}
{{define "navmodal"}}
<div class="post-modal" id="post-modal" role="dialog" aria-modal="true" aria-label="Expanded post">
@@ -902,914 +77,3 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
</div>
{{end}}
-{{define "navscript"}}
-<script>
- const SNONUX_SOUNDS = {{.ThemeSoundsJSON}};
- // Inject wild-mode badge used by all themes
- (function() { var b=document.createElement('div'); b.id='sno-wild-badge'; b.textContent='WILD MODE'; document.body.appendChild(b); })();
- const SNONUX_WILD_PRESETS = {
- aurora: {
- banner: 'SOLAR STORM',
- ticker: ['FIELD INTERFERENCE', 'PLASMA DRIFT', 'PARTICLE BOMBARDMENT', 'CHROMATIC SPIKE'],
- scraps: ['MAGNETIC SHEAR', 'SOLAR WIND', 'AURORA NOISE', 'ION STORM', 'POLAR ARC'],
- flash: 'rgba(220,255,240,0.72)',
- emoji: ['\u2728','\u2604\uFE0F','\u{1F320}','\u{1F30C}','\u2B50']
- },
- brutalist: {
- banner: 'STRUCTURAL COLLAPSE',
- ticker: ['CONDEMNED', 'REBAR EXPOSED', 'FOUNDATION FAILURE', 'CRACK PROPAGATION'],
- scraps: ['CONDEMNED', 'RUST BLEED', 'SHEAR WALL LOST', 'LOAD PATH BROKEN', 'SPALLING'],
- flash: 'rgba(255,210,190,0.58)',
- emoji: ['\u{1F9F1}','\u2692\uFE0F','\u26A0\uFE0F','\u{1F6A7}','\u{1F4A5}']
- },
- cosmos: {
- banner: 'SUPERNOVA',
- ticker: ['SINGULARITY LENSING', 'GAMMA BURST', 'SHOCKWAVE EXPANDING', 'SPACETIME TEAR'],
- scraps: ['WHITEOUT', 'EVENT HORIZON', 'RADIATION FRONT', 'LENS LOCK', 'CORE BREACH'],
- flash: 'rgba(255,255,255,0.8)',
- emoji: ['\u{1F30C}','\u2604\uFE0F','\u{1F4AB}','\u2B50','\u{1FA90}']
- },
- dos: {
- banner: 'KERNEL PANIC',
- ticker: ['ABORT, RETRY, FAIL?', 'MEMORY CORRUPTION', 'STACK DUMP', 'SEGMENT FAULT'],
- scraps: ['DEAD BEEF', 'C0FFEE', 'BAD SECTOR', 'IRQ STORM', 'NULL PTR', 'HEX DUMP'],
- flash: 'rgba(255,255,255,0.88)',
- emoji: ['\u{1F4BE}','\u{1F4BB}','\u26A1','\u2620\uFE0F','\u{1F41B}']
- },
- matrix: {
- banner: 'CASCADE FAILURE',
- ticker: ['SENTINEL TRACE', 'GLYPH SATURATION', 'PHOSPHOR BURN-IN', 'RAIN AT TERMINAL VELOCITY'],
- scraps: ['SENTINEL', '0XDECODE', 'OVERRIDE', 'TRACE LOST', 'MACHINE DREAM'],
- flash: 'rgba(180,255,190,0.68)',
- emoji: ['\u{1F441}\uFE0F','\u{1F4A0}','\u{1F916}','\u{1F50D}','\u26D3\uFE0F']
- },
- neon: {
- banner: 'GAS DISCHARGE',
- ticker: ['TUBE ARC', 'ULTRAVIOLET BLEED', 'STROBE LOCK', 'SHORT CIRCUIT'],
- scraps: ['ARC OVERLOAD', 'PLASMA SIGN', 'NOBLE GAS', 'HARD STROBE', 'OVERDRIVE'],
- flash: 'rgba(255,245,170,0.72)',
- emoji: ['\u26A1','\u{1F4A5}','\u{1F52E}','\u2728','\u{1F388}']
- },
- ocean: {
- banner: 'HADAL DESCENT',
- ticker: ['PRESSURE SPIKE', 'BIOLUMINESCENT SWARM', 'ABYSSAL DRAG', 'TSUNAMI FRONT'],
- scraps: ['NO SURFACE', 'CRUSH DEPTH', 'TENTACLE DRIFT', 'SONAR LOST', 'DEEP CURRENT'],
- flash: 'rgba(200,255,255,0.54)',
- emoji: ['\u{1F419}','\u{1F420}','\u{1F30A}','\u{1F41A}','\u{1F9DC}']
- },
- plasma: {
- banner: 'FUSION BREACH',
- ticker: ['CONTAINMENT FAILURE', 'TOKAMAK DISTORTION', 'THERMAL RUNAWAY', 'WHITE-BLUE CORE'],
- scraps: ['ION SPRAY', 'FIELD LOSS', 'HEAT HAZE', 'QUENCH', 'ARC SHELL'],
- flash: 'rgba(230,250,255,0.78)',
- emoji: ['\u{1F300}','\u26A1','\u{1F4A0}','\u2728','\u{1F52C}']
- },
- retro: {
- banner: 'TAPE EAT',
- ticker: ['TRACKING LOSS', 'CHROMA SPLIT', 'MAGNETIC SNOW', 'CLICK-EJECT'],
- scraps: ['NO SIGNAL', 'HEAD DRAG', 'ROLL HOLD', 'SNOW PACK', 'EJECT CYCLE'],
- flash: 'rgba(255,226,178,0.6)',
- emoji: ['\u{1F4FC}','\u{1F4FA}','\u{1F3AE}','\u{1F579}\uFE0F','\u{1F4FB}']
- },
- retrofuture: {
- banner: 'ATOMIC TWILIGHT',
- ticker: ['GEIGER STATIC', 'FALLOUT DUST', 'RADIATION BURN', 'IRRADIATED SEPIA'],
- scraps: ['FALLOUT', 'BETA LEAK', 'ASH DRIFT', 'HALF-LIFE', 'GLOW CLOUD'],
- flash: 'rgba(255,240,180,0.62)',
- emoji: ['\u2622\uFE0F','\u{1F4A3}','\u{1F3ED}','\u2623\uFE0F','\u{1F9EA}']
- },
- spaceage: {
- banner: 'RE-ENTRY BURN',
- ticker: ['HEAT SHIELD LOSS', 'PLASMA BLACKOUT', 'COMMS STATIC', 'G-FORCE COMPRESSION'],
- scraps: ['BLACKOUT', 'SPARK SHOWER', 'PLASMA SHEATH', 'HULL GLOW', 'COMMS LOST'],
- flash: 'rgba(255,220,190,0.68)',
- emoji: ['\u{1F680}','\u{1F6F8}','\u{1FA90}','\u{1F30D}','\u2B50']
- },
- synthwave: {
- banner: 'GRID COLLAPSE',
- ticker: ['VOID PERSPECTIVE', 'MOLTEN SUN', 'CHROMA TEAR', 'OUT OF MEMORY'],
- scraps: ['VOID GRID', 'SUN DRIP', 'NEON PANIC', 'FRAME DROP', 'MEMORY STARVE'],
- flash: 'rgba(255,210,255,0.68)',
- emoji: ['\u{1F305}','\u{1F3B6}','\u{1F3B9}','\u{1F338}','\u{1F52E}']
- },
- terminal: {
- banner: 'FORK BOMB',
- ticker: ['PROCESS STORM', 'STACK TRACE WATERFALL', 'MEMORY GARBAGE', 'BSOD CREEP'],
- scraps: ['PID 65535', 'STACK OVERFLOW', 'OOM KILL', 'PANIC', '(:'],
- flash: 'rgba(180,255,180,0.7)',
- emoji: ['\u{1F4BB}','\u{1F41B}','\u2620\uFE0F','\u{1F5A5}\uFE0F','\u26A1']
- },
- tropicale: {
- banner: 'CATEGORY 5',
- ticker: ['HORIZONTAL RAIN', 'STORM SURGE', 'DEBRIS FIELD', 'WIND SHEAR'],
- scraps: ['PALM SNAP', 'SURGE LINE', 'SPRAY WALL', 'FLYING ROOF', 'LANDFALL'],
- flash: 'rgba(240,255,255,0.74)',
- emoji: ['\u{1F334}','\u{1F3D6}\uFE0F','\u{1F940}','\u{1F965}','\u{1F30A}']
- },
- noir: {
- banner: 'BLACKOUT DISTRICT',
- ticker: ['BLINDS SLAMMED SHUT', 'SIREN SWEEP', 'PROJECTOR BURN', 'MIDNIGHT DOWNPOUR'],
- scraps: ['NO WITNESSES', 'WET ASPHALT', 'RED CHANNEL', 'BLUE CHANNEL', 'SMOKE CURTAIN'],
- flash: 'rgba(255,245,225,0.66)',
- emoji: ['\u{1F576}\uFE0F','\u{1F52B}','\u{1F3A9}','\u{1F6AC}','\u{1F5DD}\uFE0F']
- },
- cathedral: {
- banner: 'LAST JUDGMENT',
- ticker: ['BELL SHOCKWAVE', 'INCENSE FIRESTORM', 'ROSE WINDOW FRACTURE', 'APSE IN FLAME'],
- scraps: ['REQUIEM', 'SHARD RAIN', 'VESPER BURN', 'GLORIA STATIC', 'NAVE COLLAPSE'],
- flash: 'rgba(255,239,202,0.72)',
- emoji: ['\u{1F54E}','\u{1F56F}\uFE0F','\u271D\uFE0F','\u{1F54A}\uFE0F','\u{1F3F0}']
- },
- surveillance: {
- banner: 'TOTAL COMPROMISE',
- ticker: ['CAMERA MESH BREACH', 'TRACKING LOSS', 'MULTIPLEX PANIC', 'ALERT CASCADE'],
- scraps: ['FLAGGED', 'OVERRIDDEN', 'TRACE LOOP', 'BOX LOST', 'ALERT 99'],
- flash: 'rgba(210,255,225,0.72)',
- emoji: ['\u{1F4F9}','\u{1F441}\uFE0F','\u{1F6A8}','\u{1F50D}','\u{1F4E1}']
- },
- biomech: {
- banner: 'CONTAINMENT RUPTURE',
- ticker: ['SYNAPSE STORM', 'TISSUE ARC', 'MEMBRANE TEAR', 'HYBRID OVERDRIVE'],
- scraps: ['VENTRICLE', 'MYCELIUM', 'RUPTURE', 'BIOFILM', 'NERVE GRID'],
- flash: 'rgba(255,205,220,0.7)',
- emoji: ['\u{1F9EC}','\u{1F9E0}','\u{1F9A0}','\u{1F52C}','\u{1FAC0}']
- },
- paper: {
- banner: 'PRESS JAM',
- ticker: ['TONER BLIZZARD', 'INK BLEED', 'COPY LAMP WHITEOUT', 'PAGE STORM'],
- scraps: ['MISPRINT', 'SKEWED FEED', 'RAG EDGE', 'CARBON DUST', 'REDACTION'],
- flash: 'rgba(255,250,236,0.82)',
- emoji: ['\u{1F4C4}','\u270F\uFE0F','\u{1F4CE}','\u2702\uFE0F','\u{1F5DE}\uFE0F']
- },
- volcano: {
- banner: 'PYROCLASTIC SURGE',
- ticker: ['ASH CASCADE', 'LAVA BOMB IMPACT', 'EARTHQUAKE SHAKE', 'SULFUR CLOUD'],
- scraps: ['ASHFALL', 'VENT BLAST', 'PYROCLAST', 'SEISMIC HIT', 'MAGMA SPRAY'],
- flash: 'rgba(255,220,150,0.72)',
- emoji: ['\u{1F30B}','\u{1F525}','\u{1F4A5}','\u2668\uFE0F','\u{1FAA8}']
- }
- };
- function snonuxDetectThemeName() {
- var splash = document.getElementById('splash-overlay');
- if (splash) {
- for (var i = 0; i < splash.classList.length; i++) {
- var cls = splash.classList[i];
- if (cls.indexOf('splash-') === 0 && cls !== 'splash-overlay' && cls.indexOf('splash--') !== 0) {
- return cls.slice(7);
- }
- }
- }
- return 'neon';
- }
- function snonuxEnsureWildRoot() {
- var root = document.getElementById('sno-wild-root');
- if (root) return root;
- root = document.createElement('div');
- root.id = 'sno-wild-root';
- root.setAttribute('aria-hidden', 'true');
- root.innerHTML =
- '<div id="sno-wild-colorwash" class="sno-wild-layer"></div>' +
- '<div id="sno-wild-rain" class="sno-wild-layer"></div>' +
- '<div id="sno-wild-wave" class="sno-wild-layer"></div>' +
- '<div id="sno-wild-beacon" class="sno-wild-layer"></div>' +
- '<div id="sno-wild-noise" class="sno-wild-layer"></div>' +
- '<div id="sno-wild-banner"></div>' +
- '<div id="sno-wild-ticker"><span></span></div>' +
- '<div id="sno-wild-scraps"></div>';
- document.body.appendChild(root);
- return root;
- }
- function snonuxApplyWildPreset(theme) {
- var preset = SNONUX_WILD_PRESETS[theme] || SNONUX_WILD_PRESETS.neon;
- var html = document.documentElement;
- var body = document.body;
- html.setAttribute('data-sno-theme', theme);
- body.setAttribute('data-sno-theme', theme);
- var root = snonuxEnsureWildRoot();
- var banner = root.querySelector('#sno-wild-banner');
- var ticker = root.querySelector('#sno-wild-ticker span');
- var scraps = root.querySelector('#sno-wild-scraps');
- banner.textContent = preset.banner;
- ticker.textContent = ' ' + preset.ticker.join(' // ') + ' // ' + preset.ticker.join(' // ') + ' ';
- scraps.innerHTML = '';
- var phrases = preset.scraps || [];
- var count = theme === 'dos' || theme === 'terminal' || theme === 'matrix' ? 22 : 16;
- for (var i = 0; i < count; i++) {
- var span = document.createElement('span');
- span.textContent = phrases[i % phrases.length];
- span.style.setProperty('--x', (8 + Math.random() * 84).toFixed(2) + '%');
- span.style.setProperty('--y', (12 + Math.random() * 72).toFixed(2) + '%');
- span.style.setProperty('--rot', ((Math.random() * 36) - 18).toFixed(1) + 'deg');
- span.style.setProperty('--dx', ((Math.random() * 120) - 60).toFixed(1) + 'px');
- span.style.setProperty('--dy', ((Math.random() * 100) - 50).toFixed(1) + 'px');
- span.style.setProperty('--dur', (4.5 + Math.random() * 5.5).toFixed(2) + 's');
- span.style.setProperty('--delay', (-Math.random() * 6).toFixed(2) + 's');
- scraps.appendChild(span);
- }
- window._snonuxWildTheme = theme;
- window._snonuxWildFlashColor = preset.flash || 'rgba(255,255,255,0.7)';
- }
- function snonuxPulseFlash(color, duration) {
- var ov = document.createElement('div');
- ov.style.cssText = 'position:fixed;inset:0;z-index:9998;pointer-events:none;opacity:0;';
- document.body.appendChild(ov);
- var tone = color || window._snonuxWildFlashColor || 'rgba(255,255,255,0.7)';
- [0, 70, 150, 250].forEach(function(d, i) {
- setTimeout(function() {
- ov.style.transition = 'opacity 0.08s linear';
- ov.style.background = tone;
- ov.style.opacity = (i % 2 === 0) ? '0.82' : '0';
- }, d);
- });
- setTimeout(function() {
- ov.style.transition = 'opacity 0.25s linear';
- ov.style.opacity = '0';
- }, Math.max(180, duration || 320));
- setTimeout(function() { ov.remove(); }, Math.max(520, duration || 320) + 260);
- }
- function snonuxScheduleWildBursts() {
- clearTimeout(window._snonuxWildBurstTimer);
- if (!window._snoWildActive) return;
- var delay = 1400 + Math.random() * 3600;
- window._snonuxWildBurstTimer = setTimeout(function() {
- if (!window._snoWildActive) return;
- snonuxPulseFlash(window._snonuxWildFlashColor, 260);
- snonuxScheduleWildBursts();
- }, delay);
- }
- function snonuxSetWildState(on) {
- var body = document.body;
- var badge = document.getElementById('sno-wild-badge');
- body.classList.toggle('sno-wild-active', !!on);
- if (badge) badge.classList.toggle('sno-wild-on', !!on);
- if (on) {
- snonuxApplyWildPreset(window._snonuxWildTheme || snonuxDetectThemeName());
- snonuxScheduleWildBursts();
- body.classList.add('sno-wild-hue');
- snonuxStartFlyingEmoji();
- snonuxStartRandomFlips();
- } else {
- clearTimeout(window._snonuxWildBurstTimer);
- body.classList.remove('sno-wild-hue');
- snonuxStopFlyingEmoji();
- snonuxStopRandomFlips();
- }
- }
- // === WILD FLYING EMOJI ===
- function snonuxStartFlyingEmoji() {
- snonuxStopFlyingEmoji();
- var zone = document.getElementById('sno-flyzone');
- if (!zone) {
- zone = document.createElement('div');
- zone.id = 'sno-flyzone';
- zone.setAttribute('aria-hidden', 'true');
- document.body.appendChild(zone);
- }
- function spawn() {
- if (!window._snoWildActive) return;
- var preset = SNONUX_WILD_PRESETS[window._snonuxWildTheme] || SNONUX_WILD_PRESETS.neon;
- var emojis = preset.emoji || ['\u2B50'];
- var s = document.createElement('span');
- s.textContent = emojis[Math.floor(Math.random() * emojis.length)];
- var top = (5 + Math.random() * 80).toFixed(1);
- var dur = (3 + Math.random() * 4).toFixed(2);
- var dir = Math.random() > 0.5 ? 'sno-fly-lr' : 'sno-fly-rl';
- var wobble = ((Math.random() - 0.5) * 60).toFixed(0);
- var rot = (180 + Math.random() * 540).toFixed(0);
- s.style.setProperty('--ftop', top + '%');
- s.style.setProperty('--fy', wobble + 'px');
- s.style.setProperty('--frot', rot + 'deg');
- s.style.animationName = dir;
- s.style.animationDuration = dur + 's';
- zone.appendChild(s);
- setTimeout(function() { s.remove(); }, parseFloat(dur) * 1000 + 200);
- window._snoFlyTimer = setTimeout(spawn, 800 + Math.random() * 2200);
- }
- spawn();
- }
- function snonuxStopFlyingEmoji() {
- clearTimeout(window._snoFlyTimer);
- var zone = document.getElementById('sno-flyzone');
- if (zone) zone.innerHTML = '';
- }
- // === WILD RANDOM FLIPS ===
- function snonuxStartRandomFlips() {
- snonuxStopRandomFlips();
- function flip() {
- if (!window._snoWildActive) return;
- var allPosts = document.querySelectorAll('.post:not(.post-active)');
- if (allPosts.length > 0) {
- var p = allPosts[Math.floor(Math.random() * allPosts.length)];
- p.classList.add('sno-fx-flip');
- setTimeout(function() { p.classList.remove('sno-fx-flip'); }, 1500);
- }
- window._snoFlipTimer = setTimeout(flip, 2500 + Math.random() * 4000);
- }
- window._snoFlipTimer = setTimeout(flip, 1500 + Math.random() * 2000);
- }
- function snonuxStopRandomFlips() {
- clearTimeout(window._snoFlipTimer);
- var flipped = document.querySelectorAll('.sno-fx-flip');
- flipped.forEach(function(el) { el.classList.remove('sno-fx-flip'); });
- }
- (function snonuxWildSetup() {
- window._snoWildActive = !!window._snoWildActive;
- snonuxApplyWildPreset(snonuxDetectThemeName());
- snonuxSetWildState(window._snoWildActive);
- })();
- // Dramatic lightning flash on wild mode activation/deactivation
- function snonuxWildFlash(on) {
- var ov = document.createElement('div');
- ov.style.cssText = 'position:fixed;inset:0;z-index:9998;pointer-events:none;';
- document.body.appendChild(ov);
- if (on) {
- // Three rapid lightning flashes on activation
- [0, 80, 180, 300, 440].forEach(function(d, i) {
- setTimeout(function() {
- ov.style.background = (i % 2 === 0) ? 'rgba(255,255,200,0.72)' : 'transparent';
- }, d);
- });
- setTimeout(function() { ov.remove(); }, 550);
- // Persistent storm overlay with intermittent flicker
- var storm = document.createElement('div');
- storm.id = 'sno-wild-storm';
- storm.style.cssText = 'position:fixed;inset:0;z-index:4998;pointer-events:none;' +
- 'background:radial-gradient(ellipse at 50% 0%,rgba(255,255,200,0.88) 0%,transparent 55%);' +
- 'animation:sno-wild-flicker 3.7s ease-in-out infinite;';
- document.body.appendChild(storm);
- } else {
- // Brief dark veil on deactivation
- ov.style.background = 'rgba(0,0,0,0.45)';
- ov.style.transition = 'opacity 0.45s';
- setTimeout(function() { ov.style.opacity = '0'; }, 60);
- setTimeout(function() { ov.remove(); }, 550);
- var storm = document.getElementById('sno-wild-storm');
- if (storm) { storm.style.transition = 'opacity 0.5s'; storm.style.opacity = '0'; setTimeout(function() { storm.remove(); }, 600); }
- }
- }
- function snonuxWaveType(w) {
- if (w === 'square') return 'square';
- if (w === 'triangle') return 'triangle';
- return 'sine';
- }
- (function splashSetup() {
- var el = document.getElementById('splash-overlay');
- if (!el) return;
- var splashAudioCtx = null;
- var splashChimePlayed = false;
- function playSplashChime() {
- if (splashChimePlayed) return;
- try {
- if (!splashAudioCtx) {
- splashAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
- }
- var ctx = splashAudioCtx;
- function ring() {
- splashChimePlayed = true;
- // Theme override: if defined, plays its own sound and returns true to skip the default chime.
- if (window.snonuxSplashSound && window.snonuxSplashSound(ctx)) return;
- var now = ctx.currentTime;
- var sp = SNONUX_SOUNDS.splash;
- var freqs = sp.freqs;
- var spacing = sp.spacing != null ? sp.spacing : 0.075;
- var gainAm = sp.gain != null ? sp.gain : 0.1;
- var wave = snonuxWaveType(sp.wave);
- var i, osc, g, t0;
- for (i = 0; i < freqs.length; i++) {
- osc = ctx.createOscillator();
- g = ctx.createGain();
- osc.connect(g);
- g.connect(ctx.destination);
- osc.type = wave;
- osc.frequency.value = freqs[i];
- t0 = now + i * spacing;
- g.gain.setValueAtTime(0, t0);
- g.gain.linearRampToValueAtTime(gainAm, t0 + 0.028);
- g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.52);
- osc.start(t0);
- osc.stop(t0 + 0.55);
- }
- }
- ctx.resume().then(ring).catch(function() {});
- } 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');
- el.focus({ preventScroll: true });
- }
- 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();
- }
- var triggers = document.querySelectorAll('.logo-mark, .logo-title h1, #sn-logo');
- triggers.forEach(function(trigger) {
- trigger.addEventListener('click', openSplashFromHeader);
- });
- window._snonuxDismissSplash = dismiss;
- window._snonuxShowSplash = show;
- if (document.documentElement.classList.contains('sno-splash-skip')) {
- dismiss();
- return;
- }
- playSplashChime();
- el.addEventListener('pointerdown', function() { playSplashChime(); }, { passive: true });
- el.addEventListener('click', function(e) { e.preventDefault(); dismiss(); });
- el.focus({ preventScroll: true });
- })();
-
- // === KEYBOARD NAVIGATION ===
- // j / ArrowDown → next post k / ArrowUp → previous post
- // h / ArrowLeft → previous page l / ArrowRight → next page
- // PageUp/PageDown → scroll the post list; re-highlight post at top of visible area
- // Enter / click post → expand modal Esc → close modal
- const posts = document.querySelectorAll('.post');
- let currentIndex = posts.length > 0 ? 0 : -1;
- const prevPageURL = {{.PrevPageJSON}};
- const nextPageURL = {{.NextPageJSON}};
-
- if (currentIndex >= 0) selectPost(0);
-
- function setActiveHighlight(index, playSound, scrollIntoView) {
- if (posts.length === 0) return;
- var prevIdx = currentIndex;
- if (currentIndex >= 0) posts[currentIndex].classList.remove('post-active');
- currentIndex = Math.max(0, Math.min(index, posts.length - 1));
- posts[currentIndex].classList.add('post-active');
- if (prevIdx >= 0 && prevIdx !== currentIndex && posts[prevIdx]) {
- var ghost = posts[prevIdx].querySelector('.sno-afterimage');
- if (!ghost) {
- ghost = document.createElement('div');
- ghost.className = 'sno-afterimage';
- posts[prevIdx].appendChild(ghost);
- }
- ghost.classList.remove('sno-afterimage-active');
- void ghost.offsetWidth;
- ghost.classList.add('sno-afterimage-active');
- }
- if (scrollIntoView) {
- posts[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
- if (playSound) playNavSound();
- }
-
- function selectPost(index) {
- setActiveHighlight(index, true, true);
- if (window.snonuxNavEffect) window.snonuxNavEffect();
- }
-
- /** Pick the post that should be active for the current viewport (anchor near top of visible area). */
- function activeIndexForVisibleRegion(sc) {
- if (posts.length === 0) return -1;
- var scrTop, scrBot, anchorY;
- if (sc) {
- var scr = sc.getBoundingClientRect();
- scrTop = scr.top;
- scrBot = scr.bottom;
- anchorY = scr.top + Math.min(scr.height * 0.18, 100);
- } else {
- scrTop = 0;
- scrBot = window.innerHeight;
- anchorY = window.innerHeight * 0.15;
- }
- var i, pr;
- for (i = 0; i < posts.length; i++) {
- pr = posts[i].getBoundingClientRect();
- if (pr.top <= anchorY && anchorY < pr.bottom) return i;
- }
- for (i = 0; i < posts.length; i++) {
- pr = posts[i].getBoundingClientRect();
- if (pr.bottom > scrTop && pr.top < scrBot) return i;
- }
- return posts.length - 1;
- }
-
- function playNavSound() {
- try {
- var n = SNONUX_SOUNDS.nav;
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
- osc.connect(gain); gain.connect(ctx.destination);
- osc.frequency.value = n.freq;
- osc.type = snonuxWaveType(n.wave);
- var dur = n.dur != null ? n.dur : 0.08;
- var g = n.gain != null ? n.gain : 0.12;
- gain.gain.setValueAtTime(g, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.02);
- } catch (_) {}
- }
-
- function playOpenSound() {
- try {
- var o = SNONUX_SOUNDS.open;
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
- osc.connect(gain); gain.connect(ctx.destination);
- osc.type = snonuxWaveType(o.wave);
- var dur = o.dur != null ? o.dur : 0.14;
- var g = o.gain != null ? o.gain : 0.1;
- osc.frequency.setValueAtTime(o.start, ctx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(o.end, ctx.currentTime + dur);
- gain.gain.setValueAtTime(g, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.06);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.07);
- } catch (_) {}
- }
-
- function playCloseSound() {
- try {
- var c = SNONUX_SOUNDS.close;
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
- osc.connect(gain); gain.connect(ctx.destination);
- osc.type = snonuxWaveType(c.wave);
- var dur = c.dur != null ? c.dur : 0.15;
- var g = c.gain != null ? c.gain : 0.1;
- osc.frequency.setValueAtTime(c.start, ctx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(c.end, ctx.currentTime + dur);
- gain.gain.setValueAtTime(g, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.05);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.06);
- } catch (_) {}
- }
-
- function playBounceSound() {
- try {
- var b = SNONUX_SOUNDS.bounce;
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
- osc.connect(gain); gain.connect(ctx.destination);
- osc.type = snonuxWaveType(b.wave);
- var dur = b.dur != null ? b.dur : 0.12;
- var g = b.gain != null ? b.gain : 0.1;
- osc.frequency.setValueAtTime(b.start, ctx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(b.end, ctx.currentTime + dur);
- gain.gain.setValueAtTime(g, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.05);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.06);
- } catch (_) {}
- }
-
- var _snoBounceCls = ['sno-fx-bounce-left','sno-fx-bounce-right','sno-fx-bounce-left-wild','sno-fx-bounce-right-wild',
- 'sno-fx-bounce-up','sno-fx-bounce-down','sno-fx-bounce-up-wild','sno-fx-bounce-down-wild'];
-
- function bounceEffect(dir) {
- var ov = document.querySelector('.overlay');
- if (!ov) return;
- var wild = !!window._snoWildActive;
- var map = { left: wild ? 'sno-fx-bounce-left-wild' : 'sno-fx-bounce-left',
- right: wild ? 'sno-fx-bounce-right-wild' : 'sno-fx-bounce-right',
- up: wild ? 'sno-fx-bounce-up-wild' : 'sno-fx-bounce-up',
- down: wild ? 'sno-fx-bounce-down-wild' : 'sno-fx-bounce-down' };
- var cls = map[dir] || map.down;
- _snoBounceCls.forEach(function(c) { ov.classList.remove(c); });
- void ov.offsetWidth;
- ov.classList.add(cls);
- var dur = wild ? 540 : 380;
- setTimeout(function() { ov.classList.remove(cls); }, dur);
- playBounceSound();
- if (wild) snonuxPulseFlash(window._snonuxWildFlashColor, 200);
- }
-
- // === 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 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;
- 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() {
- vx *= FRICTION;
- vy *= FRICTION;
- x += vx;
- y += vy;
- clampAndBounce();
- 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 {
- raf = null;
- }
- }
-
- function ensureLoop() {
- if (!raf) raf = requestAnimationFrame(tick);
- }
-
- return {
- keyPush: function(e) {
- var dx = 0, dy = 0;
- switch (e.key) {
- case 'h': case 'ArrowLeft': dx = -PUSH; break;
- case 'l': case 'ArrowRight': dx = PUSH; break;
- case 'k': case 'ArrowUp': dy = -PUSH; break;
- case 'j': case 'ArrowDown': dy = PUSH; break;
- default: return false;
- }
- e.preventDefault();
- vx += dx;
- vy += dy;
- 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 el = getEl();
- if (el) el.style.transform = '';
- if (raf) { cancelAnimationFrame(raf); raf = null; }
- },
- stop: function() {
- if (raf) { cancelAnimationFrame(raf); raf = null; }
- 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) {
- if (posts.length === 0) return;
- setActiveHighlight(index, false, !!scrollIntoView);
- var post = posts[currentIndex];
- var postText = post ? post.querySelector('.post-text') : null;
- if (!postText) return;
- var modal = document.getElementById('post-modal');
- var modalInner = modal ? modal.querySelector('.modal-inner') : null;
- document.getElementById('modal-content').innerHTML = postText.innerHTML;
- modal.classList.add('active');
- modalDrift.reset();
- if (window.snonuxOpenEffect) window.snonuxOpenEffect(post);
- modal.scrollTop = 0;
- if (modalInner) {
- modalInner.scrollTop = 0;
- requestAnimationFrame(function() {
- modalInner.scrollIntoView({ block: 'center', inline: 'nearest' });
- });
- }
- playOpenSound();
- }
-
- function closeModal() {
- modalDrift.stop();
- document.getElementById('post-modal').classList.remove('active');
- playCloseSound();
- if (window.snonuxCloseEffect) window.snonuxCloseEffect();
- }
-
- (function postClickOpen() {
- posts.forEach(function(post, idx) {
- post.addEventListener('click', function(e) {
- if (e.target.closest('a, button, audio, video, input, textarea, select, label')) return;
- openPostAt(idx, true);
- });
- });
- })();
-
- (function deepLinkFromHash() {
- var h = location.hash;
- if (!h || h.indexOf('#post-') !== 0) return;
- var id = decodeURIComponent(h.slice(6));
- var el = document.getElementById('post-' + id);
- if (!el) return;
- var idx = parseInt(el.getAttribute('data-index'), 10);
- if (isNaN(idx)) return;
- openPostAt(idx, true);
- })();
-
- document.addEventListener('keydown', function(e) {
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
- var splash = document.getElementById('splash-overlay');
- if (splash && !splash.classList.contains('splash--dismissed')) {
- 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 if (modalDrift.keyPush(e)) { playNavSound(); }
- return;
- }
- switch (e.key) {
- case 'PageUp':
- case 'PageDown': {
- var sc = document.getElementById('post-content');
- var step = (sc && sc.clientHeight) ? sc.clientHeight : window.innerHeight;
- var dy = (e.key === 'PageUp') ? -step : step;
- if (sc) {
- sc.scrollTop += dy;
- } else {
- window.scrollBy(0, dy);
- }
- var idx = activeIndexForVisibleRegion(sc);
- if (idx >= 0) setActiveHighlight(idx, true, false);
- if (window.snonuxScrollEffect) window.snonuxScrollEffect(e.key === 'PageUp' ? 'up' : 'down');
- e.preventDefault();
- break;
- }
- case 'j': case 'ArrowDown':
- if (currentIndex >= posts.length - 1) { bounceEffect('down'); }
- else { selectPost(currentIndex + 1); }
- e.preventDefault(); break;
- case 'k': case 'ArrowUp':
- if (currentIndex <= 0) { bounceEffect('up'); }
- else { selectPost(currentIndex - 1); }
- e.preventDefault(); break;
- case 'h': case 'ArrowLeft':
- if (prevPageURL) { playNavSound(); if (window.snonuxPageEffect) window.snonuxPageEffect(); window.location.href = prevPageURL; }
- else { bounceEffect('left'); }
- e.preventDefault(); break;
- case 'l': case 'ArrowRight':
- if (nextPageURL) { playNavSound(); if (window.snonuxPageEffect) window.snonuxPageEffect(); window.location.href = nextPageURL; }
- else { bounceEffect('right'); }
- e.preventDefault(); break;
- case 'Enter': openPostAt(currentIndex, true); e.preventDefault(); break;
- case 'w':
- window._snoWildActive = !window._snoWildActive;
- if (window.snonuxWildToggle) window.snonuxWildToggle();
- snonuxSetWildState(window._snoWildActive);
- snonuxWildFlash(window._snoWildActive);
- e.preventDefault(); break;
- }
- });
-
- // === MODAL SCROLL-END INDICATOR ===
- (function modalScrollEnd() {
- var mi = document.querySelector('#post-modal .modal-inner');
- if (!mi) return;
- mi.addEventListener('scroll', function() {
- var atEnd = mi.scrollHeight - mi.scrollTop - mi.clientHeight < 4;
- var el = document.getElementById('sno-scroll-end');
- if (!el) return;
- if (atEnd) {
- el.classList.remove('sno-scroll-end-active');
- void el.offsetWidth;
- el.classList.add('sno-scroll-end-active');
- }
- }, { passive: true });
- })();
-
- // === IDLE BREATHING ===
- (function idleBreathe() {
- var timer = null;
- var IDLE_DELAY = 10000;
- function startBreathe() {
- stopBreathe();
- timer = setTimeout(function() {
- if (currentIndex >= 0 && posts[currentIndex]) {
- posts[currentIndex].classList.add('sno-idle-breathe');
- }
- }, IDLE_DELAY);
- }
- function stopBreathe() {
- clearTimeout(timer);
- for (var i = 0; i < posts.length; i++) {
- posts[i].classList.remove('sno-idle-breathe');
- }
- }
- function resetIdle() { stopBreathe(); startBreathe(); }
- document.addEventListener('keydown', resetIdle);
- document.addEventListener('pointermove', resetIdle, { passive: true });
- document.addEventListener('pointerdown', resetIdle, { passive: true });
- startBreathe();
- })();
-
- // === FIRST-VISIT PARTICLE BURST ===
- (function firstVisitBurst() {
- var key = 'sno-visited';
- try { if (sessionStorage.getItem(key)) return; sessionStorage.setItem(key, '1'); } catch (_) { return; }
- if (document.documentElement.classList.contains('sno-splash-skip')) return;
- var origDismiss = window._snonuxDismissSplash;
- if (!origDismiss) return;
- window._snonuxDismissSplash = function() {
- origDismiss();
- var burst = document.createElement('div');
- burst.id = 'sno-burst';
- burst.setAttribute('aria-hidden', 'true');
- document.body.appendChild(burst);
- var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
- for (var i = 0; i < 36; i++) {
- var s = document.createElement('span');
- var angle = (i / 36) * Math.PI * 2 + (Math.random() - 0.5) * 0.4;
- var dist = 80 + Math.random() * 180;
- s.style.left = cx + 'px';
- s.style.top = cy + 'px';
- s.style.setProperty('--px', (Math.cos(angle) * dist).toFixed(1) + 'px');
- s.style.setProperty('--py', (Math.sin(angle) * dist).toFixed(1) + 'px');
- s.style.setProperty('--pdur', (0.4 + Math.random() * 0.5).toFixed(2) + 's');
- s.style.setProperty('--pdel', (Math.random() * 0.12).toFixed(2) + 's');
- s.style.width = (4 + Math.random() * 5) + 'px';
- s.style.height = s.style.width;
- burst.appendChild(s);
- }
- setTimeout(function() { burst.remove(); }, 1200);
- };
- })();
-
- // === CURSOR SPARKLE TRAIL ===
- (function cursorSparkle() {
- var throttle = 0;
- document.addEventListener('pointermove', function(e) {
- var now = Date.now();
- if (now - throttle < 60) return;
- throttle = now;
- var d = document.createElement('div');
- d.className = 'sno-sparkle';
- var size = 3 + Math.random() * 4;
- d.style.width = size + 'px';
- d.style.height = size + 'px';
- d.style.left = (e.clientX - size / 2 + (Math.random() - 0.5) * 10) + 'px';
- d.style.top = (e.clientY - size / 2 + (Math.random() - 0.5) * 10) + 'px';
- d.style.background = 'currentColor';
- d.style.opacity = '0.7';
- d.style.animation = 'sno-sparkle ' + (0.35 + Math.random() * 0.25).toFixed(2) + 's ease-out forwards';
- document.body.appendChild(d);
- setTimeout(function() { d.remove(); }, 650);
- }, { passive: true });
- })();
-</script>
-{{end}}
diff --git a/internal/generator/templates/shared/shared.css b/internal/generator/templates/shared/shared.css
new file mode 100644
index 0000000..a852156
--- /dev/null
+++ b/internal/generator/templates/shared/shared.css
@@ -0,0 +1,925 @@
+/* Non-active posts are translucent so the WebGL background shows through.
+ The active/highlighted post snaps to full opacity for clear reading.
+ Hover on a non-active post partially reveals it before selection. */
+.post { position:relative; }
+.post:not(.post-active) { opacity: 0.55; transition: opacity 0.25s ease; }
+.post:not(.post-active):hover { opacity: 0.85; }
+.post.post-active { opacity: 1 !important; transition: opacity 0.15s ease; }
+/* Thumbnail sizing in list view; modal overrides to full width. */
+.post-image { max-height:220px; max-width:100%; object-fit:cover; cursor:pointer; }
+#post-modal .post-image { max-height:none; width:100%; max-width:100%; object-fit:contain; cursor:default; }
+/* Semi-transparent modal backdrop so the WebGL scene stays visible behind
+ the expanded post. Theme-specific modal-inner keeps its own background. */
+.post-modal { background:rgba(0,0,0,0.55) !important; backdrop-filter:blur(6px) !important; }
+#post-modal.active { display:flex !important; align-items:center; justify-content:center; }
+#post-modal .modal-inner { width:fit-content; max-width:min(100%, 90vw); max-height:calc(100vh - 80px); overflow:auto; margin:0 auto !important; will-change:transform; }
+/* Content area max-width across all themes */
+.overlay { max-width:1200px; margin-left:auto; margin-right:auto; }
+/* Pagination: newer + older in a footer bar (below scrollable posts, like the header) */
+.page-nav-dual { display:flex; justify-content:center; align-items:center; flex-wrap:wrap;
+ gap:clamp(16px,4vw,48px); }
+/* Flex column layout: let #post-content shrink so overflow-y scrolls; footer stays visible */
+#post-content.content { min-height:0; }
+.page-nav-footer { flex-shrink:0; width:100%; box-sizing:border-box; }
+.page-nav-footer .page-nav { margin:0; }
+/* ~Half-height footer bar vs default .page-nav padding */
+.page-nav-footer .page-nav a { padding-top:4px; padding-bottom:4px; }
+/* Shared nav FX keyframes — themes apply these classes for brief effects */
+/* Modal open: post zooms/flies into the modal overlay */
+@keyframes sno-modal-zoom { 0%{transform:scale(0.82) translateY(18px);opacity:0} 60%{transform:scale(1.02) translateY(-3px);opacity:1} 100%{transform:scale(1) translateY(0);opacity:1} }
+@keyframes sno-modal-fly-up { 0%{transform:translateY(60px) scale(0.92);opacity:0} 70%{transform:translateY(-4px) scale(1.01);opacity:1} 100%{transform:translateY(0) scale(1);opacity:1} }
+@keyframes sno-modal-slide-in { 0%{transform:translateX(-40px) scale(0.96);opacity:0} 65%{transform:translateX(4px) scale(1.005);opacity:1} 100%{transform:translateX(0) scale(1);opacity:1} }
+@keyframes sno-modal-expand { 0%{transform:scale(0.05) rotate(-4deg);opacity:0} 55%{transform:scale(1.04) rotate(0.5deg);opacity:1} 100%{transform:scale(1) rotate(0);opacity:1} }
+.sno-modal-zoom .modal-inner { animation:sno-modal-zoom 0.36s cubic-bezier(0.22,1,0.36,1) both !important; }
+.sno-modal-fly .modal-inner { animation:sno-modal-fly-up 0.34s cubic-bezier(0.22,1,0.36,1) both !important; }
+.sno-modal-slide .modal-inner { animation:sno-modal-slide-in 0.32s cubic-bezier(0.22,1,0.36,1) both !important; }
+.sno-modal-expand .modal-inner { animation:sno-modal-expand 0.4s cubic-bezier(0.22,1,0.36,1) both !important; }
+@keyframes sno-shake { 0%,100%{transform:translate(0)} 14%{transform:translate(-7px,4px)} 28%{transform:translate(7px,-5px)} 42%{transform:translate(-5px,6px)} 56%{transform:translate(6px,-4px)} 70%{transform:translate(-4px,3px)} 86%{transform:translate(4px,-2px)} }
+@keyframes sno-zoom-fwd { 0%{transform:scale(1)} 40%{transform:scale(1.05)} 100%{transform:scale(1)} }
+@keyframes sno-glitch { 0%,100%{transform:translate(0) skewX(0)} 20%{transform:translate(-5px,0) skewX(-4deg)} 40%{transform:translate(5px,0) skewX(4deg)} 60%{transform:translate(-3px,0)} 80%{transform:translate(3px,0)} }
+@keyframes sno-bounce-left { 0%{transform:translateX(0)} 25%{transform:translateX(-18px)} 50%{transform:translateX(6px)} 75%{transform:translateX(-3px)} 100%{transform:translateX(0)} }
+@keyframes sno-bounce-right { 0%{transform:translateX(0)} 25%{transform:translateX(18px)} 50%{transform:translateX(-6px)} 75%{transform:translateX(3px)} 100%{transform:translateX(0)} }
+@keyframes sno-bounce-left-wild { 0%{transform:translateX(0) rotate(0)} 15%{transform:translateX(-48px) rotate(-3deg)} 35%{transform:translateX(22px) rotate(2deg)} 55%{transform:translateX(-12px) rotate(-1.2deg)} 75%{transform:translateX(6px) rotate(0.5deg)} 100%{transform:translateX(0) rotate(0)} }
+@keyframes sno-bounce-right-wild { 0%{transform:translateX(0) rotate(0)} 15%{transform:translateX(48px) rotate(3deg)} 35%{transform:translateX(-22px) rotate(-2deg)} 55%{transform:translateX(12px) rotate(1.2deg)} 75%{transform:translateX(-6px) rotate(-0.5deg)} 100%{transform:translateX(0) rotate(0)} }
+.sno-fx-bounce-left { animation:sno-bounce-left 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-right { animation:sno-bounce-right 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-left-wild { animation:sno-bounce-left-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-right-wild { animation:sno-bounce-right-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
+/* Vertical boundary bounce (top/bottom post list) */
+@keyframes sno-bounce-up { 0%{transform:translateY(0)} 25%{transform:translateY(-14px)} 50%{transform:translateY(5px)} 75%{transform:translateY(-2px)} 100%{transform:translateY(0)} }
+@keyframes sno-bounce-down { 0%{transform:translateY(0)} 25%{transform:translateY(14px)} 50%{transform:translateY(-5px)} 75%{transform:translateY(2px)} 100%{transform:translateY(0)} }
+@keyframes sno-bounce-up-wild { 0%{transform:translateY(0) rotate(0)} 15%{transform:translateY(-40px) rotate(-2.5deg)} 35%{transform:translateY(18px) rotate(1.5deg)} 55%{transform:translateY(-9px) rotate(-0.8deg)} 75%{transform:translateY(4px) rotate(0.3deg)} 100%{transform:translateY(0) rotate(0)} }
+@keyframes sno-bounce-down-wild { 0%{transform:translateY(0) rotate(0)} 15%{transform:translateY(40px) rotate(2.5deg)} 35%{transform:translateY(-18px) rotate(-1.5deg)} 55%{transform:translateY(9px) rotate(0.8deg)} 75%{transform:translateY(-4px) rotate(-0.3deg)} 100%{transform:translateY(0) rotate(0)} }
+.sno-fx-bounce-up { animation:sno-bounce-up 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-down { animation:sno-bounce-down 0.35s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-up-wild { animation:sno-bounce-up-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
+.sno-fx-bounce-down-wild { animation:sno-bounce-down-wild 0.5s cubic-bezier(.36,.07,.19,.97) both !important; }
+/* Post hover ripple — radial ring emanates from center on hover */
+@keyframes sno-hover-ripple { 0%{transform:translate(-50%,-50%) scale(0);opacity:0.5} 100%{transform:translate(-50%,-50%) scale(1);opacity:0} }
+.post:not(.post-active)::before { content:''; position:absolute; top:50%; left:50%; width:120%; height:120%;
+ border-radius:50%; border:2px solid currentColor; pointer-events:none;
+ transform:translate(-50%,-50%) scale(0); opacity:0; z-index:0; }
+.post:not(.post-active):hover::before { animation:sno-hover-ripple 0.6s ease-out forwards; }
+/* Modal scroll-end flash — brief glow at bottom of modal-inner when scrolled to end */
+@keyframes sno-scroll-end-flash { 0%{opacity:0.7} 100%{opacity:0} }
+.modal-inner .sno-scroll-end { position:sticky; bottom:0; left:0; right:0; height:3px; pointer-events:none;
+ background:linear-gradient(90deg, transparent, currentColor, transparent); opacity:0; }
+.modal-inner .sno-scroll-end.sno-scroll-end-active { animation:sno-scroll-end-flash 0.5s ease-out forwards; }
+/* Idle breathing — gentle glow pulse on active post after inactivity */
+@keyframes sno-idle-breathe { 0%,100%{box-shadow:inherit} 50%{box-shadow:0 0 18px 4px currentColor} }
+.post-active.sno-idle-breathe { animation:sno-idle-breathe 3s ease-in-out infinite; }
+/* First-visit particle burst */
+@keyframes sno-particle-fly { 0%{transform:translate(0,0) scale(1);opacity:1} 100%{transform:translate(var(--px),var(--py)) scale(0);opacity:0} }
+#sno-burst { position:fixed; inset:0; z-index:9999; pointer-events:none; }
+#sno-burst span { position:absolute; width:6px; height:6px; border-radius:50%; background:currentColor;
+ animation:sno-particle-fly var(--pdur) ease-out forwards; animation-delay:var(--pdel); opacity:0; }
+/* Cursor sparkle trail (normal mode) */
+@keyframes sno-sparkle { 0%{transform:scale(1);opacity:0.8} 100%{transform:scale(0);opacity:0} }
+.sno-sparkle { position:fixed; pointer-events:none; z-index:9990; border-radius:50%; }
+/* Post afterimage — ghost of previously selected post */
+@keyframes sno-afterimage { 0%{opacity:0.4;transform:scale(1)} 100%{opacity:0;transform:scale(0.97)} }
+.sno-afterimage { position:absolute; inset:0; pointer-events:none; z-index:0;
+ border:1px solid currentColor; border-radius:inherit; opacity:0; }
+.sno-afterimage-active { animation:sno-afterimage 0.5s ease-out forwards; }
+/* Wild flying emoji */
+@keyframes sno-fly-lr { 0%{transform:translateX(-60px) translateY(var(--fy,0)) rotate(0)} 100%{transform:translateX(calc(100vw + 60px)) translateY(var(--fy,0)) rotate(var(--frot,360deg))} }
+@keyframes sno-fly-rl { 0%{transform:translateX(calc(100vw + 60px)) translateY(var(--fy,0)) rotate(0)} 100%{transform:translateX(-60px) translateY(var(--fy,0)) rotate(var(--frot,-360deg))} }
+#sno-flyzone { position:fixed; inset:0; z-index:9985; pointer-events:none; overflow:hidden; }
+#sno-flyzone span { position:absolute; top:var(--ftop,50%); font-size:clamp(1.2rem,2.5vw,2rem);
+ animation-timing-function:linear; animation-fill-mode:forwards; }
+/* Wild random post flip */
+@keyframes sno-flip-post { 0%{transform:scaleY(1)} 25%{transform:scaleY(-1)} 75%{transform:scaleY(-1)} 100%{transform:scaleY(1)} }
+.sno-fx-flip { animation:sno-flip-post 1.4s ease-in-out both !important; transform-origin:center; }
+/* Wild hue drift on body */
+@keyframes sno-hue-drift { 0%{filter:hue-rotate(0)} 100%{filter:hue-rotate(360deg)} }
+body.sno-wild-hue { animation:sno-hue-drift 12s linear infinite; }
+@keyframes sno-wild-pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
+/* Storm overlay that flickers like distant lightning while wild mode is on */
+@keyframes sno-wild-flicker { 0%,84%,87%,91%,94%,100%{opacity:0} 85%,90%{opacity:0.75} 86%,92%{opacity:0.35} }
+.sno-fx-shake { animation:sno-shake 0.38s cubic-bezier(.36,.07,.19,.97) both !important; transform-origin:center; }
+.sno-fx-zoom { animation:sno-zoom-fwd 0.32s ease both !important; }
+.sno-fx-glitch { animation:sno-glitch 0.3s ease both !important; }
+/* Wild mode badge — only visible (opacity:1) when .sno-wild-on is present */
+#sno-wild-badge { position:fixed; top:10px; right:12px; z-index:5000; padding:3px 12px;
+ font-size:0.64rem; letter-spacing:0.2em; text-transform:uppercase; border-radius:2px;
+ pointer-events:none; opacity:0; transition:opacity 0.4s;
+ background:rgba(220,0,0,0.92); color:#fff; border:1px solid rgba(255,100,100,0.8);
+ font-family:monospace; }
+/* Animation only runs when wild is actually on, preventing invisible badge pulse */
+#sno-wild-badge.sno-wild-on { opacity:1; animation:sno-wild-pulse 0.9s ease-in-out infinite; }
+#sno-wild-root { position:fixed; inset:0; z-index:18; pointer-events:none; opacity:0;
+ transition:opacity 0.35s ease; overflow:hidden; isolation:isolate; }
+body.sno-wild-active #sno-wild-root { opacity:1; }
+#sno-wild-root .sno-wild-layer,
+#sno-wild-root #sno-wild-scraps,
+#sno-wild-root #sno-wild-banner,
+#sno-wild-root #sno-wild-ticker { position:absolute; inset:0; }
+#sno-wild-colorwash {
+ background-image:var(--sno-wild-colorwash, none);
+ background-size:var(--sno-wild-colorwash-size, cover);
+ background-position:center;
+ mix-blend-mode:screen;
+ opacity:var(--sno-wild-colorwash-opacity, 0.55);
+ animation:sno-wild-drift 9s ease-in-out infinite;
+}
+#sno-wild-rain {
+ background-image:var(--sno-wild-rain, none);
+ background-size:var(--sno-wild-rain-size, 220px 220px);
+ background-repeat:repeat;
+ background-position:0 0;
+ mix-blend-mode:screen;
+ opacity:var(--sno-wild-rain-opacity, 0.18);
+ animation:sno-wild-rainfall var(--sno-wild-rain-speed, 1.2s) linear infinite;
+}
+#sno-wild-wave {
+ inset:auto 0 -8vh 0;
+ height:70vh;
+ background:var(--sno-wild-wave, none);
+ opacity:var(--sno-wild-wave-opacity, 0.32);
+ transform-origin:50% 100%;
+ animation:sno-wild-surge 6.4s ease-in-out infinite;
+}
+#sno-wild-beacon {
+ background:var(--sno-wild-beacon, none);
+ opacity:var(--sno-wild-beacon-opacity, 0.38);
+ filter:blur(var(--sno-wild-beacon-blur, 0px));
+ mix-blend-mode:screen;
+ animation:sno-wild-lens 4.5s ease-in-out infinite;
+}
+#sno-wild-noise {
+ background-image:
+ linear-gradient(90deg, rgba(255,255,255,0.08) 0 1px, transparent 1px 100%),
+ linear-gradient(180deg, rgba(255,255,255,0.07) 0 1px, transparent 1px 100%),
+ repeating-linear-gradient(180deg,
+ rgba(255,255,255,0.08) 0 2px,
+ rgba(0,0,0,0.02) 2px 4px,
+ transparent 4px 8px);
+ background-size:3px 3px, 100% 3px, 100% 8px;
+ mix-blend-mode:overlay;
+ opacity:var(--sno-wild-noise-opacity, 0.12);
+ animation:sno-wild-static-shift 0.22s steps(2) infinite;
+}
+#sno-wild-banner {
+ inset:18px auto auto 50%;
+ transform:translateX(-50%);
+ width:max-content;
+ max-width:min(92vw, 920px);
+ height:auto;
+ padding:10px 18px;
+ border:1px solid var(--sno-wild-accent, rgba(255,255,255,0.8));
+ background:var(--sno-wild-banner-bg, rgba(0,0,0,0.82));
+ color:var(--sno-wild-banner-color, #fff);
+ font:700 clamp(0.75rem, 1vw + 0.55rem, 1.15rem)/1.1 monospace;
+ letter-spacing:0.38em;
+ text-transform:uppercase;
+ text-align:center;
+ box-shadow:0 0 28px var(--sno-wild-banner-glow, rgba(255,255,255,0.25));
+ text-shadow:0 0 18px var(--sno-wild-banner-glow, rgba(255,255,255,0.25));
+ white-space:nowrap;
+ animation:sno-wild-banner-flicker 1.2s steps(2) infinite;
+}
+#sno-wild-ticker {
+ inset:auto 0 12px 0;
+ height:30px;
+ overflow:hidden;
+ opacity:0.92;
+ color:var(--sno-wild-accent, #fff);
+ font:700 0.76rem/30px monospace;
+ letter-spacing:0.32em;
+ text-transform:uppercase;
+ text-shadow:0 0 14px var(--sno-wild-banner-glow, rgba(255,255,255,0.28));
+}
+#sno-wild-ticker span {
+ position:absolute;
+ left:0;
+ top:0;
+ display:inline-block;
+ white-space:nowrap;
+ padding-left:100%;
+ animation:sno-wild-marquee 13s linear infinite;
+}
+#sno-wild-scraps span {
+ position:absolute;
+ left:var(--x);
+ top:var(--y);
+ transform:rotate(var(--rot));
+ color:var(--sno-wild-scrap-color, rgba(255,255,255,0.82));
+ font:700 clamp(0.6rem, 0.45rem + 0.45vw, 0.94rem)/1 monospace;
+ letter-spacing:0.24em;
+ text-transform:uppercase;
+ white-space:nowrap;
+ opacity:0;
+ text-shadow:0 0 10px var(--sno-wild-banner-glow, rgba(255,255,255,0.18));
+ animation:sno-wild-scrap var(--dur, 6s) linear infinite;
+ animation-delay:var(--delay, 0s);
+}
+body.sno-wild-active .overlay,
+body.sno-wild-active header,
+body.sno-wild-active .post,
+body.sno-wild-active .page-nav-footer { will-change:transform, filter, opacity; }
+body.sno-wild-active .overlay { position:relative; z-index:24 !important; isolation:isolate; }
+body.sno-wild-active .post:not(.post-active) {
+ opacity:0.32 !important;
+ filter:saturate(0.78) brightness(0.82);
+}
+body.sno-wild-active .post.post-active {
+ z-index:2;
+ opacity:1 !important;
+ background:rgba(0,0,0,0.84) !important;
+ backdrop-filter:blur(14px) saturate(0.94);
+ -webkit-backdrop-filter:blur(14px) saturate(0.94);
+ box-shadow:
+ 0 0 0 2px var(--sno-wild-accent, rgba(255,255,255,0.82)),
+ 0 18px 44px rgba(0,0,0,0.55),
+ inset 0 0 0 1px rgba(255,255,255,0.06) !important;
+ text-shadow:none !important;
+ filter:none !important;
+ animation:none !important;
+ transform:none !important;
+}
+body.sno-wild-active .post.post-active::after {
+ content:"";
+ position:absolute;
+ inset:0;
+ border-radius:inherit;
+ pointer-events:none;
+ background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.01) 38%, rgba(0,0,0,0.08));
+}
+body.sno-wild-active .post.post-active > * { position:relative; z-index:1; }
+@keyframes sno-wild-drift {
+ 0%,100% { transform:translate3d(0,0,0) scale(1); }
+ 50% { transform:translate3d(-2%,1.5%,0) scale(1.08); }
+}
+@keyframes sno-wild-rainfall {
+ from { background-position:0 0, 0 0, 0 0; }
+ to { background-position:0 280px, 160px 420px, -140px 640px; }
+}
+@keyframes sno-wild-surge {
+ 0%,100% { transform:translateY(16%) scaleY(0.92) skewX(0deg); }
+ 50% { transform:translateY(-8%) scaleY(1.12) skewX(-4deg); }
+}
+@keyframes sno-wild-lens {
+ 0%,100% { transform:scale(0.9); opacity:0.18; }
+ 50% { transform:scale(1.24); opacity:0.52; }
+}
+@keyframes sno-wild-static-shift {
+ 0% { transform:translate3d(0,0,0); }
+ 25% { transform:translate3d(-1.2%,0.6%,0); }
+ 50% { transform:translate3d(1%, -0.6%,0); }
+ 75% { transform:translate3d(0.7%,1.1%,0); }
+ 100% { transform:translate3d(0,0,0); }
+}
+@keyframes sno-wild-banner-flicker {
+ 0%,100% { opacity:0.95; }
+ 12% { opacity:0.4; }
+ 13% { opacity:1; }
+ 32% { opacity:0.6; }
+ 33% { opacity:1; }
+ 61% { opacity:0.48; }
+ 62% { opacity:0.98; }
+}
+@keyframes sno-wild-marquee {
+ from { transform:translateX(0); }
+ to { transform:translateX(-46%); }
+}
+@keyframes sno-wild-scrap {
+ 0% { transform:translate3d(0,0,0) rotate(var(--rot)); opacity:0; }
+ 12% { opacity:0.78; }
+ 45% { transform:translate3d(var(--dx), var(--dy), 0) rotate(calc(var(--rot) + 10deg)); opacity:0.92; }
+ 100% { transform:translate3d(calc(var(--dx) * -0.75), calc(var(--dy) * -0.75), 0) rotate(calc(var(--rot) - 8deg)); opacity:0.18; }
+}
+@keyframes sno-wild-hard-blink {
+ 0%,44%,100% { filter:invert(0) grayscale(0) contrast(1.04); }
+ 45%,49% { filter:invert(1) grayscale(1) contrast(1.8); }
+ 50%,53% { filter:invert(0) grayscale(0) contrast(1.15); }
+ 54%,58% { filter:invert(1) grayscale(1) contrast(1.9); }
+}
+@keyframes sno-wild-jitter {
+ 0%,100% { transform:translate(0,0); }
+ 20% { transform:translate(-4px,2px); }
+ 40% { transform:translate(5px,-4px); }
+ 60% { transform:translate(-3px,4px); }
+ 80% { transform:translate(3px,-1px); }
+}
+@keyframes sno-wild-roll {
+ 0%,100% { transform:translateY(0); }
+ 28% { transform:translateY(-12px); }
+ 29% { transform:translateY(16px); }
+ 58% { transform:translateY(-8px); }
+ 59% { transform:translateY(6px); }
+}
+@keyframes sno-wild-collapse {
+ 0%,100% { transform:perspective(1200px) rotateX(0deg) scale(1); }
+ 50% { transform:perspective(1200px) rotateX(10deg) scale(1.04) translateY(1.2%); }
+}
+@keyframes sno-wild-compress {
+ 0%,100% { transform:scaleY(1); }
+ 40% { transform:scaleY(0.9); }
+ 70% { transform:scaleY(1.08); }
+}
+@keyframes sno-wild-text-burn {
+ 0%,100% { text-shadow:0 0 0 transparent; }
+ 33% { text-shadow:-2px 0 rgba(255,60,60,0.75), 2px 0 rgba(100,255,255,0.7); }
+ 66% { text-shadow:2px 0 rgba(255,240,80,0.75), -2px 0 rgba(90,90,255,0.7); }
+}
+body[data-sno-theme="aurora"] {
+ --sno-wild-colorwash:
+ linear-gradient(112deg, transparent 0 16%, rgba(0,255,179,0.2) 26%, transparent 38%, rgba(0,207,232,0.24) 48%, transparent 60%, rgba(192,132,252,0.24) 70%, transparent 84%),
+ radial-gradient(circle at 50% 28%, rgba(255,255,255,0.18) 0%, rgba(0,255,179,0.08) 20%, transparent 48%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 68%, rgba(236,255,255,0.9) 76% 80%, transparent 88%),
+ linear-gradient(180deg, transparent 0 72%, rgba(0,255,179,0.6) 80% 84%, transparent 92%),
+ linear-gradient(180deg, transparent 0 64%, rgba(192,132,252,0.55) 72% 78%, transparent 86%);
+ --sno-wild-rain-size:160px 240px, 220px 280px, 290px 330px;
+ --sno-wild-rain-opacity:0.3;
+ --sno-wild-rain-speed:1.05s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,255,179,0.08) 38%, rgba(192,132,252,0.22) 76%, rgba(3,8,17,0.88) 100%);
+ --sno-wild-wave-opacity:0.62;
+ --sno-wild-beacon:radial-gradient(circle at 50% 24%, rgba(255,255,255,0.3) 0%, rgba(0,255,179,0.14) 18%, transparent 54%);
+ --sno-wild-accent:#8fffe7;
+ --sno-wild-banner-bg:rgba(3,16,28,0.74);
+ --sno-wild-banner-color:#e9fff8;
+ --sno-wild-banner-glow:rgba(0,255,179,0.34);
+ --sno-wild-scrap-color:rgba(224,248,240,0.8);
+}
+body.sno-wild-active[data-sno-theme="aurora"] header,
+body.sno-wild-active[data-sno-theme="aurora"] .nav-hints,
+body.sno-wild-active[data-sno-theme="aurora"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="aurora"] .page-nav-footer { animation:sno-wild-text-burn 1.9s steps(1) infinite; }
+body[data-sno-theme="brutalist"] {
+ --sno-wild-colorwash:
+ linear-gradient(134deg, transparent 0 24%, rgba(255,255,255,0.08) 24.5% 25.2%, transparent 26% 42%, rgba(255,255,255,0.06) 42.5% 43.2%, transparent 44%),
+ linear-gradient(28deg, transparent 0 30%, rgba(195,32,32,0.22) 30.5% 31.5%, transparent 32% 65%, rgba(255,255,255,0.07) 65.4% 66.2%, transparent 67%),
+ radial-gradient(circle at 50% 10%, rgba(255,130,80,0.18) 0%, transparent 44%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 72%, rgba(133,65,22,0.9) 82% 84%, transparent 90%),
+ linear-gradient(180deg, transparent 0 60%, rgba(255,255,255,0.35) 70% 72%, transparent 80%);
+ --sno-wild-rain-size:240px 300px, 340px 360px;
+ --sno-wild-rain-opacity:0.22;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(24,24,24,0.22) 38%, rgba(92,16,16,0.4) 78%, rgba(0,0,0,0.88) 100%);
+ --sno-wild-accent:#ff5d52;
+ --sno-wild-banner-bg:rgba(12,12,12,0.84);
+ --sno-wild-banner-color:#fff3ef;
+ --sno-wild-banner-glow:rgba(255,93,82,0.28);
+ --sno-wild-scrap-color:rgba(255,214,204,0.82);
+}
+body.sno-wild-active[data-sno-theme="brutalist"] header,
+body.sno-wild-active[data-sno-theme="brutalist"] .nav-hints,
+body.sno-wild-active[data-sno-theme="brutalist"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="brutalist"] .page-nav-footer { animation:sno-wild-jitter 0.22s steps(2) infinite; }
+body[data-sno-theme="cosmos"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 48%, rgba(255,255,255,0.5) 0 2%, rgba(99,0,164,0.5) 6%, rgba(12,4,33,0.05) 16%, transparent 26%),
+ radial-gradient(circle at 50% 48%, transparent 0 22%, rgba(255,255,255,0.18) 26% 27%, transparent 31%),
+ conic-gradient(from 0deg at 50% 48%, rgba(255,255,255,0.28), transparent 18%, rgba(135,206,250,0.2) 32%, transparent 52%, rgba(255,240,150,0.24) 68%, transparent 84%, rgba(255,255,255,0.28));
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 70%, rgba(255,255,255,0.92) 80% 82%, transparent 90%),
+ linear-gradient(180deg, transparent 0 72%, rgba(180,128,255,0.7) 82% 84%, transparent 92%);
+ --sno-wild-rain-size:260px 280px, 320px 360px;
+ --sno-wild-rain-opacity:0.26;
+ --sno-wild-beacon:radial-gradient(circle at 50% 48%, rgba(255,255,255,0.75) 0%, rgba(155,115,255,0.24) 14%, transparent 36%);
+ --sno-wild-wave:radial-gradient(circle at 50% 110%, rgba(255,255,255,0.12) 0%, rgba(20,6,44,0.52) 45%, rgba(2,2,12,0.94) 100%);
+ --sno-wild-accent:#f8d9ff;
+ --sno-wild-banner-bg:rgba(8,6,24,0.82);
+ --sno-wild-banner-color:#ffffff;
+ --sno-wild-banner-glow:rgba(238,214,255,0.34);
+ --sno-wild-scrap-color:rgba(240,228,255,0.82);
+}
+body.sno-wild-active[data-sno-theme="cosmos"] header,
+body.sno-wild-active[data-sno-theme="cosmos"] .nav-hints,
+body.sno-wild-active[data-sno-theme="cosmos"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="cosmos"] .page-nav-footer { animation:sno-wild-collapse 4.2s ease-in-out infinite; }
+body[data-sno-theme="dos"] {
+ --sno-wild-colorwash:
+ linear-gradient(90deg, rgba(255,255,255,0.12) 0 50%, rgba(0,0,0,0.16) 50% 100%),
+ radial-gradient(circle at 50% 14%, rgba(85,255,255,0.18) 0%, transparent 38%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 62%, rgba(255,255,255,0.95) 68% 72%, transparent 78%),
+ linear-gradient(180deg, transparent 0 54%, rgba(85,255,255,0.85) 60% 64%, transparent 70%),
+ linear-gradient(180deg, transparent 0 70%, rgba(255,255,85,0.75) 76% 80%, transparent 86%);
+ --sno-wild-rain-size:120px 180px, 190px 220px, 280px 260px;
+ --sno-wild-rain-opacity:0.34;
+ --sno-wild-rain-speed:0.62s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,170,0.18) 42%, rgba(0,0,0,0.82) 100%);
+ --sno-wild-accent:#ffff55;
+ --sno-wild-banner-bg:#0000aa;
+ --sno-wild-banner-color:#ffffff;
+ --sno-wild-banner-glow:rgba(85,255,255,0.28);
+ --sno-wild-scrap-color:#ffff55;
+}
+body.sno-wild-active[data-sno-theme="dos"] header,
+body.sno-wild-active[data-sno-theme="dos"] .nav-hints,
+body.sno-wild-active[data-sno-theme="dos"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="dos"] .page-nav-footer {
+ animation:sno-wild-hard-blink 0.72s steps(1) infinite, sno-wild-jitter 0.14s steps(2) infinite;
+}
+body[data-sno-theme="matrix"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 0%, rgba(0,255,65,0.18) 0%, transparent 42%),
+ linear-gradient(180deg, rgba(0,40,0,0) 0%, rgba(0,255,65,0.09) 60%, rgba(0,0,0,0.62) 100%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 54%, rgba(187,255,210,1) 62% 68%, transparent 76%),
+ linear-gradient(180deg, transparent 0 44%, rgba(0,255,65,0.9) 52% 58%, transparent 66%),
+ linear-gradient(180deg, transparent 0 68%, rgba(140,255,180,0.72) 76% 82%, transparent 90%);
+ --sno-wild-rain-size:90px 180px, 130px 210px, 200px 280px;
+ --sno-wild-rain-opacity:0.5;
+ --sno-wild-rain-speed:0.38s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,255,65,0.04) 40%, rgba(0,0,0,0.92) 100%);
+ --sno-wild-accent:#98ffac;
+ --sno-wild-banner-bg:rgba(0,10,0,0.78);
+ --sno-wild-banner-color:#d8ffe2;
+ --sno-wild-banner-glow:rgba(0,255,65,0.32);
+ --sno-wild-scrap-color:rgba(152,255,172,0.84);
+}
+body.sno-wild-active[data-sno-theme="matrix"] header,
+body.sno-wild-active[data-sno-theme="matrix"] .nav-hints,
+body.sno-wild-active[data-sno-theme="matrix"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="matrix"] .page-nav-footer { animation:sno-wild-text-burn 1.1s steps(1) infinite; }
+body[data-sno-theme="neon"] {
+ --sno-wild-colorwash:
+ conic-gradient(from 90deg at 50% 20%, rgba(255,231,0,0.16), transparent 18%, rgba(255,0,204,0.22) 32%, transparent 50%, rgba(0,245,255,0.24) 66%, transparent 84%, rgba(255,231,0,0.16)),
+ radial-gradient(circle at 50% 15%, rgba(255,255,255,0.22) 0%, rgba(0,245,255,0.08) 26%, transparent 56%);
+ --sno-wild-rain:
+ linear-gradient(118deg, transparent 0 46%, rgba(255,255,255,0.95) 49% 50%, transparent 53%),
+ linear-gradient(62deg, transparent 0 45%, rgba(0,245,255,0.75) 48% 49%, transparent 52%),
+ linear-gradient(132deg, transparent 0 42%, rgba(255,0,204,0.82) 45% 46%, transparent 49%);
+ --sno-wild-rain-size:280px 280px, 360px 360px, 420px 420px;
+ --sno-wild-rain-opacity:0.34;
+ --sno-wild-rain-speed:0.72s;
+ --sno-wild-beacon:radial-gradient(circle at 50% 20%, rgba(255,255,255,0.44) 0%, rgba(255,0,204,0.18) 14%, transparent 42%);
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,0,204,0.08) 44%, rgba(11,0,26,0.88) 100%);
+ --sno-wild-accent:#ffe700;
+ --sno-wild-banner-bg:rgba(11,0,26,0.8);
+ --sno-wild-banner-color:#faff9c;
+ --sno-wild-banner-glow:rgba(255,231,0,0.42);
+ --sno-wild-scrap-color:rgba(255,245,174,0.88);
+}
+body.sno-wild-active[data-sno-theme="neon"] header,
+body.sno-wild-active[data-sno-theme="neon"] .nav-hints,
+body.sno-wild-active[data-sno-theme="neon"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="neon"] .page-nav-footer { animation:sno-wild-text-burn 1.3s steps(1) infinite; }
+body[data-sno-theme="ocean"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 120%, rgba(0,180,255,0.22) 0%, rgba(0,31,63,0.08) 34%, transparent 56%),
+ radial-gradient(circle at 20% 28%, rgba(147,255,227,0.16) 0%, transparent 32%),
+ radial-gradient(circle at 78% 22%, rgba(120,255,235,0.14) 0%, transparent 30%);
+ --sno-wild-rain:
+ linear-gradient(165deg, transparent 0 58%, rgba(220,255,255,0.95) 62% 66%, transparent 72%),
+ linear-gradient(175deg, transparent 0 48%, rgba(98,255,228,0.72) 52% 56%, transparent 62%),
+ linear-gradient(170deg, transparent 0 68%, rgba(170,255,240,0.78) 72% 76%, transparent 82%);
+ --sno-wild-rain-size:170px 260px, 240px 320px, 310px 360px;
+ --sno-wild-rain-opacity:0.26;
+ --sno-wild-rain-speed:0.82s;
+ --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(0,255,220,0.3) 0%, rgba(0,80,120,0.32) 28%, rgba(1,20,40,0.92) 74%);
+ --sno-wild-wave-opacity:0.86;
+ --sno-wild-beacon:radial-gradient(circle at 50% 88%, rgba(147,255,227,0.18) 0%, transparent 38%);
+ --sno-wild-accent:#bafcff;
+ --sno-wild-banner-bg:rgba(0,24,42,0.8);
+ --sno-wild-banner-color:#e1ffff;
+ --sno-wild-banner-glow:rgba(120,255,235,0.34);
+ --sno-wild-scrap-color:rgba(202,240,248,0.86);
+}
+body.sno-wild-active[data-sno-theme="ocean"] header,
+body.sno-wild-active[data-sno-theme="ocean"] .nav-hints,
+body.sno-wild-active[data-sno-theme="ocean"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="ocean"] .page-nav-footer { animation:sno-wild-collapse 5.2s ease-in-out infinite; }
+body[data-sno-theme="plasma"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 48%, rgba(255,255,255,0.66) 0%, rgba(165,230,255,0.18) 10%, transparent 24%),
+ conic-gradient(from 0deg at 50% 48%, rgba(255,255,255,0.28), rgba(157,255,255,0.16), rgba(251,146,255,0.28), rgba(255,255,255,0.28)),
+ radial-gradient(circle at 50% 110%, rgba(65,14,90,0.34) 0%, transparent 52%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 70%, rgba(255,255,255,0.92) 78% 82%, transparent 90%),
+ linear-gradient(180deg, transparent 0 60%, rgba(132,255,255,0.72) 68% 72%, transparent 80%);
+ --sno-wild-rain-size:220px 260px, 320px 360px;
+ --sno-wild-rain-opacity:0.28;
+ --sno-wild-beacon:radial-gradient(circle at 50% 48%, rgba(255,255,255,0.78) 0%, rgba(128,224,255,0.2) 12%, transparent 36%);
+ --sno-wild-wave:linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(144,220,255,0.12) 44%, rgba(8,2,20,0.9) 100%);
+ --sno-wild-accent:#fef3ff;
+ --sno-wild-banner-bg:rgba(18,6,36,0.8);
+ --sno-wild-banner-color:#ffffff;
+ --sno-wild-banner-glow:rgba(160,224,255,0.42);
+ --sno-wild-scrap-color:rgba(248,240,255,0.88);
+}
+body.sno-wild-active[data-sno-theme="plasma"] header,
+body.sno-wild-active[data-sno-theme="plasma"] .nav-hints,
+body.sno-wild-active[data-sno-theme="plasma"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="plasma"] .page-nav-footer { animation:sno-wild-text-burn 1.5s steps(1) infinite; }
+body[data-sno-theme="retro"] {
+ --sno-wild-colorwash:
+ linear-gradient(90deg, rgba(255,80,0,0.18) 0 18%, transparent 24% 76%, rgba(255,220,80,0.14) 82% 100%),
+ linear-gradient(180deg, rgba(255,255,255,0.08), transparent 16%, rgba(255,255,255,0.06) 34%, transparent 54%, rgba(255,255,255,0.05) 78%, transparent 100%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 46%, rgba(255,255,255,0.88) 52% 56%, transparent 62%),
+ linear-gradient(180deg, transparent 0 64%, rgba(255,190,50,0.82) 70% 74%, transparent 80%);
+ --sno-wild-rain-size:160px 220px, 260px 300px;
+ --sno-wild-rain-opacity:0.24;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,190,50,0.08) 42%, rgba(10,4,0,0.9) 100%);
+ --sno-wild-accent:#ffcf63;
+ --sno-wild-banner-bg:rgba(24,10,0,0.82);
+ --sno-wild-banner-color:#ffe7a5;
+ --sno-wild-banner-glow:rgba(255,190,50,0.34);
+ --sno-wild-scrap-color:rgba(255,215,120,0.86);
+}
+body.sno-wild-active[data-sno-theme="retro"] header,
+body.sno-wild-active[data-sno-theme="retro"] .nav-hints,
+body.sno-wild-active[data-sno-theme="retro"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="retro"] .page-nav-footer {
+ animation:sno-wild-roll 0.85s linear infinite, sno-wild-jitter 0.18s steps(2) infinite;
+}
+body[data-sno-theme="retrofuture"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 20%, rgba(255,220,140,0.24) 0%, transparent 38%),
+ linear-gradient(180deg, rgba(169,111,46,0.08) 0%, rgba(133,86,33,0.16) 60%, rgba(50,29,12,0.52) 100%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 66%, rgba(255,232,180,0.82) 74% 78%, transparent 86%),
+ linear-gradient(180deg, transparent 0 58%, rgba(170,120,60,0.7) 66% 70%, transparent 78%);
+ --sno-wild-rain-size:180px 240px, 260px 320px;
+ --sno-wild-rain-opacity:0.22;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,206,128,0.07) 42%, rgba(28,18,9,0.92) 100%);
+ --sno-wild-accent:#ffd793;
+ --sno-wild-banner-bg:rgba(26,16,8,0.82);
+ --sno-wild-banner-color:#fff4d6;
+ --sno-wild-banner-glow:rgba(255,215,147,0.3);
+ --sno-wild-scrap-color:rgba(255,233,190,0.86);
+}
+body.sno-wild-active[data-sno-theme="retrofuture"] header,
+body.sno-wild-active[data-sno-theme="retrofuture"] .nav-hints,
+body.sno-wild-active[data-sno-theme="retrofuture"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="retrofuture"] .page-nav-footer { animation:sno-wild-text-burn 1.6s steps(1) infinite; }
+body[data-sno-theme="spaceage"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(255,170,90,0.16) 0%, transparent 24%, rgba(80,220,255,0.12) 54%, transparent 100%),
+ radial-gradient(circle at 50% -10%, rgba(255,255,255,0.46) 0%, rgba(255,180,120,0.16) 18%, transparent 42%);
+ --sno-wild-rain:
+ linear-gradient(160deg, transparent 0 52%, rgba(255,220,190,0.95) 58% 62%, transparent 68%),
+ linear-gradient(170deg, transparent 0 42%, rgba(255,138,84,0.74) 48% 52%, transparent 58%),
+ linear-gradient(180deg, transparent 0 62%, rgba(120,220,255,0.68) 68% 72%, transparent 78%);
+ --sno-wild-rain-size:180px 260px, 260px 320px, 360px 420px;
+ --sno-wild-rain-opacity:0.28;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,149,90,0.08) 44%, rgba(0,10,18,0.9) 100%);
+ --sno-wild-accent:#ffd0a8;
+ --sno-wild-banner-bg:rgba(3,18,26,0.82);
+ --sno-wild-banner-color:#fff1e0;
+ --sno-wild-banner-glow:rgba(255,172,124,0.34);
+ --sno-wild-scrap-color:rgba(255,225,206,0.82);
+}
+body.sno-wild-active[data-sno-theme="spaceage"] header,
+body.sno-wild-active[data-sno-theme="spaceage"] .nav-hints,
+body.sno-wild-active[data-sno-theme="spaceage"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="spaceage"] .page-nav-footer {
+ animation:sno-wild-compress 1.8s ease-in-out infinite, sno-wild-jitter 0.42s steps(2) infinite;
+}
+body[data-sno-theme="synthwave"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(255,20,147,0.14) 0%, transparent 20%, rgba(0,255,255,0.12) 52%, transparent 74%),
+ radial-gradient(circle at 50% 18%, rgba(255,255,255,0.34) 0%, rgba(255,20,147,0.14) 14%, transparent 36%),
+ linear-gradient(90deg, rgba(255,20,147,0.14) 0 1px, transparent 1px 11%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 68%, rgba(255,255,255,0.95) 76% 80%, transparent 88%),
+ linear-gradient(180deg, transparent 0 60%, rgba(255,20,147,0.82) 68% 72%, transparent 80%),
+ linear-gradient(180deg, transparent 0 50%, rgba(0,255,255,0.74) 58% 62%, transparent 70%);
+ --sno-wild-rain-size:160px 220px, 260px 320px, 360px 400px;
+ --sno-wild-rain-opacity:0.32;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,20,147,0.08) 42%, rgba(5,1,22,0.92) 100%);
+ --sno-wild-accent:#ff7de9;
+ --sno-wild-banner-bg:rgba(18,0,34,0.84);
+ --sno-wild-banner-color:#ffe9ff;
+ --sno-wild-banner-glow:rgba(255,125,233,0.4);
+ --sno-wild-scrap-color:rgba(255,225,255,0.86);
+}
+body.sno-wild-active[data-sno-theme="synthwave"] header,
+body.sno-wild-active[data-sno-theme="synthwave"] .nav-hints,
+body.sno-wild-active[data-sno-theme="synthwave"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="synthwave"] .page-nav-footer { animation:sno-wild-collapse 3.8s ease-in-out infinite; }
+body[data-sno-theme="terminal"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(51,255,51,0.14) 0%, transparent 24%, rgba(0,0,0,0.14) 46%, transparent 68%, rgba(51,255,51,0.1) 100%),
+ radial-gradient(circle at 50% 10%, rgba(255,255,255,0.18) 0%, transparent 38%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 52%, rgba(255,255,255,0.94) 58% 62%, transparent 68%),
+ linear-gradient(180deg, transparent 0 44%, rgba(51,255,51,0.9) 50% 54%, transparent 60%),
+ linear-gradient(180deg, transparent 0 64%, rgba(140,255,140,0.7) 70% 74%, transparent 80%);
+ --sno-wild-rain-size:100px 180px, 140px 210px, 220px 260px;
+ --sno-wild-rain-opacity:0.4;
+ --sno-wild-rain-speed:0.42s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(51,255,51,0.06) 42%, rgba(0,0,0,0.95) 100%);
+ --sno-wild-accent:#8dff8d;
+ --sno-wild-banner-bg:rgba(0,10,0,0.8);
+ --sno-wild-banner-color:#d7ffd7;
+ --sno-wild-banner-glow:rgba(51,255,51,0.34);
+ --sno-wild-scrap-color:rgba(141,255,141,0.84);
+}
+body.sno-wild-active[data-sno-theme="terminal"] header,
+body.sno-wild-active[data-sno-theme="terminal"] .nav-hints,
+body.sno-wild-active[data-sno-theme="terminal"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="terminal"] .page-nav-footer {
+ animation:sno-wild-hard-blink 0.82s steps(1) infinite, sno-wild-jitter 0.16s steps(2) infinite;
+}
+body.sno-wild-active[data-sno-theme="terminal"] .post.post-active {
+ filter:invert(1) grayscale(1) contrast(1.28) !important;
+ background:#fff !important;
+ color:#000 !important;
+ text-shadow:none !important;
+}
+body.sno-wild-active[data-sno-theme="terminal"] .post.post-active a { color:#000 !important; }
+body.sno-wild-active[data-sno-theme="terminal"] .post.post-active .post-time,
+body.sno-wild-active[data-sno-theme="terminal"] .post.post-active .post-header strong { color:#000 !important; }
+body[data-sno-theme="tropicale"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(255,255,255,0.12) 0%, transparent 18%, rgba(0,110,160,0.1) 46%, rgba(0,40,70,0.38) 100%),
+ radial-gradient(circle at 50% 110%, rgba(0,210,255,0.24) 0%, transparent 44%);
+ --sno-wild-rain:
+ linear-gradient(168deg, transparent 0 54%, rgba(255,255,255,0.98) 60% 64%, transparent 70%),
+ linear-gradient(176deg, transparent 0 42%, rgba(200,255,255,0.72) 48% 52%, transparent 58%),
+ linear-gradient(172deg, transparent 0 64%, rgba(255,230,180,0.52) 70% 74%, transparent 80%);
+ --sno-wild-rain-size:150px 260px, 230px 320px, 320px 400px;
+ --sno-wild-rain-opacity:0.44;
+ --sno-wild-rain-speed:0.5s;
+ --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(175,255,255,0.28) 0%, rgba(0,135,190,0.32) 26%, rgba(0,24,44,0.92) 74%);
+ --sno-wild-wave-opacity:0.88;
+ --sno-wild-accent:#fff2bf;
+ --sno-wild-banner-bg:rgba(0,34,56,0.82);
+ --sno-wild-banner-color:#fff7db;
+ --sno-wild-banner-glow:rgba(255,242,191,0.34);
+ --sno-wild-scrap-color:rgba(255,244,213,0.84);
+}
+body.sno-wild-active[data-sno-theme="tropicale"] header,
+body.sno-wild-active[data-sno-theme="tropicale"] .nav-hints,
+body.sno-wild-active[data-sno-theme="tropicale"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="tropicale"] .page-nav-footer { animation:sno-wild-jitter 0.3s steps(2) infinite; }
+body[data-sno-theme="volcano"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(60,60,60,0.28) 0%, rgba(40,40,40,0.18) 26%, transparent 48%, rgba(255,140,0,0.18) 72%, rgba(255,214,102,0.14) 100%),
+ radial-gradient(circle at 50% 8%, rgba(255,255,255,0.24) 0%, rgba(255,150,60,0.1) 18%, transparent 42%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 42%, rgba(110,110,110,0.92) 48% 54%, transparent 60%),
+ linear-gradient(180deg, transparent 0 54%, rgba(255,170,80,0.82) 60% 66%, transparent 72%),
+ linear-gradient(180deg, transparent 0 64%, rgba(255,235,140,0.54) 70% 76%, transparent 82%);
+ --sno-wild-rain-size:150px 230px, 220px 300px, 320px 360px;
+ --sno-wild-rain-opacity:0.38;
+ --sno-wild-rain-speed:0.68s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,120,20,0.1) 42%, rgba(55,20,0,0.9) 100%);
+ --sno-wild-accent:#ffdf76;
+ --sno-wild-banner-bg:rgba(22,8,0,0.84);
+ --sno-wild-banner-color:#fff0c2;
+ --sno-wild-banner-glow:rgba(255,190,70,0.34);
+ --sno-wild-scrap-color:rgba(255,230,170,0.86);
+}
+body.sno-wild-active[data-sno-theme="volcano"] header,
+body.sno-wild-active[data-sno-theme="volcano"] .nav-hints,
+body.sno-wild-active[data-sno-theme="volcano"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="volcano"] .page-nav-footer { animation:sno-wild-jitter 0.24s steps(2) infinite; }
+body[data-sno-theme="noir"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 18% 18%, rgba(36,65,130,0.2) 0%, transparent 24%),
+ radial-gradient(circle at 82% 24%, rgba(169,55,43,0.24) 0%, transparent 24%),
+ linear-gradient(180deg, rgba(240,234,214,0.08) 0%, transparent 26%, rgba(0,0,0,0.14) 48%, transparent 72%, rgba(240,234,214,0.06) 100%);
+ --sno-wild-rain:
+ linear-gradient(168deg, transparent 0 56%, rgba(255,255,255,0.96) 62% 66%, transparent 72%),
+ linear-gradient(174deg, transparent 0 48%, rgba(210,220,255,0.72) 54% 58%, transparent 64%),
+ linear-gradient(170deg, transparent 0 64%, rgba(255,170,170,0.68) 70% 74%, transparent 80%);
+ --sno-wild-rain-size:170px 260px, 250px 340px, 340px 420px;
+ --sno-wild-rain-opacity:0.34;
+ --sno-wild-rain-speed:0.58s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(255,255,255,0.04) 44%, rgba(0,0,0,0.96) 100%);
+ --sno-wild-beacon:linear-gradient(90deg, rgba(36,65,130,0.12), transparent 28%, rgba(169,55,43,0.14) 72%, transparent);
+ --sno-wild-accent:#f0ead6;
+ --sno-wild-banner-bg:rgba(5,5,5,0.84);
+ --sno-wild-banner-color:#fff6e2;
+ --sno-wild-banner-glow:rgba(240,234,214,0.28);
+ --sno-wild-scrap-color:rgba(255,242,224,0.84);
+}
+body.sno-wild-active[data-sno-theme="noir"] header,
+body.sno-wild-active[data-sno-theme="noir"] .nav-hints,
+body.sno-wild-active[data-sno-theme="noir"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="noir"] .page-nav-footer { animation:sno-wild-roll 1.1s linear infinite, sno-wild-text-burn 1.8s steps(1) infinite; }
+body[data-sno-theme="cathedral"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 10%, rgba(217,191,120,0.22) 0%, transparent 26%),
+ conic-gradient(from 0deg at 50% 18%, rgba(79,127,209,0.18), rgba(113,35,61,0.22), rgba(217,191,120,0.18), rgba(79,127,209,0.18)),
+ linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 26%, rgba(0,0,0,0.18) 100%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 64%, rgba(255,220,140,0.92) 72% 76%, transparent 84%),
+ linear-gradient(180deg, transparent 0 58%, rgba(255,140,90,0.72) 66% 70%, transparent 78%),
+ linear-gradient(180deg, transparent 0 52%, rgba(120,160,255,0.66) 60% 64%, transparent 72%);
+ --sno-wild-rain-size:180px 220px, 260px 320px, 320px 420px;
+ --sno-wild-rain-opacity:0.26;
+ --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(217,191,120,0.18) 0%, rgba(113,35,61,0.24) 34%, rgba(8,6,10,0.94) 78%);
+ --sno-wild-beacon:radial-gradient(circle at 50% 18%, rgba(255,244,210,0.36) 0%, rgba(217,191,120,0.16) 18%, transparent 40%);
+ --sno-wild-accent:#f5d89a;
+ --sno-wild-banner-bg:rgba(18,11,20,0.84);
+ --sno-wild-banner-color:#fff4d8;
+ --sno-wild-banner-glow:rgba(217,191,120,0.34);
+ --sno-wild-scrap-color:rgba(255,239,206,0.86);
+}
+body.sno-wild-active[data-sno-theme="cathedral"] header,
+body.sno-wild-active[data-sno-theme="cathedral"] .nav-hints,
+body.sno-wild-active[data-sno-theme="cathedral"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="cathedral"] .page-nav-footer { animation:sno-wild-collapse 4.4s ease-in-out infinite, sno-wild-text-burn 1.7s steps(1) infinite; }
+body[data-sno-theme="surveillance"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(99,243,168,0.12) 0%, transparent 22%, rgba(255,77,92,0.08) 56%, transparent 100%),
+ linear-gradient(90deg, rgba(99,243,168,0.08) 0 1px, transparent 1px 8%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 52%, rgba(188,255,212,0.96) 58% 62%, transparent 68%),
+ linear-gradient(180deg, transparent 0 44%, rgba(99,243,168,0.82) 50% 54%, transparent 60%),
+ linear-gradient(180deg, transparent 0 62%, rgba(255,77,92,0.68) 68% 72%, transparent 78%);
+ --sno-wild-rain-size:100px 180px, 160px 220px, 260px 320px;
+ --sno-wild-rain-opacity:0.4;
+ --sno-wild-rain-speed:0.38s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(99,243,168,0.06) 42%, rgba(0,0,0,0.95) 100%);
+ --sno-wild-accent:#bcffd4;
+ --sno-wild-banner-bg:rgba(6,12,9,0.84);
+ --sno-wild-banner-color:#ecfff5;
+ --sno-wild-banner-glow:rgba(99,243,168,0.34);
+ --sno-wild-scrap-color:rgba(188,255,212,0.86);
+}
+body.sno-wild-active[data-sno-theme="surveillance"] header,
+body.sno-wild-active[data-sno-theme="surveillance"] .nav-hints,
+body.sno-wild-active[data-sno-theme="surveillance"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="surveillance"] .page-nav-footer { animation:sno-wild-hard-blink 0.94s steps(1) infinite, sno-wild-jitter 0.18s steps(2) infinite; }
+body[data-sno-theme="biomech"] {
+ --sno-wild-colorwash:
+ radial-gradient(circle at 50% 44%, rgba(245,91,125,0.22) 0%, transparent 24%),
+ conic-gradient(from 0deg at 50% 44%, rgba(147,255,216,0.2), rgba(128,63,93,0.28), rgba(208,199,187,0.14), rgba(147,255,216,0.2)),
+ linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(147,255,216,0.04) 40%, rgba(0,0,0,0.24) 100%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 64%, rgba(245,91,125,0.86) 72% 76%, transparent 84%),
+ linear-gradient(180deg, transparent 0 56%, rgba(147,255,216,0.84) 64% 68%, transparent 76%),
+ linear-gradient(180deg, transparent 0 48%, rgba(208,199,187,0.62) 56% 60%, transparent 68%);
+ --sno-wild-rain-size:150px 220px, 240px 340px, 320px 420px;
+ --sno-wild-rain-opacity:0.3;
+ --sno-wild-wave:radial-gradient(circle at 50% 100%, rgba(245,91,125,0.18) 0%, rgba(128,63,93,0.3) 32%, rgba(5,4,8,0.96) 78%);
+ --sno-wild-beacon:radial-gradient(circle at 50% 44%, rgba(255,210,220,0.28) 0%, rgba(147,255,216,0.12) 14%, transparent 36%);
+ --sno-wild-accent:#93ffd8;
+ --sno-wild-banner-bg:rgba(10,8,14,0.84);
+ --sno-wild-banner-color:#e7fff6;
+ --sno-wild-banner-glow:rgba(147,255,216,0.34);
+ --sno-wild-scrap-color:rgba(216,255,242,0.88);
+}
+body.sno-wild-active[data-sno-theme="biomech"] header,
+body.sno-wild-active[data-sno-theme="biomech"] .nav-hints,
+body.sno-wild-active[data-sno-theme="biomech"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="biomech"] .page-nav-footer { animation:sno-wild-collapse 3.6s ease-in-out infinite, sno-wild-jitter 0.22s steps(2) infinite; }
+body[data-sno-theme="paper"] {
+ --sno-wild-colorwash:
+ linear-gradient(180deg, rgba(184,58,46,0.1) 0%, transparent 18%, rgba(62,93,138,0.06) 52%, rgba(0,0,0,0.04) 100%),
+ radial-gradient(circle at 22% 24%, rgba(0,0,0,0.05) 0%, transparent 18%),
+ radial-gradient(circle at 78% 72%, rgba(0,0,0,0.05) 0%, transparent 18%);
+ --sno-wild-rain:
+ linear-gradient(180deg, transparent 0 54%, rgba(0,0,0,0.34) 60% 64%, transparent 70%),
+ linear-gradient(180deg, transparent 0 62%, rgba(184,58,46,0.46) 68% 72%, transparent 78%),
+ linear-gradient(180deg, transparent 0 48%, rgba(62,93,138,0.42) 54% 58%, transparent 64%);
+ --sno-wild-rain-size:160px 220px, 260px 320px, 360px 420px;
+ --sno-wild-rain-opacity:0.24;
+ --sno-wild-rain-speed:0.74s;
+ --sno-wild-wave:linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(0,0,0,0.03) 44%, rgba(215,199,167,0.2) 100%);
+ --sno-wild-accent:#b83a2e;
+ --sno-wild-banner-bg:rgba(250,244,231,0.88);
+ --sno-wild-banner-color:#16120f;
+ --sno-wild-banner-glow:rgba(184,58,46,0.18);
+ --sno-wild-scrap-color:rgba(22,18,15,0.76);
+}
+body.sno-wild-active[data-sno-theme="paper"] header,
+body.sno-wild-active[data-sno-theme="paper"] .nav-hints,
+body.sno-wild-active[data-sno-theme="paper"] .post:not(.post-active),
+body.sno-wild-active[data-sno-theme="paper"] .page-nav-footer { animation:sno-wild-roll 0.9s linear infinite, sno-wild-text-burn 2.2s steps(1) infinite; }
+/* Host note under the site subtitle (all themes) */
+.logo-host { font-size:0.65rem; opacity:0.55; margin-top:4px; letter-spacing:0.3px; line-height:1.3; }
+/* Atom feed link in header (paired with transmit in .nav) */
+.nav { display:flex; align-items:center; gap:clamp(10px,2.2vw,20px); flex-wrap:wrap; justify-content:flex-end; }
+a.header-feed-link { font-size:0.8rem; text-decoration:none; opacity:0.82; letter-spacing:0.04em; white-space:nowrap; }
+a.header-feed-link:hover { opacity:1; text-decoration:underline; }
+/* Header logo/title can reopen the splash overlay. */
+.logo-mark, .logo-title h1, #sn-logo { cursor:pointer; }
+/* Full-viewport splash (theme-specific colours/animation on each .splash-THEMENAME) */
+#splash-overlay { position:fixed; inset:0; z-index:2000; display:flex; flex-direction:column; align-items:center;
+ justify-content:center; text-align:center; padding:max(16px,4vw); box-sizing:border-box; cursor:pointer;
+ transition:opacity .55s ease, visibility .55s ease, transform .55s ease; }
+#splash-overlay.splash--dismissed { opacity:0 !important; visibility:hidden !important;
+ pointer-events:none !important; transform:scale(1.02); }
+#splash-overlay:focus { outline:2px solid rgba(255,255,255,0.35); outline-offset:4px; }
+/* Vignette over WebGL so 3D motion does not overpower the edges */
+#splash-overlay::before { content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
+ background: radial-gradient(ellipse 92% 82% at 50% 42%, rgba(0,0,0,0) 32%, rgba(0,0,0,0.26) 68%, rgba(0,0,0,0.48) 100%); }
+.splash-title { font-weight:700; letter-spacing:0.06em; line-height:1.15; }
+.splash-tag { margin-top:0.35rem; font-size:0.76rem; letter-spacing:0.2em; text-transform:uppercase; }
+.splash-hint { margin-top:1.25rem; font-size:0.72rem; letter-spacing:0.12em; }
+#splash-overlay .splash-gl-canvas { position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0; pointer-events:none; }
+/* Frosted panel so title/tag/hint stay readable over busy shaders */
+#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);
+ 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; }
+/* Images embedded in markdown posts */
+.post-text img { max-width:100%; max-height:320px; object-fit:contain; border-radius:6px; cursor:pointer; }
+#post-modal .post-text img, #modal-content img { max-height:none; cursor:default; }
+/* Markdown post typography: restore spacing stripped by the global reset.
+ Rules apply to both the list-view .post-text and the zoomed-in #modal-content
+ so the formatting is identical in both views. */
+.post-text p, #modal-content p { margin:0.65em 0; }
+.post-text ul, .post-text ol, #modal-content ul, #modal-content ol { margin:0.65em 0; padding-left:1.8em; }
+.post-text li, #modal-content li { margin:0.3em 0; }
+.post-text p:first-child, #modal-content p:first-child { margin-top:0; }
+.post-text p:last-child, .post-text ul:last-child, .post-text ol:last-child,
+#modal-content p:last-child, #modal-content ul:last-child, #modal-content ol:last-child { margin-bottom:0; }
+/* === CRT / VHS filter toggle === */
+#sno-crt-overlay { position:fixed; inset:0; z-index:9990; pointer-events:none; opacity:0; transition:opacity 0.35s ease; }
+body.sno-crt-on #sno-crt-overlay { opacity:1; }
+#sno-crt-overlay .crt-scanlines { position:absolute; inset:0;
+ background:repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.18) 2px, rgba(0,0,0,0.18) 4px); }
+#sno-crt-overlay .crt-flicker { position:absolute; inset:0;
+ background:rgba(255,255,255,0.03); animation:sno-crt-flicker 0.15s infinite; pointer-events:none; }
+@keyframes sno-crt-flicker { 0%,100%{opacity:1} 50%{opacity:0.85} }
+body.sno-crt-on .overlay { filter: contrast(1.12) brightness(0.94) saturate(0.85) url(#sno-crt-distort); }
+/* Ghost mode */
+body.sno-ghost-mode .post:not(.post-active) { opacity:0.08 !important; filter:grayscale(0.6); transition:opacity 0.4s ease, filter 0.4s ease; }
+body.sno-ghost-mode .post.post-active { opacity:1 !important; box-shadow:0 0 28px 6px currentColor !important; }
+body.sno-ghost-mode header,
+body.sno-ghost-mode .page-nav-footer,
+body.sno-ghost-mode .nav-hints { opacity:0.25 !important; transition:opacity 0.4s ease; }
+/* Post transition animations */
+@keyframes sno-enter-from-below { 0%{transform:translateY(22px) scale(0.97);opacity:0.55} 100%{transform:translateY(0) scale(1);opacity:1} }
+@keyframes sno-enter-from-above { 0%{transform:translateY(-22px) scale(0.97);opacity:0.55} 100%{transform:translateY(0) scale(1);opacity:1} }
+.sno-enter-down { animation:sno-enter-from-below 0.3s cubic-bezier(0.22,1,0.36,1) both !important; }
+.sno-enter-up { animation:sno-enter-from-above 0.3s cubic-bezier(0.22,1,0.36,1) both !important; }
+/* Seasonal auto-effects — CSS-only animated particles */
+@keyframes sno-snow-fall { from{background-position:0 0,60px 40px,120px 80px} to{background-position:0 240px,60px 280px,120px 320px} }
+@keyframes sno-spring-drift { from{background-position:0 0,80px 60px} to{background-position:40px 180px,120px 240px} }
+@keyframes sno-firefly { 0%,100%{opacity:0.2;background-position:0 0,100px 80px,50px 150px} 50%{opacity:0.6;background-position:20px 30px,120px 110px,70px 180px} }
+@keyframes sno-leaves-fall { from{background-position:0 0,70px 50px,140px 100px} to{background-position:-30px 200px,40px 250px,110px 300px} }
+body.sno-season-winter::after { content:''; position:fixed; inset:0; z-index:2; pointer-events:none;
+ background-image: radial-gradient(circle,rgba(255,255,255,0.75) 1px,transparent 1px), radial-gradient(circle,rgba(230,240,255,0.5) 1.5px,transparent 1.5px), radial-gradient(circle,rgba(255,255,255,0.35) 1px,transparent 1px);
+ background-size: 120px 120px, 180px 180px, 240px 240px; background-position:0 0,60px 40px,120px 80px;
+ animation:sno-snow-fall 10s linear infinite; opacity:0.3; }
+body.sno-season-spring::after { content:''; position:fixed; inset:0; z-index:2; pointer-events:none;
+ background-image: radial-gradient(circle,rgba(255,192,203,0.55) 1.5px,transparent 1.5px), radial-gradient(circle,rgba(255,255,180,0.4) 1px,transparent 1px);
+ background-size: 160px 160px, 220px 220px; background-position:0 0,80px 60px;
+ animation:sno-spring-drift 12s linear infinite; opacity:0.25; }
+body.sno-season-summer::after { content:''; position:fixed; inset:0; z-index:2; pointer-events:none;
+ background-image: radial-gradient(circle,rgba(180,255,80,0.5) 1.5px,transparent 1.5px), radial-gradient(circle,rgba(200,255,120,0.35) 1px,transparent 1px), radial-gradient(circle,rgba(160,240,60,0.25) 1.5px,transparent 1.5px);
+ background-size: 200px 200px, 280px 280px, 360px 360px; background-position:0 0,100px 80px,50px 150px;
+ animation:sno-firefly 6s ease-in-out infinite; opacity:0.35; }
+body.sno-season-autumn::after { content:''; position:fixed; inset:0; z-index:2; pointer-events:none;
+ background-image: radial-gradient(circle,rgba(210,105,30,0.5) 1.5px,transparent 1.5px), radial-gradient(circle,rgba(205,92,92,0.4) 1px,transparent 1px), radial-gradient(circle,rgba(218,165,32,0.35) 1.5px,transparent 1.5px);
+ background-size: 140px 140px, 200px 200px, 260px 260px; background-position:0 0,70px 50px,140px 100px;
+ animation:sno-leaves-fall 14s linear infinite; opacity:0.28; }
+/* Konami unlocked banner */
+#sno-konami-banner { position:fixed; top:40%; left:50%; transform:translate(-50%,-50%); z-index:9999; padding:14px 36px; font:700 1.8rem monospace; letter-spacing:0.4em; text-transform:uppercase; background:rgba(220,0,0,0.92); color:#fff; border:2px solid #fff; box-shadow:0 0 50px rgba(255,0,0,0.6); pointer-events:none; opacity:0; transition:opacity 0.3s; white-space:nowrap; }
+/* Reorganized nav-hints shared layout */
+.nav-hints { display:flex; flex-direction:column; gap:0; padding:0 !important; }
+.nav-hints-row { display:flex; align-items:center; flex-wrap:wrap; gap:clamp(10px, 2vw, 18px); padding:4px 24px; }
+.nav-hints-row + .nav-hints-row { border-top:1px solid rgba(128,128,128,0.12); }
+.nav-hints-label { font-size:0.58rem; text-transform:uppercase; letter-spacing:0.18em; opacity:0.45; min-width:2.2em; }
+.nav-fx-button {
+ appearance:none;
+ border:0;
+ background:none;
+ color:inherit;
+ font:inherit;
+ padding:0;
+ margin:0;
+ display:inline-flex;
+ align-items:center;
+ gap:0.35rem;
+ cursor:pointer;
+ opacity:0.86;
+ transition:opacity 0.18s ease, transform 0.18s ease, text-shadow 0.18s ease;
+}
+.nav-fx-button:hover { opacity:1; }
+.nav-fx-button[aria-pressed="true"] { opacity:1; text-shadow:0 0 10px currentColor; }
+.nav-fx-button.sno-fx-triggered { transform:translateY(1px) scale(0.98); }
+.nav-fx-button:focus-visible {
+ outline:1px solid currentColor;
+ outline-offset:4px;
+ border-radius:3px;
+}
+/* Parallax tilt */
+body.sno-parallax-on { perspective:1200px; }
+body.sno-parallax-on .overlay { transform-style:preserve-3d; transition:transform 0.1s ease-out; will-change:transform; }
+body.sno-parallax-on .post { transform-style:preserve-3d; transition:transform 0.1s ease-out; }
+/* Hacker hover scramble */
+.sno-scramble-target { display:inline-block; }
+/* Vaporwave sunset (synthwave/retro only) */
+#sno-vaporwave-sunset { position:fixed; bottom:0; left:0; right:0; height:35vh; z-index:1; pointer-events:none; opacity:0; transition:opacity 1s ease; }
+body.sno-vaporwave-on #sno-vaporwave-sunset { opacity:1; }
+/* Post scatter */
+@keyframes sno-scatter-out { 0%{transform:translate(0,0) rotate(0)} 40%{transform:translate(var(--sx),var(--sy)) rotate(var(--sr))} 100%{transform:translate(0,0) rotate(0)} }
+.sno-scatter { animation:sno-scatter-out 0.9s cubic-bezier(0.22,1,0.36,1) both !important; }
+/* Screenshot flash */
+#sno-shutter-flash { position:fixed; inset:0; z-index:9999; pointer-events:none; background:#fff; opacity:0; }
+/* Matrix decode on images */
+@keyframes sno-matrix-decode { 0%{filter:brightness(2) contrast(2) saturate(0) blur(1px);clip-path:inset(0 0 100% 0)} 30%{filter:brightness(1.5) contrast(3) saturate(0) blur(0.5px);clip-path:inset(0 0 60% 0)} 60%{filter:brightness(1.2) contrast(2) saturate(0.5) blur(0px);clip-path:inset(0 0 20% 0)} 100%{filter:brightness(1) contrast(1) saturate(1);clip-path:inset(0 0 0% 0)} }
+body[data-sno-theme="matrix"] .post-text img,
+body[data-sno-theme="terminal"] .post-text img,
+body[data-sno-theme="dos"] .post-text img { animation:sno-matrix-decode 0.8s ease-out both; }
diff --git a/internal/generator/templates/shared/shared.js b/internal/generator/templates/shared/shared.js
new file mode 100644
index 0000000..bf3cf35
--- /dev/null
+++ b/internal/generator/templates/shared/shared.js
@@ -0,0 +1,1343 @@
+ var SNONUX_SOUNDS = (typeof window !== "undefined" && window.SNONUX_SOUNDS) || {};
+ // Inject wild-mode badge used by all themes
+ (function() { var b=document.createElement('div'); b.id='sno-wild-badge'; b.textContent='WILD MODE'; document.body.appendChild(b); })();
+ const SNONUX_WILD_PRESETS = {
+ aurora: {
+ banner: 'SOLAR STORM',
+ ticker: ['FIELD INTERFERENCE', 'PLASMA DRIFT', 'PARTICLE BOMBARDMENT', 'CHROMATIC SPIKE'],
+ scraps: ['MAGNETIC SHEAR', 'SOLAR WIND', 'AURORA NOISE', 'ION STORM', 'POLAR ARC'],
+ flash: 'rgba(220,255,240,0.72)',
+ emoji: ['\u2728','\u2604\uFE0F','\u{1F320}','\u{1F30C}','\u2B50']
+ },
+ brutalist: {
+ banner: 'STRUCTURAL COLLAPSE',
+ ticker: ['CONDEMNED', 'REBAR EXPOSED', 'FOUNDATION FAILURE', 'CRACK PROPAGATION'],
+ scraps: ['CONDEMNED', 'RUST BLEED', 'SHEAR WALL LOST', 'LOAD PATH BROKEN', 'SPALLING'],
+ flash: 'rgba(255,210,190,0.58)',
+ emoji: ['\u{1F9F1}','\u2692\uFE0F','\u26A0\uFE0F','\u{1F6A7}','\u{1F4A5}']
+ },
+ cosmos: {
+ banner: 'SUPERNOVA',
+ ticker: ['SINGULARITY LENSING', 'GAMMA BURST', 'SHOCKWAVE EXPANDING', 'SPACETIME TEAR'],
+ scraps: ['WHITEOUT', 'EVENT HORIZON', 'RADIATION FRONT', 'LENS LOCK', 'CORE BREACH'],
+ flash: 'rgba(255,255,255,0.8)',
+ emoji: ['\u{1F30C}','\u2604\uFE0F','\u{1F4AB}','\u2B50','\u{1FA90}']
+ },
+ dos: {
+ banner: 'KERNEL PANIC',
+ ticker: ['ABORT, RETRY, FAIL?', 'MEMORY CORRUPTION', 'STACK DUMP', 'SEGMENT FAULT'],
+ scraps: ['DEAD BEEF', 'C0FFEE', 'BAD SECTOR', 'IRQ STORM', 'NULL PTR', 'HEX DUMP'],
+ flash: 'rgba(255,255,255,0.88)',
+ emoji: ['\u{1F4BE}','\u{1F4BB}','\u26A1','\u2620\uFE0F','\u{1F41B}']
+ },
+ matrix: {
+ banner: 'CASCADE FAILURE',
+ ticker: ['SENTINEL TRACE', 'GLYPH SATURATION', 'PHOSPHOR BURN-IN', 'RAIN AT TERMINAL VELOCITY'],
+ scraps: ['SENTINEL', '0XDECODE', 'OVERRIDE', 'TRACE LOST', 'MACHINE DREAM'],
+ flash: 'rgba(180,255,190,0.68)',
+ emoji: ['\u{1F441}\uFE0F','\u{1F4A0}','\u{1F916}','\u{1F50D}','\u26D3\uFE0F']
+ },
+ neon: {
+ banner: 'GAS DISCHARGE',
+ ticker: ['TUBE ARC', 'ULTRAVIOLET BLEED', 'STROBE LOCK', 'SHORT CIRCUIT'],
+ scraps: ['ARC OVERLOAD', 'PLASMA SIGN', 'NOBLE GAS', 'HARD STROBE', 'OVERDRIVE'],
+ flash: 'rgba(255,245,170,0.72)',
+ emoji: ['\u26A1','\u{1F4A5}','\u{1F52E}','\u2728','\u{1F388}']
+ },
+ ocean: {
+ banner: 'HADAL DESCENT',
+ ticker: ['PRESSURE SPIKE', 'BIOLUMINESCENT SWARM', 'ABYSSAL DRAG', 'TSUNAMI FRONT'],
+ scraps: ['NO SURFACE', 'CRUSH DEPTH', 'TENTACLE DRIFT', 'SONAR LOST', 'DEEP CURRENT'],
+ flash: 'rgba(200,255,255,0.54)',
+ emoji: ['\u{1F419}','\u{1F420}','\u{1F30A}','\u{1F41A}','\u{1F9DC}']
+ },
+ plasma: {
+ banner: 'FUSION BREACH',
+ ticker: ['CONTAINMENT FAILURE', 'TOKAMAK DISTORTION', 'THERMAL RUNAWAY', 'WHITE-BLUE CORE'],
+ scraps: ['ION SPRAY', 'FIELD LOSS', 'HEAT HAZE', 'QUENCH', 'ARC SHELL'],
+ flash: 'rgba(230,250,255,0.78)',
+ emoji: ['\u{1F300}','\u26A1','\u{1F4A0}','\u2728','\u{1F52C}']
+ },
+ retro: {
+ banner: 'TAPE EAT',
+ ticker: ['TRACKING LOSS', 'CHROMA SPLIT', 'MAGNETIC SNOW', 'CLICK-EJECT'],
+ scraps: ['NO SIGNAL', 'HEAD DRAG', 'ROLL HOLD', 'SNOW PACK', 'EJECT CYCLE'],
+ flash: 'rgba(255,226,178,0.6)',
+ emoji: ['\u{1F4FC}','\u{1F4FA}','\u{1F3AE}','\u{1F579}\uFE0F','\u{1F4FB}']
+ },
+ retrofuture: {
+ banner: 'ATOMIC TWILIGHT',
+ ticker: ['GEIGER STATIC', 'FALLOUT DUST', 'RADIATION BURN', 'IRRADIATED SEPIA'],
+ scraps: ['FALLOUT', 'BETA LEAK', 'ASH DRIFT', 'HALF-LIFE', 'GLOW CLOUD'],
+ flash: 'rgba(255,240,180,0.62)',
+ emoji: ['\u2622\uFE0F','\u{1F4A3}','\u{1F3ED}','\u2623\uFE0F','\u{1F9EA}']
+ },
+ spaceage: {
+ banner: 'RE-ENTRY BURN',
+ ticker: ['HEAT SHIELD LOSS', 'PLASMA BLACKOUT', 'COMMS STATIC', 'G-FORCE COMPRESSION'],
+ scraps: ['BLACKOUT', 'SPARK SHOWER', 'PLASMA SHEATH', 'HULL GLOW', 'COMMS LOST'],
+ flash: 'rgba(255,220,190,0.68)',
+ emoji: ['\u{1F680}','\u{1F6F8}','\u{1FA90}','\u{1F30D}','\u2B50']
+ },
+ synthwave: {
+ banner: 'GRID COLLAPSE',
+ ticker: ['VOID PERSPECTIVE', 'MOLTEN SUN', 'CHROMA TEAR', 'OUT OF MEMORY'],
+ scraps: ['VOID GRID', 'SUN DRIP', 'NEON PANIC', 'FRAME DROP', 'MEMORY STARVE'],
+ flash: 'rgba(255,210,255,0.68)',
+ emoji: ['\u{1F305}','\u{1F3B6}','\u{1F3B9}','\u{1F338}','\u{1F52E}']
+ },
+ terminal: {
+ banner: 'FORK BOMB',
+ ticker: ['PROCESS STORM', 'STACK TRACE WATERFALL', 'MEMORY GARBAGE', 'BSOD CREEP'],
+ scraps: ['PID 65535', 'STACK OVERFLOW', 'OOM KILL', 'PANIC', '(:'],
+ flash: 'rgba(180,255,180,0.7)',
+ emoji: ['\u{1F4BB}','\u{1F41B}','\u2620\uFE0F','\u{1F5A5}\uFE0F','\u26A1']
+ },
+ tropicale: {
+ banner: 'CATEGORY 5',
+ ticker: ['HORIZONTAL RAIN', 'STORM SURGE', 'DEBRIS FIELD', 'WIND SHEAR'],
+ scraps: ['PALM SNAP', 'SURGE LINE', 'SPRAY WALL', 'FLYING ROOF', 'LANDFALL'],
+ flash: 'rgba(240,255,255,0.74)',
+ emoji: ['\u{1F334}','\u{1F3D6}\uFE0F','\u{1F940}','\u{1F965}','\u{1F30A}']
+ },
+ noir: {
+ banner: 'BLACKOUT DISTRICT',
+ ticker: ['BLINDS SLAMMED SHUT', 'SIREN SWEEP', 'PROJECTOR BURN', 'MIDNIGHT DOWNPOUR'],
+ scraps: ['NO WITNESSES', 'WET ASPHALT', 'RED CHANNEL', 'BLUE CHANNEL', 'SMOKE CURTAIN'],
+ flash: 'rgba(255,245,225,0.66)',
+ emoji: ['\u{1F576}\uFE0F','\u{1F52B}','\u{1F3A9}','\u{1F6AC}','\u{1F5DD}\uFE0F']
+ },
+ cathedral: {
+ banner: 'LAST JUDGMENT',
+ ticker: ['BELL SHOCKWAVE', 'INCENSE FIRESTORM', 'ROSE WINDOW FRACTURE', 'APSE IN FLAME'],
+ scraps: ['REQUIEM', 'SHARD RAIN', 'VESPER BURN', 'GLORIA STATIC', 'NAVE COLLAPSE'],
+ flash: 'rgba(255,239,202,0.72)',
+ emoji: ['\u{1F54E}','\u{1F56F}\uFE0F','\u271D\uFE0F','\u{1F54A}\uFE0F','\u{1F3F0}']
+ },
+ surveillance: {
+ banner: 'TOTAL COMPROMISE',
+ ticker: ['CAMERA MESH BREACH', 'TRACKING LOSS', 'MULTIPLEX PANIC', 'ALERT CASCADE'],
+ scraps: ['FLAGGED', 'OVERRIDDEN', 'TRACE LOOP', 'BOX LOST', 'ALERT 99'],
+ flash: 'rgba(210,255,225,0.72)',
+ emoji: ['\u{1F4F9}','\u{1F441}\uFE0F','\u{1F6A8}','\u{1F50D}','\u{1F4E1}']
+ },
+ biomech: {
+ banner: 'CONTAINMENT RUPTURE',
+ ticker: ['SYNAPSE STORM', 'TISSUE ARC', 'MEMBRANE TEAR', 'HYBRID OVERDRIVE'],
+ scraps: ['VENTRICLE', 'MYCELIUM', 'RUPTURE', 'BIOFILM', 'NERVE GRID'],
+ flash: 'rgba(255,205,220,0.7)',
+ emoji: ['\u{1F9EC}','\u{1F9E0}','\u{1F9A0}','\u{1F52C}','\u{1FAC0}']
+ },
+ paper: {
+ banner: 'PRESS JAM',
+ ticker: ['TONER BLIZZARD', 'INK BLEED', 'COPY LAMP WHITEOUT', 'PAGE STORM'],
+ scraps: ['MISPRINT', 'SKEWED FEED', 'RAG EDGE', 'CARBON DUST', 'REDACTION'],
+ flash: 'rgba(255,250,236,0.82)',
+ emoji: ['\u{1F4C4}','\u270F\uFE0F','\u{1F4CE}','\u2702\uFE0F','\u{1F5DE}\uFE0F']
+ },
+ volcano: {
+ banner: 'PYROCLASTIC SURGE',
+ ticker: ['ASH CASCADE', 'LAVA BOMB IMPACT', 'EARTHQUAKE SHAKE', 'SULFUR CLOUD'],
+ scraps: ['ASHFALL', 'VENT BLAST', 'PYROCLAST', 'SEISMIC HIT', 'MAGMA SPRAY'],
+ flash: 'rgba(255,220,150,0.72)',
+ emoji: ['\u{1F30B}','\u{1F525}','\u{1F4A5}','\u2668\uFE0F','\u{1FAA8}']
+ }
+ };
+ function snonuxDetectThemeName() {
+ // The shell sets window.SNONUX_CURRENT_THEME synchronously in <head>.
+ // Falls back to the splash class for resilience if something raced.
+ if (typeof window !== 'undefined' && window.SNONUX_CURRENT_THEME) {
+ return window.SNONUX_CURRENT_THEME;
+ }
+ var splash = document.getElementById('splash-overlay');
+ if (splash) {
+ for (var i = 0; i < splash.classList.length; i++) {
+ var cls = splash.classList[i];
+ if (cls.indexOf('splash-') === 0 && cls !== 'splash-overlay' && cls.indexOf('splash--') !== 0) {
+ return cls.slice(7);
+ }
+ }
+ }
+ return 'neon';
+ }
+
+ // snonuxSwitchTheme persists the user's choice and reloads.
+ // The shell's boot script picks it up on the next load.
+ function snonuxSwitchTheme(theme) {
+ var all = (typeof window !== 'undefined' && window.SNONUX_ALL_THEMES) || [];
+ if (all.indexOf(theme) < 0) return;
+ try { localStorage.setItem('snonuxTheme', theme); } catch (_) {}
+ location.reload();
+ }
+
+ function snonuxRandomTheme() {
+ var all = (typeof window !== 'undefined' && window.SNONUX_ALL_THEMES) || [];
+ if (all.length <= 1) return null;
+ var current = snonuxDetectThemeName();
+ var pool = all.filter(function (t) { return t !== current; });
+ return pool[Math.floor(Math.random() * pool.length)];
+ }
+ function snonuxEnsureWildRoot() {
+ var root = document.getElementById('sno-wild-root');
+ if (root) return root;
+ root = document.createElement('div');
+ root.id = 'sno-wild-root';
+ root.setAttribute('aria-hidden', 'true');
+ root.innerHTML =
+ '<div id="sno-wild-colorwash" class="sno-wild-layer"></div>' +
+ '<div id="sno-wild-rain" class="sno-wild-layer"></div>' +
+ '<div id="sno-wild-wave" class="sno-wild-layer"></div>' +
+ '<div id="sno-wild-beacon" class="sno-wild-layer"></div>' +
+ '<div id="sno-wild-noise" class="sno-wild-layer"></div>' +
+ '<div id="sno-wild-banner"></div>' +
+ '<div id="sno-wild-ticker"><span></span></div>' +
+ '<div id="sno-wild-scraps"></div>';
+ document.body.appendChild(root);
+ return root;
+ }
+ function snonuxApplyWildPreset(theme) {
+ var preset = SNONUX_WILD_PRESETS[theme] || SNONUX_WILD_PRESETS.neon;
+ var html = document.documentElement;
+ var body = document.body;
+ html.setAttribute('data-sno-theme', theme);
+ body.setAttribute('data-sno-theme', theme);
+ var root = snonuxEnsureWildRoot();
+ var banner = root.querySelector('#sno-wild-banner');
+ var ticker = root.querySelector('#sno-wild-ticker span');
+ var scraps = root.querySelector('#sno-wild-scraps');
+ banner.textContent = preset.banner;
+ ticker.textContent = ' ' + preset.ticker.join(' // ') + ' // ' + preset.ticker.join(' // ') + ' ';
+ scraps.innerHTML = '';
+ var phrases = preset.scraps || [];
+ var count = theme === 'dos' || theme === 'terminal' || theme === 'matrix' ? 22 : 16;
+ for (var i = 0; i < count; i++) {
+ var span = document.createElement('span');
+ span.textContent = phrases[i % phrases.length];
+ span.style.setProperty('--x', (8 + Math.random() * 84).toFixed(2) + '%');
+ span.style.setProperty('--y', (12 + Math.random() * 72).toFixed(2) + '%');
+ span.style.setProperty('--rot', ((Math.random() * 36) - 18).toFixed(1) + 'deg');
+ span.style.setProperty('--dx', ((Math.random() * 120) - 60).toFixed(1) + 'px');
+ span.style.setProperty('--dy', ((Math.random() * 100) - 50).toFixed(1) + 'px');
+ span.style.setProperty('--dur', (4.5 + Math.random() * 5.5).toFixed(2) + 's');
+ span.style.setProperty('--delay', (-Math.random() * 6).toFixed(2) + 's');
+ scraps.appendChild(span);
+ }
+ window._snonuxWildTheme = theme;
+ window._snonuxWildFlashColor = preset.flash || 'rgba(255,255,255,0.7)';
+ }
+ function snonuxPulseFlash(color, duration) {
+ var ov = document.createElement('div');
+ ov.style.cssText = 'position:fixed;inset:0;z-index:9998;pointer-events:none;opacity:0;';
+ document.body.appendChild(ov);
+ var tone = color || window._snonuxWildFlashColor || 'rgba(255,255,255,0.7)';
+ [0, 70, 150, 250].forEach(function(d, i) {
+ setTimeout(function() {
+ ov.style.transition = 'opacity 0.08s linear';
+ ov.style.background = tone;
+ ov.style.opacity = (i % 2 === 0) ? '0.82' : '0';
+ }, d);
+ });
+ setTimeout(function() {
+ ov.style.transition = 'opacity 0.25s linear';
+ ov.style.opacity = '0';
+ }, Math.max(180, duration || 320));
+ setTimeout(function() { ov.remove(); }, Math.max(520, duration || 320) + 260);
+ }
+ function snonuxScheduleWildBursts() {
+ clearTimeout(window._snonuxWildBurstTimer);
+ if (!window._snoWildActive) return;
+ var delay = 1400 + Math.random() * 3600;
+ window._snonuxWildBurstTimer = setTimeout(function() {
+ if (!window._snoWildActive) return;
+ snonuxPulseFlash(window._snonuxWildFlashColor, 260);
+ snonuxScheduleWildBursts();
+ }, delay);
+ }
+ function snonuxSetWildState(on) {
+ var body = document.body;
+ var badge = document.getElementById('sno-wild-badge');
+ body.classList.toggle('sno-wild-active', !!on);
+ if (badge) badge.classList.toggle('sno-wild-on', !!on);
+ if (on) {
+ snonuxApplyWildPreset(window._snonuxWildTheme || snonuxDetectThemeName());
+ snonuxScheduleWildBursts();
+ body.classList.add('sno-wild-hue');
+ snonuxStartFlyingEmoji();
+ snonuxStartRandomFlips();
+ } else {
+ clearTimeout(window._snonuxWildBurstTimer);
+ body.classList.remove('sno-wild-hue');
+ snonuxStopFlyingEmoji();
+ snonuxStopRandomFlips();
+ }
+ }
+ // === WILD FLYING EMOJI ===
+ function snonuxStartFlyingEmoji() {
+ snonuxStopFlyingEmoji();
+ var zone = document.getElementById('sno-flyzone');
+ if (!zone) {
+ zone = document.createElement('div');
+ zone.id = 'sno-flyzone';
+ zone.setAttribute('aria-hidden', 'true');
+ document.body.appendChild(zone);
+ }
+ function spawn() {
+ if (!window._snoWildActive) return;
+ var preset = SNONUX_WILD_PRESETS[window._snonuxWildTheme] || SNONUX_WILD_PRESETS.neon;
+ var emojis = preset.emoji || ['\u2B50'];
+ var s = document.createElement('span');
+ s.textContent = emojis[Math.floor(Math.random() * emojis.length)];
+ var top = (5 + Math.random() * 80).toFixed(1);
+ var dur = (3 + Math.random() * 4).toFixed(2);
+ var dir = Math.random() > 0.5 ? 'sno-fly-lr' : 'sno-fly-rl';
+ var wobble = ((Math.random() - 0.5) * 60).toFixed(0);
+ var rot = (180 + Math.random() * 540).toFixed(0);
+ s.style.setProperty('--ftop', top + '%');
+ s.style.setProperty('--fy', wobble + 'px');
+ s.style.setProperty('--frot', rot + 'deg');
+ s.style.animationName = dir;
+ s.style.animationDuration = dur + 's';
+ zone.appendChild(s);
+ setTimeout(function() { s.remove(); }, parseFloat(dur) * 1000 + 200);
+ window._snoFlyTimer = setTimeout(spawn, 800 + Math.random() * 2200);
+ }
+ spawn();
+ }
+ function snonuxStopFlyingEmoji() {
+ clearTimeout(window._snoFlyTimer);
+ var zone = document.getElementById('sno-flyzone');
+ if (zone) zone.innerHTML = '';
+ }
+ // === WILD RANDOM FLIPS ===
+ function snonuxStartRandomFlips() {
+ snonuxStopRandomFlips();
+ function flip() {
+ if (!window._snoWildActive) return;
+ var allPosts = document.querySelectorAll('.post:not(.post-active)');
+ if (allPosts.length > 0) {
+ var p = allPosts[Math.floor(Math.random() * allPosts.length)];
+ p.classList.add('sno-fx-flip');
+ setTimeout(function() { p.classList.remove('sno-fx-flip'); }, 1500);
+ }
+ window._snoFlipTimer = setTimeout(flip, 2500 + Math.random() * 4000);
+ }
+ window._snoFlipTimer = setTimeout(flip, 1500 + Math.random() * 2000);
+ }
+ function snonuxStopRandomFlips() {
+ clearTimeout(window._snoFlipTimer);
+ var flipped = document.querySelectorAll('.sno-fx-flip');
+ flipped.forEach(function(el) { el.classList.remove('sno-fx-flip'); });
+ }
+ (function snonuxWildSetup() {
+ window._snoWildActive = !!window._snoWildActive;
+ snonuxApplyWildPreset(snonuxDetectThemeName());
+ snonuxSetWildState(window._snoWildActive);
+ })();
+ // Dramatic lightning flash on wild mode activation/deactivation
+ function snonuxWildFlash(on) {
+ var ov = document.createElement('div');
+ ov.style.cssText = 'position:fixed;inset:0;z-index:9998;pointer-events:none;';
+ document.body.appendChild(ov);
+ if (on) {
+ // Three rapid lightning flashes on activation
+ [0, 80, 180, 300, 440].forEach(function(d, i) {
+ setTimeout(function() {
+ ov.style.background = (i % 2 === 0) ? 'rgba(255,255,200,0.72)' : 'transparent';
+ }, d);
+ });
+ setTimeout(function() { ov.remove(); }, 550);
+ // Persistent storm overlay with intermittent flicker
+ var storm = document.createElement('div');
+ storm.id = 'sno-wild-storm';
+ storm.style.cssText = 'position:fixed;inset:0;z-index:4998;pointer-events:none;' +
+ 'background:radial-gradient(ellipse at 50% 0%,rgba(255,255,200,0.88) 0%,transparent 55%);' +
+ 'animation:sno-wild-flicker 3.7s ease-in-out infinite;';
+ document.body.appendChild(storm);
+ } else {
+ // Brief dark veil on deactivation
+ ov.style.background = 'rgba(0,0,0,0.45)';
+ ov.style.transition = 'opacity 0.45s';
+ setTimeout(function() { ov.style.opacity = '0'; }, 60);
+ setTimeout(function() { ov.remove(); }, 550);
+ var storm = document.getElementById('sno-wild-storm');
+ if (storm) { storm.style.transition = 'opacity 0.5s'; storm.style.opacity = '0'; setTimeout(function() { storm.remove(); }, 600); }
+ }
+ }
+ function snonuxWaveType(w) {
+ if (w === 'square') return 'square';
+ if (w === 'triangle') return 'triangle';
+ return 'sine';
+ }
+ (function splashSetup() {
+ var el = document.getElementById('splash-overlay');
+ if (!el) return;
+ var splashAudioCtx = null;
+ var splashChimePlayed = false;
+ function playSplashChime() {
+ if (splashChimePlayed) return;
+ try {
+ if (!splashAudioCtx) {
+ splashAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ }
+ var ctx = splashAudioCtx;
+ function ring() {
+ splashChimePlayed = true;
+ // Theme override: if defined, plays its own sound and returns true to skip the default chime.
+ if (window.snonuxSplashSound && window.snonuxSplashSound(ctx)) return;
+ var now = ctx.currentTime;
+ var sp = SNONUX_SOUNDS.splash;
+ var freqs = sp.freqs;
+ var spacing = sp.spacing != null ? sp.spacing : 0.075;
+ var gainAm = sp.gain != null ? sp.gain : 0.1;
+ var wave = snonuxWaveType(sp.wave);
+ var i, osc, g, t0;
+ for (i = 0; i < freqs.length; i++) {
+ osc = ctx.createOscillator();
+ g = ctx.createGain();
+ osc.connect(g);
+ g.connect(ctx.destination);
+ osc.type = wave;
+ osc.frequency.value = freqs[i];
+ t0 = now + i * spacing;
+ g.gain.setValueAtTime(0, t0);
+ g.gain.linearRampToValueAtTime(gainAm, t0 + 0.028);
+ g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.52);
+ osc.start(t0);
+ osc.stop(t0 + 0.55);
+ }
+ }
+ ctx.resume().then(ring).catch(function() {});
+ } 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');
+ el.focus({ preventScroll: true });
+ }
+ 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();
+ }
+ function bindHeaderTriggers() {
+ var triggers = document.querySelectorAll('.logo-mark, .logo-title h1, #sn-logo');
+ triggers.forEach(function(trigger) {
+ trigger.addEventListener('click', openSplashFromHeader);
+ });
+ }
+ bindHeaderTriggers();
+ // Exposed so the runtime theme-meta apply can re-bind after replacing
+ // <header> innerHTML — the freshly-injected nodes have no listeners.
+ window._snonuxRebindHeader = bindHeaderTriggers;
+ window._snonuxDismissSplash = dismiss;
+ window._snonuxShowSplash = show;
+ if (document.documentElement.classList.contains('sno-splash-skip')) {
+ dismiss();
+ return;
+ }
+ playSplashChime();
+ el.addEventListener('pointerdown', function() { playSplashChime(); }, { passive: true });
+ el.addEventListener('click', function(e) { e.preventDefault(); dismiss(); });
+ el.focus({ preventScroll: true });
+ })();
+
+ // === SPECIAL EFFECTS HELPERS ===
+ function playBassDrop() {
+ try {
+ var ctx = new (window.AudioContext || window.webkitAudioContext)();
+ var osc = ctx.createOscillator();
+ var gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.type = 'sawtooth';
+ osc.frequency.setValueAtTime(130, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(28, ctx.currentTime + 0.55);
+ gain.gain.setValueAtTime(0.22, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.9);
+ osc.start(); osc.stop(ctx.currentTime + 0.95);
+ } catch(_) {}
+ }
+ function snonuxParticleBurst(count, color) {
+ var burst = document.createElement('div');
+ burst.id = 'sno-burst';
+ burst.setAttribute('aria-hidden', 'true');
+ document.body.appendChild(burst);
+ var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
+ for (var i = 0; i < count; i++) {
+ var s = document.createElement('span');
+ var angle = (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.6;
+ var dist = 60 + Math.random() * 280;
+ s.style.left = cx + 'px';
+ s.style.top = cy + 'px';
+ s.style.setProperty('--px', (Math.cos(angle) * dist).toFixed(1) + 'px');
+ s.style.setProperty('--py', (Math.sin(angle) * dist).toFixed(1) + 'px');
+ s.style.setProperty('--pdur', (0.25 + Math.random() * 0.35).toFixed(2) + 's');
+ s.style.setProperty('--pdel', (Math.random() * 0.08).toFixed(2) + 's');
+ s.style.width = (3 + Math.random() * 6) + 'px';
+ s.style.height = s.style.width;
+ s.style.background = color || 'currentColor';
+ burst.appendChild(s);
+ }
+ setTimeout(function() { burst.remove(); }, 900);
+ }
+ function snonuxKonamiExplosion() {
+ document.querySelectorAll('.post').forEach(function(p) {
+ p.classList.add('sno-fx-flip');
+ setTimeout(function() { p.classList.remove('sno-fx-flip'); }, 1500);
+ });
+ var ov = document.createElement('div');
+ ov.style.cssText = 'position:fixed;inset:0;z-index:9997;pointer-events:none;background:rgba(255,255,255,0.92);mix-blend-mode:difference;transition:opacity 0.25s';
+ document.body.appendChild(ov);
+ setTimeout(function() { ov.style.opacity='0'; setTimeout(function(){ov.remove();}, 300); }, 220);
+ snonuxParticleBurst(72, '#fff');
+ playBassDrop();
+ var banner = document.createElement('div');
+ banner.id = 'sno-konami-banner';
+ banner.textContent = 'UNLOCKED';
+ document.body.appendChild(banner);
+ requestAnimationFrame(function() { banner.style.opacity='1'; });
+ setTimeout(function() { banner.style.opacity='0'; setTimeout(function(){banner.remove();}, 350); }, 2200);
+ }
+ function snonuxCriticalOverload() {
+ var ov = document.createElement('div');
+ ov.style.cssText = 'position:fixed;inset:0;z-index:9997;pointer-events:none;background:#fff;mix-blend-mode:difference;';
+ document.body.appendChild(ov);
+ [0, 70, 140, 210].forEach(function(d,i){
+ setTimeout(function(){ ov.style.opacity = (i%2===0)?'1':'0.25'; }, d);
+ });
+ setTimeout(function(){ ov.style.transition='opacity 0.3s'; ov.style.opacity='0'; setTimeout(function(){ov.remove();}, 350); }, 320);
+ document.querySelectorAll('.post').forEach(function(p){
+ p.classList.add('sno-fx-flip');
+ setTimeout(function(){ p.classList.remove('sno-fx-flip'); }, 1500);
+ });
+ playBassDrop();
+ snonuxParticleBurst(96, window._snonuxWildFlashColor || '#fff');
+ }
+ // Konami code tracker
+ (function konamiSetup(){
+ var seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
+ var idx = 0;
+ document.addEventListener('keydown', function(e){
+ if (e.key === seq[idx]) { idx++; if (idx >= seq.length) { idx = 0; snonuxKonamiExplosion(); } }
+ else { idx = (e.key === seq[0]) ? 1 : 0; }
+ });
+ })();
+ // CRT overlay injection
+ (function crtSetup(){
+ if (document.getElementById('sno-crt-overlay')) return;
+ var crt = document.createElement('div');
+ crt.id = 'sno-crt-overlay';
+ crt.innerHTML = '<div class="crt-scanlines"></div><div class="crt-flicker"></div>';
+ crt.setAttribute('aria-hidden','true');
+ document.body.appendChild(crt);
+ var svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
+ svg.id = 'sno-crt-svg';
+ svg.setAttribute('style','position:absolute;width:0;height:0;');
+ svg.innerHTML = '<defs><filter id="sno-crt-distort"><feTurbulence type="fractalNoise" baseFrequency="0.012 0.006" numOctaves="2" result="noise"/><feDisplacementMap in="SourceGraphic" in2="noise" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter></defs>';
+ document.body.appendChild(svg);
+ })();
+ // Seasonal effects
+ (function seasonalEffects(){
+ var month = new Date().getMonth();
+ var season = (month >= 11 || month <= 1) ? 'winter' : (month <= 4) ? 'spring' : (month <= 7) ? 'summer' : 'autumn';
+ document.body.classList.add('sno-season-' + season);
+ })();
+ // === VAPORWAVE SUNSET (synthwave / retro only) ===
+ (function vaporwaveSetup(){
+ var theme = snonuxDetectThemeName();
+ if (theme !== 'synthwave' && theme !== 'retro') return;
+ var sunset = document.createElement('div');
+ sunset.id = 'sno-vaporwave-sunset';
+ sunset.setAttribute('aria-hidden','true');
+ sunset.innerHTML = '<svg style="width:100%;height:100%;" preserveAspectRatio="none" viewBox="0 0 100 100"><defs><linearGradient id="vwg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#ff00cc" stop-opacity="0.35"/><stop offset="40%" stop-color="#ff00cc" stop-opacity="0.1"/><stop offset="100%" stop-color="#0a0a1a" stop-opacity="0"/></linearGradient></defs><ellipse cx="50" cy="95" rx="55" ry="18" fill="url(#vwg)"/><rect x="0" y="0" width="100" height="100" fill="none"/><g opacity="0.35">' + Array.from({length:12},function(_,i){return '<rect x="0" y="' + (75 + i*2.1) + '" width="100" height="' + (1.2 + i*0.2) + '" fill="#ff00cc" opacity="' + (0.5 - i*0.04).toFixed(2) + '"/>';}).join('') + '</g></svg>';
+ document.body.appendChild(sunset);
+ document.body.classList.add('sno-vaporwave-on');
+ })();
+ // === PARALLAX TILT ===
+ (function parallaxSetup(){
+ var enabled = true; // on by default
+ document.body.classList.add('sno-parallax-on');
+ window.snonuxToggleParallax = function(on){ enabled = on; document.body.classList.toggle('sno-parallax-on', on); };
+ document.addEventListener('mousemove', function(e){
+ if (!enabled) return;
+ var x = (e.clientX / window.innerWidth - 0.5) * 2;
+ var y = (e.clientY / window.innerHeight - 0.5) * 2;
+ var ov = document.querySelector('.overlay');
+ if (ov) {
+ var rotY = (x * 4.5).toFixed(2);
+ var rotX = (-y * 4.0).toFixed(2);
+ ov.style.transform = 'rotateY(' + rotY + 'deg) rotateX(' + rotX + 'deg) scale(1.04)';
+ }
+ // Subtle depth per post
+ document.querySelectorAll('.post').forEach(function(p, i){
+ var depth = ((i % 3) - 1) * 12;
+ p.style.transform = 'translateZ(' + depth + 'px)';
+ });
+ }, { passive: true });
+ })();
+ // === HACKER HOVER SCRAMBLE ===
+ (function scrambleSetup(){
+ var glyphs = '!<>-_\\/[]{}—=+*^?#________';
+ function scramble(el){
+ var original = el.getAttribute('data-scramble') || el.textContent;
+ if (!el.getAttribute('data-scramble')) el.setAttribute('data-scramble', original);
+ var len = original.length;
+ var iter = 0;
+ var interval = setInterval(function(){
+ el.textContent = original.split('').map(function(ch, i){
+ if (i < iter) return original[i];
+ return glyphs[Math.floor(Math.random() * glyphs.length)];
+ }).join('');
+ iter += 1/3;
+ if (iter >= len) { clearInterval(interval); el.textContent = original; }
+ }, 30);
+ }
+ function attach(){
+ document.querySelectorAll('.post-header strong, .logo-title h1, .splash-title').forEach(function(el){
+ if (el._scrambleAttached) return;
+ el._scrambleAttached = true;
+ el.addEventListener('mouseenter', function(){ scramble(el); });
+ });
+ }
+ attach();
+ // re-attach after any dynamic content changes (not common here but safe)
+ window.snonuxAttachScramble = attach;
+ })();
+ // === SCREENSHOT FLASH ===
+ function snonuxScreenshotFlash(){
+ var flash = document.createElement('div');
+ flash.id = 'sno-shutter-flash';
+ document.body.appendChild(flash);
+ flash.style.transition = 'none';
+ flash.style.opacity = '1';
+ // Shutter sound
+ try {
+ var ctx = new (window.AudioContext || window.webkitAudioContext)();
+ var osc = ctx.createOscillator();
+ var gain = ctx.createGain();
+ var noise = ctx.createScriptProcessor(4096, 1, 1);
+ noise.onaudioprocess = function(e){
+ var out = e.outputBuffer.getChannelData(0);
+ for (var i = 0; i < out.length; i++) out[i] = (Math.random() * 2 - 1) * 0.15;
+ };
+ noise.connect(gain); gain.connect(ctx.destination);
+ gain.gain.setValueAtTime(0.15, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12);
+ noise.connect(gain);
+ osc.connect(gain);
+ osc.type = 'square';
+ osc.frequency.setValueAtTime(600, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(60, ctx.currentTime + 0.1);
+ osc.start(); osc.stop(ctx.currentTime + 0.12);
+ setTimeout(function(){ noise.disconnect(); }, 200);
+ } catch(_) {}
+ setTimeout(function(){
+ flash.style.transition = 'opacity 0.35s ease';
+ flash.style.opacity = '0';
+ setTimeout(function(){ flash.remove(); }, 400);
+ }, 80);
+ }
+ // === POST SCATTER ===
+ function snonuxPostScatter(){
+ var all = document.querySelectorAll('.post');
+ all.forEach(function(p){
+ var sx = ((Math.random() - 0.5) * window.innerWidth * 0.6).toFixed(1) + 'px';
+ var sy = ((Math.random() - 0.5) * window.innerHeight * 0.6).toFixed(1) + 'px';
+ var sr = ((Math.random() - 0.5) * 25).toFixed(1) + 'deg';
+ p.style.setProperty('--sx', sx);
+ p.style.setProperty('--sy', sy);
+ p.style.setProperty('--sr', sr);
+ p.classList.add('sno-scatter');
+ setTimeout(function(){ p.classList.remove('sno-scatter'); }, 950);
+ });
+ playBassDrop();
+ }
+ // === RAINBOW SPARKLE TRAIL ===
+ (function rainbowSparkle(){
+ var throttle = 0;
+ var hue = 0;
+ document.addEventListener('pointermove', function(e){
+ var now = Date.now();
+ if (now - throttle < 45) return;
+ throttle = now;
+ var d = document.createElement('div');
+ d.className = 'sno-sparkle';
+ var size = 3 + Math.random() * 5;
+ d.style.width = size + 'px';
+ d.style.height = size + 'px';
+ d.style.left = (e.clientX - size / 2 + (Math.random() - 0.5) * 10) + 'px';
+ d.style.top = (e.clientY - size / 2 + (Math.random() - 0.5) * 10) + 'px';
+ d.style.background = 'hsl(' + hue + ',90%,65%)';
+ d.style.opacity = '0.85';
+ d.style.animation = 'sno-sparkle ' + (0.35 + Math.random() * 0.25).toFixed(2) + 's ease-out forwards';
+ document.body.appendChild(d);
+ setTimeout(function() { d.remove(); }, 650);
+ hue = (hue + 22) % 360;
+ }, { passive: true });
+ })();
+ // === KEYBOARD NAVIGATION ===
+ // j / ArrowDown → next post k / ArrowUp → previous post
+ // h / ArrowLeft → previous page l / ArrowRight → next page
+ // PageUp/PageDown → scroll the post list; re-highlight post at top of visible area
+ // Enter / click post → expand modal Esc → close modal
+ const posts = document.querySelectorAll('.post');
+ let currentIndex = posts.length > 0 ? 0 : -1;
+ var prevPageURL = (typeof window !== "undefined") ? (window.snonuxPrevPageURL || null) : null;
+ var nextPageURL = (typeof window !== "undefined") ? (window.snonuxNextPageURL || null) : null;
+
+ if (currentIndex >= 0) selectPost(0);
+
+ function setActiveHighlight(index, playSound, scrollIntoView) {
+ if (posts.length === 0) return;
+ var prevIdx = currentIndex;
+ if (currentIndex >= 0) posts[currentIndex].classList.remove('post-active');
+ currentIndex = Math.max(0, Math.min(index, posts.length - 1));
+ posts[currentIndex].classList.add('post-active');
+ if (prevIdx >= 0 && prevIdx !== currentIndex && posts[prevIdx]) {
+ var ghost = posts[prevIdx].querySelector('.sno-afterimage');
+ if (!ghost) {
+ ghost = document.createElement('div');
+ ghost.className = 'sno-afterimage';
+ posts[prevIdx].appendChild(ghost);
+ }
+ ghost.classList.remove('sno-afterimage-active');
+ void ghost.offsetWidth;
+ ghost.classList.add('sno-afterimage-active');
+ }
+ if (scrollIntoView) {
+ posts[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ if (playSound) playNavSound();
+ }
+
+ function selectPost(index, direction) {
+ setActiveHighlight(index, true, true);
+ if (direction && posts[currentIndex]) {
+ var post = posts[currentIndex];
+ post.classList.remove('sno-enter-down', 'sno-enter-up');
+ void post.offsetWidth;
+ post.classList.add(direction === 'down' ? 'sno-enter-down' : 'sno-enter-up');
+ setTimeout(function() { post.classList.remove('sno-enter-down', 'sno-enter-up'); }, 320);
+ }
+ if (window.snonuxNavEffect) window.snonuxNavEffect();
+ }
+
+ /** Pick the post that should be active for the current viewport (anchor near top of visible area). */
+ function activeIndexForVisibleRegion(sc) {
+ if (posts.length === 0) return -1;
+ var scrTop, scrBot, anchorY;
+ if (sc) {
+ var scr = sc.getBoundingClientRect();
+ scrTop = scr.top;
+ scrBot = scr.bottom;
+ anchorY = scr.top + Math.min(scr.height * 0.18, 100);
+ } else {
+ scrTop = 0;
+ scrBot = window.innerHeight;
+ anchorY = window.innerHeight * 0.15;
+ }
+ var i, pr;
+ for (i = 0; i < posts.length; i++) {
+ pr = posts[i].getBoundingClientRect();
+ if (pr.top <= anchorY && anchorY < pr.bottom) return i;
+ }
+ for (i = 0; i < posts.length; i++) {
+ pr = posts[i].getBoundingClientRect();
+ if (pr.bottom > scrTop && pr.top < scrBot) return i;
+ }
+ return posts.length - 1;
+ }
+
+ function playNavSound() {
+ try {
+ var n = SNONUX_SOUNDS.nav;
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.frequency.value = n.freq;
+ osc.type = snonuxWaveType(n.wave);
+ var dur = n.dur != null ? n.dur : 0.08;
+ var g = n.gain != null ? n.gain : 0.12;
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.02);
+ } catch (_) {}
+ }
+
+ function playOpenSound() {
+ try {
+ var o = SNONUX_SOUNDS.open;
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.type = snonuxWaveType(o.wave);
+ var dur = o.dur != null ? o.dur : 0.14;
+ var g = o.gain != null ? o.gain : 0.1;
+ osc.frequency.setValueAtTime(o.start, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(o.end, ctx.currentTime + dur);
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.06);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.07);
+ } catch (_) {}
+ }
+
+ function playCloseSound() {
+ try {
+ var c = SNONUX_SOUNDS.close;
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.type = snonuxWaveType(c.wave);
+ var dur = c.dur != null ? c.dur : 0.15;
+ var g = c.gain != null ? c.gain : 0.1;
+ osc.frequency.setValueAtTime(c.start, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(c.end, ctx.currentTime + dur);
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.05);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.06);
+ } catch (_) {}
+ }
+
+ function playBounceSound() {
+ try {
+ var b = SNONUX_SOUNDS.bounce;
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.type = snonuxWaveType(b.wave);
+ var dur = b.dur != null ? b.dur : 0.12;
+ var g = b.gain != null ? b.gain : 0.1;
+ osc.frequency.setValueAtTime(b.start, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(b.end, ctx.currentTime + dur);
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.05);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.06);
+ } catch (_) {}
+ }
+
+ var _snoBounceCls = ['sno-fx-bounce-left','sno-fx-bounce-right','sno-fx-bounce-left-wild','sno-fx-bounce-right-wild',
+ 'sno-fx-bounce-up','sno-fx-bounce-down','sno-fx-bounce-up-wild','sno-fx-bounce-down-wild'];
+
+ function bounceEffect(dir) {
+ var ov = document.querySelector('.overlay');
+ if (!ov) return;
+ var wild = !!window._snoWildActive;
+ var map = { left: wild ? 'sno-fx-bounce-left-wild' : 'sno-fx-bounce-left',
+ right: wild ? 'sno-fx-bounce-right-wild' : 'sno-fx-bounce-right',
+ up: wild ? 'sno-fx-bounce-up-wild' : 'sno-fx-bounce-up',
+ down: wild ? 'sno-fx-bounce-down-wild' : 'sno-fx-bounce-down' };
+ var cls = map[dir] || map.down;
+ _snoBounceCls.forEach(function(c) { ov.classList.remove(c); });
+ void ov.offsetWidth;
+ ov.classList.add(cls);
+ var dur = wild ? 540 : 380;
+ setTimeout(function() { ov.classList.remove(cls); }, dur);
+ playBounceSound();
+ if (wild) snonuxPulseFlash(window._snonuxWildFlashColor, 200);
+ }
+
+ // === 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 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;
+ 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() {
+ vx *= FRICTION;
+ vy *= FRICTION;
+ x += vx;
+ y += vy;
+ clampAndBounce();
+ 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 {
+ raf = null;
+ }
+ }
+
+ function ensureLoop() {
+ if (!raf) raf = requestAnimationFrame(tick);
+ }
+
+ return {
+ keyPush: function(e) {
+ var dx = 0, dy = 0;
+ switch (e.key) {
+ case 'h': case 'ArrowLeft': dx = -PUSH; break;
+ case 'l': case 'ArrowRight': dx = PUSH; break;
+ case 'k': case 'ArrowUp': dy = -PUSH; break;
+ case 'j': case 'ArrowDown': dy = PUSH; break;
+ default: return false;
+ }
+ e.preventDefault();
+ vx += dx;
+ vy += dy;
+ 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 el = getEl();
+ if (el) el.style.transform = '';
+ if (raf) { cancelAnimationFrame(raf); raf = null; }
+ },
+ stop: function() {
+ if (raf) { cancelAnimationFrame(raf); raf = null; }
+ 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);
+ }
+ }
+ );
+
+ function getFxButton(name) {
+ return document.querySelector('.nav-fx-button[data-sno-fx="' + name + '"]');
+ }
+
+ function pulseFxButton(name) {
+ var button = getFxButton(name);
+ if (!button) return;
+ button.classList.remove('sno-fx-triggered');
+ void button.offsetWidth;
+ button.classList.add('sno-fx-triggered');
+ setTimeout(function() { button.classList.remove('sno-fx-triggered'); }, 180);
+ }
+
+ function syncFxButtonStates() {
+ var wildButton = getFxButton('wild');
+ if (wildButton) wildButton.setAttribute('aria-pressed', window._snoWildActive ? 'true' : 'false');
+ var crtButton = getFxButton('crt');
+ if (crtButton) crtButton.setAttribute('aria-pressed', document.body.classList.contains('sno-crt-on') ? 'true' : 'false');
+ var ghostButton = getFxButton('ghost');
+ if (ghostButton) ghostButton.setAttribute('aria-pressed', document.body.classList.contains('sno-ghost-mode') ? 'true' : 'false');
+ }
+
+ function setWildMode(on, opts) {
+ window._snoWildActive = !!on;
+ if (window.snonuxWildToggle) window.snonuxWildToggle();
+ snonuxSetWildState(window._snoWildActive);
+ if (opts && opts.splashMode) snonuxWildFlash(window._snoWildActive);
+ else if (window._snoWildActive) snonuxCriticalOverload();
+ else snonuxWildFlash(false);
+ if (opts && opts.kickSplash && splashDrift) {
+ splashDrift.kick((Math.random() - 0.5) * 24, -10 - Math.random() * 10);
+ }
+ pulseFxButton('wild');
+ syncFxButtonStates();
+ }
+
+ function toggleWildMode(opts) {
+ setWildMode(!window._snoWildActive, opts);
+ }
+
+ function toggleCrtMode() {
+ document.body.classList.toggle('sno-crt-on');
+ pulseFxButton('crt');
+ syncFxButtonStates();
+ }
+
+ function toggleGhostMode() {
+ document.body.classList.toggle('sno-ghost-mode');
+ pulseFxButton('ghost');
+ syncFxButtonStates();
+ }
+
+ function triggerFlashEffect() {
+ snonuxScreenshotFlash();
+ pulseFxButton('flash');
+ }
+
+ function triggerScatterEffect() {
+ snonuxPostScatter();
+ pulseFxButton('scatter');
+ }
+
+ (function setupNavFxButtons() {
+ var fxHandlers = {
+ wild: function() { toggleWildMode(); },
+ crt: toggleCrtMode,
+ ghost: toggleGhostMode,
+ flash: triggerFlashEffect,
+ scatter: triggerScatterEffect
+ };
+ document.querySelectorAll('.nav-fx-button').forEach(function(button) {
+ button.addEventListener('click', function(e) {
+ e.preventDefault();
+ var fx = button.getAttribute('data-sno-fx');
+ if (fxHandlers[fx]) fxHandlers[fx]();
+ });
+ });
+ syncFxButtonStates();
+ })();
+
+ // 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) {
+ if (posts.length === 0) return;
+ setActiveHighlight(index, false, !!scrollIntoView);
+ var post = posts[currentIndex];
+ var postText = post ? post.querySelector('.post-text') : null;
+ if (!postText) return;
+ var modal = document.getElementById('post-modal');
+ var modalInner = modal ? modal.querySelector('.modal-inner') : null;
+ document.getElementById('modal-content').innerHTML = postText.innerHTML;
+ modal.classList.add('active');
+ modalDrift.reset();
+ if (window.snonuxOpenEffect) window.snonuxOpenEffect(post);
+ modal.scrollTop = 0;
+ if (modalInner) {
+ modalInner.scrollTop = 0;
+ requestAnimationFrame(function() {
+ modalInner.scrollIntoView({ block: 'center', inline: 'nearest' });
+ });
+ }
+ playOpenSound();
+ }
+
+ function closeModal() {
+ modalDrift.stop();
+ document.getElementById('post-modal').classList.remove('active');
+ playCloseSound();
+ if (window.snonuxCloseEffect) window.snonuxCloseEffect();
+ }
+
+ (function postClickOpen() {
+ posts.forEach(function(post, idx) {
+ post.addEventListener('click', function(e) {
+ if (e.target.closest('a, button, audio, video, input, textarea, select, label')) return;
+ openPostAt(idx, true);
+ });
+ });
+ })();
+
+ (function deepLinkFromHash() {
+ var h = location.hash;
+ if (!h || h.indexOf('#post-') !== 0) return;
+ var id = decodeURIComponent(h.slice(6));
+ var el = document.getElementById('post-' + id);
+ if (!el) return;
+ var idx = parseInt(el.getAttribute('data-index'), 10);
+ if (isNaN(idx)) return;
+ openPostAt(idx, true);
+ })();
+
+ document.addEventListener('keydown', function(e) {
+ var tag = e.target.tagName;
+ // Skip when typing into a form control. SELECT counts: native typeahead
+ // (e.g. matching options by first letter) would otherwise be hijacked
+ // by 'w', 'c', 't', etc. shortcuts.
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
+ var splash = document.getElementById('splash-overlay');
+ if (splash && !splash.classList.contains('splash--dismissed')) {
+ 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();
+ toggleWildMode({ splashMode: true, kickSplash: true });
+ } else if (e.key === 'c' && !e.repeat) {
+ e.preventDefault();
+ toggleCrtMode();
+ } else if (e.key === 'g' && !e.repeat) {
+ e.preventDefault();
+ toggleGhostMode();
+ } else if (e.key === 't' && !e.repeat) {
+ e.preventDefault();
+ var pick = snonuxRandomTheme();
+ if (pick) snonuxSwitchTheme(pick);
+ } else if (splashDrift.keyPush(e)) {
+ playNavSound();
+ }
+ return;
+ }
+ if (document.getElementById('post-modal').classList.contains('active')) {
+ if (e.key === 'Escape') { closeModal(); e.preventDefault(); }
+ else if (e.key === 't' && !e.repeat) {
+ e.preventDefault();
+ var pick = snonuxRandomTheme();
+ if (pick) snonuxSwitchTheme(pick);
+ }
+ else if (modalDrift.keyPush(e)) { playNavSound(); }
+ return;
+ }
+ switch (e.key) {
+ case 'PageUp':
+ case 'PageDown': {
+ var sc = document.getElementById('post-content');
+ var step = (sc && sc.clientHeight) ? sc.clientHeight : window.innerHeight;
+ var dy = (e.key === 'PageUp') ? -step : step;
+ if (sc) {
+ sc.scrollTop += dy;
+ } else {
+ window.scrollBy(0, dy);
+ }
+ var idx = activeIndexForVisibleRegion(sc);
+ if (idx >= 0) setActiveHighlight(idx, true, false);
+ if (window.snonuxScrollEffect) window.snonuxScrollEffect(e.key === 'PageUp' ? 'up' : 'down');
+ e.preventDefault();
+ break;
+ }
+ case 'j': case 'ArrowDown':
+ if (currentIndex >= posts.length - 1) { bounceEffect('down'); }
+ else { selectPost(currentIndex + 1, 'down'); }
+ e.preventDefault(); break;
+ case 'k': case 'ArrowUp':
+ if (currentIndex <= 0) { bounceEffect('up'); }
+ else { selectPost(currentIndex - 1, 'up'); }
+ e.preventDefault(); break;
+ case 'h': case 'ArrowLeft':
+ if (prevPageURL) { playNavSound(); if (window.snonuxPageEffect) window.snonuxPageEffect(); window.location.href = prevPageURL; }
+ else { bounceEffect('left'); }
+ e.preventDefault(); break;
+ case 'l': case 'ArrowRight':
+ if (nextPageURL) { playNavSound(); if (window.snonuxPageEffect) window.snonuxPageEffect(); window.location.href = nextPageURL; }
+ else { bounceEffect('right'); }
+ e.preventDefault(); break;
+ case 'Enter': openPostAt(currentIndex, true); e.preventDefault(); break;
+ case 'w': {
+ toggleWildMode();
+ e.preventDefault(); break;
+ }
+ case 'c':
+ toggleCrtMode();
+ e.preventDefault(); break;
+ case 'g':
+ toggleGhostMode();
+ e.preventDefault(); break;
+ case 'p':
+ triggerFlashEffect();
+ e.preventDefault(); break;
+ case 'x':
+ triggerScatterEffect();
+ e.preventDefault(); break;
+ case 't': {
+ var pick = snonuxRandomTheme();
+ if (pick) snonuxSwitchTheme(pick);
+ e.preventDefault(); break;
+ }
+ }
+ });
+
+ // === MODAL SCROLL-END INDICATOR ===
+ (function modalScrollEnd() {
+ var mi = document.querySelector('#post-modal .modal-inner');
+ if (!mi) return;
+ mi.addEventListener('scroll', function() {
+ var atEnd = mi.scrollHeight - mi.scrollTop - mi.clientHeight < 4;
+ var el = document.getElementById('sno-scroll-end');
+ if (!el) return;
+ if (atEnd) {
+ el.classList.remove('sno-scroll-end-active');
+ void el.offsetWidth;
+ el.classList.add('sno-scroll-end-active');
+ }
+ }, { passive: true });
+ })();
+
+ // === IDLE BREATHING ===
+ (function idleBreathe() {
+ var timer = null;
+ var IDLE_DELAY = 10000;
+ function startBreathe() {
+ stopBreathe();
+ timer = setTimeout(function() {
+ if (currentIndex >= 0 && posts[currentIndex]) {
+ posts[currentIndex].classList.add('sno-idle-breathe');
+ }
+ }, IDLE_DELAY);
+ }
+ function stopBreathe() {
+ clearTimeout(timer);
+ for (var i = 0; i < posts.length; i++) {
+ posts[i].classList.remove('sno-idle-breathe');
+ }
+ }
+ function resetIdle() { stopBreathe(); startBreathe(); }
+ document.addEventListener('keydown', resetIdle);
+ document.addEventListener('pointermove', resetIdle, { passive: true });
+ document.addEventListener('pointerdown', resetIdle, { passive: true });
+ startBreathe();
+ })();
+
+ // === FIRST-VISIT PARTICLE BURST ===
+ (function firstVisitBurst() {
+ var key = 'sno-visited';
+ try { if (sessionStorage.getItem(key)) return; sessionStorage.setItem(key, '1'); } catch (_) { return; }
+ if (document.documentElement.classList.contains('sno-splash-skip')) return;
+ var origDismiss = window._snonuxDismissSplash;
+ if (!origDismiss) return;
+ window._snonuxDismissSplash = function() {
+ origDismiss();
+ var burst = document.createElement('div');
+ burst.id = 'sno-burst';
+ burst.setAttribute('aria-hidden', 'true');
+ document.body.appendChild(burst);
+ var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
+ for (var i = 0; i < 36; i++) {
+ var s = document.createElement('span');
+ var angle = (i / 36) * Math.PI * 2 + (Math.random() - 0.5) * 0.4;
+ var dist = 80 + Math.random() * 180;
+ s.style.left = cx + 'px';
+ s.style.top = cy + 'px';
+ s.style.setProperty('--px', (Math.cos(angle) * dist).toFixed(1) + 'px');
+ s.style.setProperty('--py', (Math.sin(angle) * dist).toFixed(1) + 'px');
+ s.style.setProperty('--pdur', (0.4 + Math.random() * 0.5).toFixed(2) + 's');
+ s.style.setProperty('--pdel', (Math.random() * 0.12).toFixed(2) + 's');
+ s.style.width = (4 + Math.random() * 5) + 'px';
+ s.style.height = s.style.width;
+ burst.appendChild(s);
+ }
+ setTimeout(function() { burst.remove(); }, 1200);
+ };
+ })();
+
+ // === THEME APPLICATION (runtime) ===
+ // The shell renders header/splash/title for SNONUX_DEFAULT_THEME. If the
+ // user has saved a different theme, fetch its meta.json and swap the
+ // theme-specific markup. Once the splash overlay is final we load theme.js
+ // — its splash WebGL initialiser must bind to the canvas it will animate.
+ (function snonuxApplyThemeMeta() {
+ if (typeof window === 'undefined') return;
+ var current = window.SNONUX_CURRENT_THEME;
+ var def = window.SNONUX_DEFAULT_THEME;
+ if (!current) return;
+
+ var splashOverlay = document.getElementById('splash-overlay');
+ if (splashOverlay) {
+ // theme.css selectors target via [data-sno-theme] (set by the
+ // boot script on <html>); the legacy splash-<theme> class on the
+ // overlay is added defensively for any rule still keyed off it.
+ splashOverlay.classList.add('splash-' + current);
+ }
+
+ function applyMeta(m) {
+ if (m.title) document.title = m.title;
+ var headerEl = document.querySelector('header');
+ if (headerEl && m.header_html) headerEl.innerHTML = m.header_html;
+ if (splashOverlay && m.splash_inner_html) splashOverlay.innerHTML = m.splash_inner_html;
+ var prevA = document.getElementById('sno-prev-page');
+ if (prevA && m.prev_page_text) prevA.innerHTML = m.prev_page_text;
+ var nextA = document.getElementById('sno-next-page');
+ if (nextA && m.next_page_text) nextA.innerHTML = m.next_page_text;
+ if (typeof window._snonuxRebindHeader === 'function') window._snonuxRebindHeader();
+ }
+
+ function loadThemeJS() {
+ var s = document.createElement('script');
+ s.src = 'themes/' + current + '/theme.js';
+ document.head.appendChild(s);
+ }
+
+ if (current === def) {
+ // Baked markup is already correct. Load theme.js straight away.
+ loadThemeJS();
+ return;
+ }
+
+ // Switched theme: swap markup + sounds first, *then* load theme.js so
+ // its splash WebGL attaches to the final canvas.
+ var pending = 2;
+ function done() { if (--pending === 0) loadThemeJS(); }
+ fetch('themes/' + current + '/meta.json')
+ .then(function (r) { return r.json(); })
+ .then(applyMeta)
+ .catch(function () {})
+ .finally(done);
+ fetch('themes/' + current + '/sounds.json')
+ .then(function (r) { return r.json(); })
+ .then(function (s) { window.SNONUX_SOUNDS = s; SNONUX_SOUNDS = s; })
+ .catch(function () {})
+ .finally(done);
+ })();
+
+ // === THEME DROPDOWN (populates select#sno-theme-select) ===
+ (function snonuxThemeDropdown() {
+ var sel = document.getElementById('sno-theme-select');
+ if (!sel) return;
+ var all = (typeof window !== 'undefined' && window.SNONUX_ALL_THEMES) || [];
+ var current = snonuxDetectThemeName();
+ sel.innerHTML = '';
+ for (var i = 0; i < all.length; i++) {
+ var name = all[i];
+ var opt = document.createElement('option');
+ opt.value = name;
+ opt.textContent = name;
+ if (name === current) opt.selected = true;
+ sel.appendChild(opt);
+ }
+ sel.addEventListener('change', function () {
+ snonuxSwitchTheme(sel.value);
+ });
+ })();
+
diff --git a/internal/generator/templates/shell.tmpl b/internal/generator/templates/shell.tmpl
new file mode 100644
index 0000000..71f18cd
--- /dev/null
+++ b/internal/generator/templates/shell.tmpl
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{.DefaultTitle}}</title>
+ <link rel="icon" href="favicon.ico" sizes="any">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
+ <script>
+ // Theme registry (filled at gen time). Keep in sync with templates/themes/*.
+ var SNONUX_ALL_THEMES = {{.AllThemesJSON}};
+ var SNONUX_DEFAULT_THEME = "{{.DefaultTheme}}";
+
+ // Per-page navigation state (computed at gen time, used by shared.js).
+ window.snonuxPrevPageURL = {{.PrevPageJSON}};
+ window.snonuxNextPageURL = {{.NextPageJSON}};
+
+ // Default theme's Web Audio preset is baked in so the splash chime
+ // can fire instantly. shared.js may overwrite this with the saved
+ // theme's sounds.json if the user picked a different one.
+ window.SNONUX_SOUNDS = {{.DefaultSoundsJSON}};
+
+ // Pick the active theme synchronously so paint blocks on the right
+ // stylesheet — no FOUC. Falls back to the default if localStorage
+ // holds nothing (or something we don't recognise).
+ (function () {
+ var saved = null;
+ try { saved = localStorage.getItem('snonuxTheme'); } catch (_) {}
+ var theme = (saved && SNONUX_ALL_THEMES.indexOf(saved) >= 0) ? saved : SNONUX_DEFAULT_THEME;
+ window.SNONUX_CURRENT_THEME = theme;
+ document.documentElement.setAttribute('data-sno-theme', theme);
+ // document.write blocks parsing until the stylesheet loads, which
+ // is exactly what we want for paint to happen after theme CSS is
+ // ready. Slightly old-school but reliable across browsers.
+ document.write('<link rel="stylesheet" href="themes/' + theme + '/theme.css">');
+ })();
+ </script>
+ <link rel="stylesheet" href="shared.css">
+ <script src="shared.js" defer></script>
+ <!--
+ theme.js is appended dynamically by shared.js *after* the splash markup
+ reflects the active theme. Loading it earlier would let the splash WebGL
+ initialiser attach to the default theme's canvas just before shared.js
+ replaces the overlay content, leaving the WebGL bound to a detached node.
+ -->
+</head>
+<body>
+ {{template "splashGate"}}
+ <div id="splash-overlay" class="splash-overlay" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">{{.DefaultSplashHTML}}</div>
+ <canvas id="three-canvas"></canvas>
+ <div class="overlay">
+ <header>{{.DefaultHeaderHTML}}</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}}" id="sno-prev-page">{{.DefaultPrevText}}</a>{{end}}
+ {{if .NextPage}}<a href="{{.NextPage}}" id="sno-next-page">{{.DefaultNextText}}</a>{{end}}
+ </div>
+ </footer>
+ {{end}}
+ </div>
+ {{template "navmodal" .}}
+</body>
+</html>
diff --git a/internal/generator/templates/themes/aurora.tmpl b/internal/generator/templates/themes/aurora.tmpl
deleted file mode 100644
index e71c576..0000000
--- a/internal/generator/templates/themes/aurora.tmpl
+++ /dev/null
@@ -1,359 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ✦ AURORA</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --green:#00ffb3; --teal:#00cfe8; --purple:#c084fc; --navy:#050d1a; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy);
- color:#e0f8f0; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(5,13,26,0.78); backdrop-filter:blur(14px);
- border-bottom:1px solid rgba(0,255,179,0.25); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800; background:linear-gradient(90deg,var(--green),var(--teal));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:#e0f8f0; letter-spacing:1px; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(224,248,240,0.55); margin-top:2px; }
- .logo-title .subtitle a { color:var(--green); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--green); }
- .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px;
- border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--teal); color:var(--navy); }
- a.header-feed-link { color:var(--green); }
- a.header-feed-link:hover { color:var(--teal); text-shadow:0 0 8px var(--green); }
- .nav-hints { background:rgba(5,13,26,0.6); border-bottom:1px solid rgba(0,255,179,0.15);
- color:rgba(224,248,240,0.45); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(0,255,179,0.1); border:1px solid rgba(0,255,179,0.35);
- color:var(--green); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--green) var(--navy); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--teal); color:var(--teal); padding:8px 20px;
- border-radius:20px; text-decoration:none; font-size:0.82rem; letter-spacing:1px; }
- .page-nav a:hover { background:var(--teal); color:var(--navy); }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(5,13,26,0.78); backdrop-filter:blur(14px);
- border-top:1px solid rgba(0,255,179,0.25); }
- .post { background:rgba(5,20,35,0.72); border:1px solid rgba(0,255,179,0.2); border-radius:10px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(6px); }
- .post:hover { border-color:var(--green); box-shadow:0 0 20px rgba(0,255,179,0.2); transform:translateY(-2px); }
- .post-active { border-color:var(--purple) !important; background:rgba(15,5,40,0.9) !important;
- box-shadow:0 0 24px rgba(192,132,252,0.35),inset 3px 0 0 var(--purple) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--teal); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--green); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--green); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(5,13,26,0.95); backdrop-filter:blur(20px);
- overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:760px; margin:0 auto; background:rgba(5,20,40,0.97);
- border:1px solid var(--green); border-radius:12px;
- box-shadow:0 0 60px rgba(0,255,179,0.25); padding:40px; }
- .modal-close { float:right; background:none; border:none; color:var(--teal);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- .splash-overlay.splash-aurora {
- background: radial-gradient(ellipse 120% 80% at 50% 120%, rgba(0,207,232,0.12) 0%, transparent 45%),
- radial-gradient(ellipse 90% 60% at 20% 20%, rgba(192,132,252,0.08) 0%, transparent 50%),
- linear-gradient(165deg, #030811 0%, var(--navy) 35%, #0a1628 70%, #050d1a 100%);
- background-size: 100% 100%, 100% 100%, 200% 200%;
- animation: splashAuroraShift 14s ease-in-out infinite;
- }
- @keyframes splashAuroraShift { 0%,100%{background-position:0% 0%, 0% 0%, 0% 50%} 50%{background-position:0% 0%, 0% 0%, 100% 50%} }
- .splash-aurora .splash-aurora-stars {
- position:absolute; inset:0; pointer-events:none; z-index:0; opacity:0.45;
- background-image:
- radial-gradient(1px 1px at 8% 12%, rgba(255,255,255,0.7) 50%, transparent 51%),
- radial-gradient(1px 1px at 22% 28%, rgba(255,255,255,0.5) 50%, transparent 51%),
- radial-gradient(1px 1px at 78% 18%, rgba(255,255,255,0.6) 50%, transparent 51%),
- radial-gradient(1px 1px at 92% 35%, rgba(255,255,255,0.45) 50%, transparent 51%),
- radial-gradient(1px 1px at 45% 8%, rgba(224,248,240,0.5) 50%, transparent 51%),
- radial-gradient(1px 1px at 65% 42%, rgba(255,255,255,0.4) 50%, transparent 51%);
- }
- .splash-aurora .splash-aurora-glow {
- position:absolute; left:50%; bottom:-5vh; transform:translateX(-50%); width:140%; height:55vh;
- pointer-events:none; z-index:0; opacity:0.55;
- background: radial-gradient(ellipse 75% 55% at 50% 100%, rgba(0,255,179,0.22) 0%, transparent 62%),
- radial-gradient(ellipse 55% 40% at 35% 95%, rgba(0,207,232,0.12) 0%, transparent 55%),
- radial-gradient(ellipse 50% 38% at 70% 92%, rgba(192,132,252,0.14) 0%, transparent 55%);
- filter: blur(0.5px);
- }
- .splash-aurora .splash-title {
- font-size:clamp(1.5rem,4.8vw,2.15rem); font-weight:700; letter-spacing:0.04em;
- background: linear-gradient(120deg, #e8fff4 0%, var(--green) 45%, var(--teal) 78%, #e0e8ff 100%);
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
- filter: drop-shadow(0 0 28px rgba(0,255,179,0.35));
- }
- .splash-aurora .splash-tag {
- margin-top:0.5rem; font-size:0.74rem; letter-spacing:0.28em; text-transform:uppercase;
- color:rgba(0,207,232,0.92); text-shadow:0 0 20px rgba(0,255,179,0.4);
- }
- .splash-aurora .splash-hint { color:rgba(224,248,240,0.88); margin-top:1.1rem; }
- .splash-aurora .splash-inner { position:relative; z-index:2; }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-aurora" 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-aurora-stars" aria-hidden="true"></div>
- <div class="splash-aurora-glow" aria-hidden="true"></div>
- <div class="splash-inner">
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Aurora uplink</div>
- <div class="splash-hint">Click or Enter to open 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,ribbons=[],SEG=48;
- var cols=[0x00ffb3,0x00cfe8,0xc084fc,0x48e8d0,0xa855f7];
- var yPos=[2,5.5,9,12.5,16], zPos=[-18,-14,-10,-7,-4];
- function cleanup(){
- window.removeEventListener('resize',sz);
- if(raf)cancelAnimationFrame(raf);raf=null;
- ribbons.forEach(function(rb){rb.geo.dispose();rb.mesh.material.dispose();});
- ribbons=[];
- 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(52,1,0.1,120);
- ca.position.set(0,10,26);ca.lookAt(0,8,-6);
- clock=new THREE.Clock();
- for(var r=0;r<5;r++){
- var geo=new THREE.PlaneGeometry(100,7,SEG,1);
- var mat=new THREE.MeshBasicMaterial({
- color:cols[r],transparent:true,opacity:0.26+r*0.02,
- side:THREE.DoubleSide,blending:THREE.AdditiveBlending,depthWrite:false
- });
- var mesh=new THREE.Mesh(geo,mat);
- mesh.position.set(0,yPos[r],zPos[r]);
- sc.add(mesh);
- ribbons.push({mesh:mesh,geo:geo,freq:0.55+0.12*r,phase:r*1.15,amp:2.4+0.2*r});
- }
- function loop(){
- raf=requestAnimationFrame(loop);
- var t=clock.getElapsedTime();
- for(var i=0;i<ribbons.length;i++){
- var rb=ribbons[i],pos=rb.geo.attributes.position;
- for(var v=0;v<pos.count;v++){
- if(pos.getY(v)>0){
- var x=pos.getX(v);
- pos.setY(v,rb.amp*Math.sin(t*rb.freq+x*0.065+rb.phase)
- +rb.amp*0.38*Math.cos(t*rb.freq*0.72+x*0.042));
- }
- }
- pos.needsUpdate=true;
- }
- ren.render(sc,ca);
- }
- sz();window.addEventListener('resize',sz);
- 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">Transmit</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>
- // Aurora WebGL: six wide ribbon meshes whose top-row vertices are animated
- // with overlapping sine waves, rendered with additive blending so they glow
- // against the dark navy sky like real aurora curtains.
- (function() {
- var _wild = false, _snoTOffset = 0, _snoLastT = 0;
- var RIBBON_COUNT = 6;
- var SEG_W = 60; // horizontal segments per ribbon
- var ribbonColors = [0x00ffb3, 0x00cfe8, 0xc084fc, 0x00ffb3, 0x48e8d0, 0xa855f7];
- var ribbonY = [-10, -4, 2, 8, 14, 20];
- var ribbonZ = [-40, -30, -22, -15, -10, -5];
- var ribbonFreq = [0.6, 0.9, 0.7, 1.1, 0.5, 0.8];
- var ribbonPhase = [0.0, 1.2, 2.4, 0.8, 3.1, 1.7];
- var ribbonAmp = [3.0, 2.5, 2.0, 3.5, 2.2, 2.8];
-
- var scene, camera, renderer, clock;
- var ribbons = [];
-
- function initThree() {
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x050d1a);
- scene.fog = new THREE.Fog(0x050d1a, 40, 120);
-
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
- camera.position.set(0, 5, 30);
- camera.lookAt(0, 5, 0);
-
- 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();
-
- for (var r = 0; r < RIBBON_COUNT; r++) {
- // Wide shallow plane; we animate the top row of vertices
- var geo = new THREE.PlaneGeometry(120, 8, SEG_W, 1);
- var mat = new THREE.MeshBasicMaterial({
- color: ribbonColors[r], transparent: true, opacity: 0.32,
- side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false
- });
- var mesh = new THREE.Mesh(geo, mat);
- mesh.position.set(0, ribbonY[r], ribbonZ[r]);
- scene.add(mesh);
- ribbons.push({ mesh: mesh, geo: geo, freq: ribbonFreq[r],
- phase: ribbonPhase[r], amp: ribbonAmp[r] });
- }
-
- 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();
- // Wild mode: accelerate time 18× so waves churn much faster
- _snoTOffset += (realT - _snoLastT) * (_wild ? 18 : 0);
- _snoLastT = realT;
- var t = realT + _snoTOffset;
-
- var ampMult = _wild ? 3.2 : 1;
- // Camera sways left/right and bobs up/down in wild mode
- camera.position.x = _wild ? Math.sin(t * 0.28) * 10 : 0;
- camera.position.y = _wild ? 5 + Math.cos(t * 0.19) * 4 : 5;
-
- for (var r = 0; r < ribbons.length; r++) {
- var rb = ribbons[r];
- var pos = rb.geo.attributes.position;
- var count = pos.count;
- // In wild mode ribbons also drift vertically so they cross and tangle
- var yDrift = _wild ? Math.sin(t * rb.freq * 0.4 + r * 1.1) * 6 : 0;
- rb.mesh.position.y = ribbonY[r] + yDrift;
- // PlaneGeometry vertices: (SEG_W+1)*2 total; top row is every other vertex
- for (var i = 0; i < count; i++) {
- var x = pos.getX(i);
- // Only animate top row (y > 0 in local space) for the waving top edge
- if (pos.getY(i) > 0) {
- pos.setY(i, rb.amp * ampMult * Math.sin(t * rb.freq + x * 0.08 + rb.phase)
- + rb.amp * ampMult * 0.4 * Math.cos(t * rb.freq * 0.7 + x * 0.05));
- }
- }
- pos.needsUpdate = true;
- }
- renderer.render(scene, camera);
- }
-
- initThree();
-
- // Aurora nav/wild effects — snow burst on navigate, blizzard storm on wild
- 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'); }, 400); }
- // Frost shimmer — aurora-colored radial
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:997;pointer-events:none;background:radial-gradient(ellipse at center,rgba(0,255,179,0.14) 0%,rgba(192,132,252,0.1) 55%,transparent 80%);transition:opacity 0.3s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 340); }, 15);
- };
- window.snonuxCloseEffect = function() {
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,207,232,0.12);transition:opacity 0.2s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 15);
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,transparent,rgba(0,207,232,0.9),rgba(120,200,100,0.9),rgba(0,207,232,0.9),transparent);' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.32s ease,opacity 0.32s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? '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);
- };
- window.snonuxNavEffect = function() {
- // Snow burst — CSS snowflakes scatter from cursor
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 380); }
- // Frost flash
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:radial-gradient(ellipse at center,rgba(0,255,179,0.18) 0%,rgba(192,132,252,0.1) 60%,transparent 100%);transition:opacity 0.22s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 250); }, 30);
- };
- window.snonuxPageEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/aurora/meta.json b/internal/generator/templates/themes/aurora/meta.json
new file mode 100644
index 0000000..b0b2cf5
--- /dev/null
+++ b/internal/generator/templates/themes/aurora/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ✦ AURORA",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-aurora-stars\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-aurora-glow\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eAurora uplink\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to open the feed\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/aurora/theme.css b/internal/generator/templates/themes/aurora/theme.css
new file mode 100644
index 0000000..325624c
--- /dev/null
+++ b/internal/generator/templates/themes/aurora/theme.css
@@ -0,0 +1,94 @@
+ :root { --green:#00ffb3; --teal:#00cfe8; --purple:#c084fc; --navy:#050d1a; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy);
+ color:#e0f8f0; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(5,13,26,0.78); backdrop-filter:blur(14px);
+ border-bottom:1px solid rgba(0,255,179,0.25); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800; background:linear-gradient(90deg,var(--green),var(--teal));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:#e0f8f0; letter-spacing:1px; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(224,248,240,0.55); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--green); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--green); }
+ .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--teal); color:var(--navy); }
+ a.header-feed-link { color:var(--green); }
+ a.header-feed-link:hover { color:var(--teal); text-shadow:0 0 8px var(--green); }
+ .nav-hints { background:rgba(5,13,26,0.6); border-bottom:1px solid rgba(0,255,179,0.15);
+ color:rgba(224,248,240,0.45); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(0,255,179,0.1); border:1px solid rgba(0,255,179,0.35);
+ color:var(--green); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--green) var(--navy); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--teal); color:var(--teal); padding:8px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.82rem; letter-spacing:1px; }
+ .page-nav a:hover { background:var(--teal); color:var(--navy); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(5,13,26,0.78); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(0,255,179,0.25); }
+ .post { background:rgba(5,20,35,0.72); border:1px solid rgba(0,255,179,0.2); border-radius:10px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(6px); }
+ .post:hover { border-color:var(--green); box-shadow:0 0 20px rgba(0,255,179,0.2); transform:translateY(-2px); }
+ .post-active { border-color:var(--purple) !important; background:rgba(15,5,40,0.9) !important;
+ box-shadow:0 0 24px rgba(192,132,252,0.35),inset 3px 0 0 var(--purple) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--teal); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--green); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--green); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(5,13,26,0.95); backdrop-filter:blur(20px);
+ overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:rgba(5,20,40,0.97);
+ border:1px solid var(--green); border-radius:12px;
+ box-shadow:0 0 60px rgba(0,255,179,0.25); padding:40px; }
+ .modal-close { float:right; background:none; border:none; color:var(--teal);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ [data-sno-theme="aurora"] .splash-overlay {
+ background: radial-gradient(ellipse 120% 80% at 50% 120%, rgba(0,207,232,0.12) 0%, transparent 45%),
+ radial-gradient(ellipse 90% 60% at 20% 20%, rgba(192,132,252,0.08) 0%, transparent 50%),
+ linear-gradient(165deg, #030811 0%, var(--navy) 35%, #0a1628 70%, #050d1a 100%);
+ background-size: 100% 100%, 100% 100%, 200% 200%;
+ animation: splashAuroraShift 14s ease-in-out infinite;
+ }
+ @keyframes splashAuroraShift { 0%,100%{background-position:0% 0%, 0% 0%, 0% 50%} 50%{background-position:0% 0%, 0% 0%, 100% 50%} }
+ [data-sno-theme="aurora"] [data-sno-theme="aurora"]-stars {
+ position:absolute; inset:0; pointer-events:none; z-index:0; opacity:0.45;
+ background-image:
+ radial-gradient(1px 1px at 8% 12%, rgba(255,255,255,0.7) 50%, transparent 51%),
+ radial-gradient(1px 1px at 22% 28%, rgba(255,255,255,0.5) 50%, transparent 51%),
+ radial-gradient(1px 1px at 78% 18%, rgba(255,255,255,0.6) 50%, transparent 51%),
+ radial-gradient(1px 1px at 92% 35%, rgba(255,255,255,0.45) 50%, transparent 51%),
+ radial-gradient(1px 1px at 45% 8%, rgba(224,248,240,0.5) 50%, transparent 51%),
+ radial-gradient(1px 1px at 65% 42%, rgba(255,255,255,0.4) 50%, transparent 51%);
+ }
+ [data-sno-theme="aurora"] [data-sno-theme="aurora"]-glow {
+ position:absolute; left:50%; bottom:-5vh; transform:translateX(-50%); width:140%; height:55vh;
+ pointer-events:none; z-index:0; opacity:0.55;
+ background: radial-gradient(ellipse 75% 55% at 50% 100%, rgba(0,255,179,0.22) 0%, transparent 62%),
+ radial-gradient(ellipse 55% 40% at 35% 95%, rgba(0,207,232,0.12) 0%, transparent 55%),
+ radial-gradient(ellipse 50% 38% at 70% 92%, rgba(192,132,252,0.14) 0%, transparent 55%);
+ filter: blur(0.5px);
+ }
+ [data-sno-theme="aurora"] .splash-title {
+ font-size:clamp(1.5rem,4.8vw,2.15rem); font-weight:700; letter-spacing:0.04em;
+ background: linear-gradient(120deg, #e8fff4 0%, var(--green) 45%, var(--teal) 78%, #e0e8ff 100%);
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent;
+ filter: drop-shadow(0 0 28px rgba(0,255,179,0.35));
+ }
+ [data-sno-theme="aurora"] .splash-tag {
+ margin-top:0.5rem; font-size:0.74rem; letter-spacing:0.28em; text-transform:uppercase;
+ color:rgba(0,207,232,0.92); text-shadow:0 0 20px rgba(0,255,179,0.4);
+ }
+ [data-sno-theme="aurora"] .splash-hint { color:rgba(224,248,240,0.88); margin-top:1.1rem; }
+ [data-sno-theme="aurora"] .splash-inner { position:relative; z-index:2; }
diff --git a/internal/generator/templates/themes/aurora/theme.js b/internal/generator/templates/themes/aurora/theme.js
new file mode 100644
index 0000000..624a4a7
--- /dev/null
+++ b/internal/generator/templates/themes/aurora/theme.js
@@ -0,0 +1,200 @@
+
+ (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,ribbons=[],SEG=48;
+ var cols=[0x00ffb3,0x00cfe8,0xc084fc,0x48e8d0,0xa855f7];
+ var yPos=[2,5.5,9,12.5,16], zPos=[-18,-14,-10,-7,-4];
+ function cleanup(){
+ window.removeEventListener('resize',sz);
+ if(raf)cancelAnimationFrame(raf);raf=null;
+ ribbons.forEach(function(rb){rb.geo.dispose();rb.mesh.material.dispose();});
+ ribbons=[];
+ 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(52,1,0.1,120);
+ ca.position.set(0,10,26);ca.lookAt(0,8,-6);
+ clock=new THREE.Clock();
+ for(var r=0;r<5;r++){
+ var geo=new THREE.PlaneGeometry(100,7,SEG,1);
+ var mat=new THREE.MeshBasicMaterial({
+ color:cols[r],transparent:true,opacity:0.26+r*0.02,
+ side:THREE.DoubleSide,blending:THREE.AdditiveBlending,depthWrite:false
+ });
+ var mesh=new THREE.Mesh(geo,mat);
+ mesh.position.set(0,yPos[r],zPos[r]);
+ sc.add(mesh);
+ ribbons.push({mesh:mesh,geo:geo,freq:0.55+0.12*r,phase:r*1.15,amp:2.4+0.2*r});
+ }
+ function loop(){
+ raf=requestAnimationFrame(loop);
+ var t=clock.getElapsedTime();
+ for(var i=0;i<ribbons.length;i++){
+ var rb=ribbons[i],pos=rb.geo.attributes.position;
+ for(var v=0;v<pos.count;v++){
+ if(pos.getY(v)>0){
+ var x=pos.getX(v);
+ pos.setY(v,rb.amp*Math.sin(t*rb.freq+x*0.065+rb.phase)
+ +rb.amp*0.38*Math.cos(t*rb.freq*0.72+x*0.042));
+ }
+ }
+ pos.needsUpdate=true;
+ }
+ ren.render(sc,ca);
+ }
+ sz();window.addEventListener('resize',sz);
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ // Aurora WebGL: six wide ribbon meshes whose top-row vertices are animated
+ // with overlapping sine waves, rendered with additive blending so they glow
+ // against the dark navy sky like real aurora curtains.
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var RIBBON_COUNT = 6;
+ var SEG_W = 60; // horizontal segments per ribbon
+ var ribbonColors = [0x00ffb3, 0x00cfe8, 0xc084fc, 0x00ffb3, 0x48e8d0, 0xa855f7];
+ var ribbonY = [-10, -4, 2, 8, 14, 20];
+ var ribbonZ = [-40, -30, -22, -15, -10, -5];
+ var ribbonFreq = [0.6, 0.9, 0.7, 1.1, 0.5, 0.8];
+ var ribbonPhase = [0.0, 1.2, 2.4, 0.8, 3.1, 1.7];
+ var ribbonAmp = [3.0, 2.5, 2.0, 3.5, 2.2, 2.8];
+
+ var scene, camera, renderer, clock;
+ var ribbons = [];
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x050d1a);
+ scene.fog = new THREE.Fog(0x050d1a, 40, 120);
+
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 5, 30);
+ camera.lookAt(0, 5, 0);
+
+ 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();
+
+ for (var r = 0; r < RIBBON_COUNT; r++) {
+ // Wide shallow plane; we animate the top row of vertices
+ var geo = new THREE.PlaneGeometry(120, 8, SEG_W, 1);
+ var mat = new THREE.MeshBasicMaterial({
+ color: ribbonColors[r], transparent: true, opacity: 0.32,
+ side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false
+ });
+ var mesh = new THREE.Mesh(geo, mat);
+ mesh.position.set(0, ribbonY[r], ribbonZ[r]);
+ scene.add(mesh);
+ ribbons.push({ mesh: mesh, geo: geo, freq: ribbonFreq[r],
+ phase: ribbonPhase[r], amp: ribbonAmp[r] });
+ }
+
+ 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();
+ // Wild mode: accelerate time 18× so waves churn much faster
+ _snoTOffset += (realT - _snoLastT) * (_wild ? 18 : 0);
+ _snoLastT = realT;
+ var t = realT + _snoTOffset;
+
+ var ampMult = _wild ? 3.2 : 1;
+ // Camera sways left/right and bobs up/down in wild mode
+ camera.position.x = _wild ? Math.sin(t * 0.28) * 10 : 0;
+ camera.position.y = _wild ? 5 + Math.cos(t * 0.19) * 4 : 5;
+
+ for (var r = 0; r < ribbons.length; r++) {
+ var rb = ribbons[r];
+ var pos = rb.geo.attributes.position;
+ var count = pos.count;
+ // In wild mode ribbons also drift vertically so they cross and tangle
+ var yDrift = _wild ? Math.sin(t * rb.freq * 0.4 + r * 1.1) * 6 : 0;
+ rb.mesh.position.y = ribbonY[r] + yDrift;
+ // PlaneGeometry vertices: (SEG_W+1)*2 total; top row is every other vertex
+ for (var i = 0; i < count; i++) {
+ var x = pos.getX(i);
+ // Only animate top row (y > 0 in local space) for the waving top edge
+ if (pos.getY(i) > 0) {
+ pos.setY(i, rb.amp * ampMult * Math.sin(t * rb.freq + x * 0.08 + rb.phase)
+ + rb.amp * ampMult * 0.4 * Math.cos(t * rb.freq * 0.7 + x * 0.05));
+ }
+ }
+ pos.needsUpdate = true;
+ }
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ // Aurora nav/wild effects — snow burst on navigate, blizzard storm on wild
+ 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'); }, 400); }
+ // Frost shimmer — aurora-colored radial
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:997;pointer-events:none;background:radial-gradient(ellipse at center,rgba(0,255,179,0.14) 0%,rgba(192,132,252,0.1) 55%,transparent 80%);transition:opacity 0.3s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 340); }, 15);
+ };
+ window.snonuxCloseEffect = function() {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,207,232,0.12);transition:opacity 0.2s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 15);
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,transparent,rgba(0,207,232,0.9),rgba(120,200,100,0.9),rgba(0,207,232,0.9),transparent);' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.32s ease,opacity 0.32s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? '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);
+ };
+ window.snonuxNavEffect = function() {
+ // Snow burst — CSS snowflakes scatter from cursor
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 380); }
+ // Frost flash
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:radial-gradient(ellipse at center,rgba(0,255,179,0.18) 0%,rgba(192,132,252,0.1) 60%,transparent 100%);transition:opacity 0.22s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 250); }, 30);
+ };
+ window.snonuxPageEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
+ };
+ })();
diff --git a/internal/generator/templates/themes/biomech.tmpl b/internal/generator/templates/themes/biomech.tmpl
deleted file mode 100644
index 9773d96..0000000
--- a/internal/generator/templates/themes/biomech.tmpl
+++ /dev/null
@@ -1,217 +0,0 @@
-<!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/biomech/meta.json b/internal/generator/templates/themes/biomech/meta.json
new file mode 100644
index 0000000..0ba749c
--- /dev/null
+++ b/internal/generator/templates/themes/biomech/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // BIOMECH",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eAnatomy\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-pod\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eContainment Membrane\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to breach the shell\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/biomech/theme.css b/internal/generator/templates/themes/biomech/theme.css
new file mode 100644
index 0000000..d43ee0b
--- /dev/null
+++ b/internal/generator/templates/themes/biomech/theme.css
@@ -0,0 +1,52 @@
+ :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;} }
+ [data-sno-theme="biomech"] .splash-overlay {
+ 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%);
+ }
+ [data-sno-theme="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; }
+ [data-sno-theme="biomech"] .splash-title { font-size:clamp(1.55rem,5vw,2.1rem); color:var(--bone); letter-spacing:0.12em; }
+ [data-sno-theme="biomech"] .splash-tag { color:var(--acid); letter-spacing:0.22em; }
+ [data-sno-theme="biomech"] .splash-hint { color:rgba(208,199,187,0.78); }
+ [data-sno-theme="biomech"] .splash-inner { text-shadow:0 2px 22px rgba(0,0,0,0.95); }
diff --git a/internal/generator/templates/themes/biomech/theme.js b/internal/generator/templates/themes/biomech/theme.js
new file mode 100644
index 0000000..5e4d3e1
--- /dev/null
+++ b/internal/generator/templates/themes/biomech/theme.js
@@ -0,0 +1,98 @@
+
+ (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);
+ })();
+
+
+ (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); };
+ })();
diff --git a/internal/generator/templates/themes/brutalist.tmpl b/internal/generator/templates/themes/brutalist.tmpl
deleted file mode 100644
index 9a02b25..0000000
--- a/internal/generator/templates/themes/brutalist.tmpl
+++ /dev/null
@@ -1,260 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SNONUX.FOO</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --red:#ff2200; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:Impact,'Arial Narrow',Arial,sans-serif;
- background:#000; color:#fff; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { height:100vh; display:flex; flex-direction:column; position:relative; z-index:10; }
- header { padding:14px 24px; background:#000; border-bottom:4px solid #fff;
- display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:16px; }
- .logo-mark { font-size:2.8rem; color:var(--red); line-height:1; }
- .logo-title h1 { font-size:2rem; color:#fff; letter-spacing:0; line-height:1; }
- .logo-title .subtitle { font-size:0.78rem; color:#888; margin-top:3px;
- font-family:'Courier New',monospace; }
- .logo-title .subtitle a { color:var(--red); text-decoration:none; }
- .logo-title .subtitle a:hover { text-decoration:underline; }
- .transmit-btn { border:3px solid var(--red); color:var(--red); padding:10px 20px;
- border-radius:0; text-decoration:none; font-family:Impact; font-size:1.05rem;
- letter-spacing:2px; transition:all 0.1s; }
- .transmit-btn:hover { background:var(--red); color:#000; }
- a.header-feed-link { color:#aaa; font-family:'Courier New',monospace; font-size:0.78rem; }
- a.header-feed-link:hover { color:var(--red); }
- .nav-hints { background:#111; border-bottom:2px solid #333; color:#888;
- padding:5px 24px; display:flex; gap:18px; font-family:'Courier New',monospace;
- font-size:0.7rem; flex-wrap:wrap; }
- .nav-hints kbd { background:#000; border:1px solid #555; color:#fff;
- border-radius:0; padding:0 5px; margin:0 2px; font-size:0.7rem; }
- .content { flex:1; overflow-y:auto; padding:20px 24px;
- scrollbar-width:thin; scrollbar-color:#fff #000; }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:3px solid #fff; color:#fff; padding:9px 22px;
- border-radius:0; text-decoration:none; font-family:Impact;
- font-size:1rem; letter-spacing:2px; }
- .page-nav a:hover { background:#fff; color:#000; }
- .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
- background:#000; border-top:4px solid #fff; }
- .post { background:#000; border:3px solid #fff; border-radius:0;
- padding:20px 22px; margin-bottom:14px; cursor:pointer;
- transition:border-color 0.1s,background 0.1s; }
- .post:hover { border-color:var(--red); }
- .post-active { border-color:var(--red) !important; background:#0d0000 !important;
- border-left-width:8px !important; box-shadow:none !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; }
- .post-time { color:#aaa; font-family:'Courier New',monospace; font-size:0.82rem; }
- .post-text { font-family:'Arial',sans-serif; font-size:1rem; line-height:1.5; }
- .post-text a { color:var(--red); text-decoration:underline; }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(0,0,0,0.98); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:780px; margin:0 auto; background:#000;
- border:4px solid #fff; border-radius:0; padding:38px;
- box-shadow:8px 8px 0 var(--red); }
- .modal-close { float:right; background:none; border:none; color:var(--red);
- font-family:Impact; font-size:1.3rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .logo-mark{font-size:2rem;} }
- .splash-overlay.splash-brutalist { background:#000; }
- .splash-brutalist .splash-frame {
- border:6px solid #fff; padding:clamp(1.5rem,5vw,2.5rem) clamp(1.25rem,4vw,2rem);
- box-shadow: 12px 12px 0 var(--red); animation: splashBrutalJolt 3s steps(2,end) infinite;
- }
- @keyframes splashBrutalJolt { 0%,100% { transform: translate(0,0); } 50% { transform: translate(2px,-2px); } }
- .splash-brutalist .splash-title { font-family:Impact,sans-serif; font-size:clamp(1.8rem,6vw,2.8rem); color:#fff; }
- .splash-brutalist .splash-tag { font-family:'Courier New',monospace; color:var(--red); }
- .splash-brutalist .splash-hint { font-family:'Courier New',monospace; color:#c8c8c8; }
- .splash-brutalist .splash-inner { text-shadow: 0 0 12px #000, 0 2px 8px #000; }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-brutalist" 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-inner splash-frame">
- <div class="splash-title">SNONUX.FOO</div>
- <div class="splash-tag">Brutalist theme</div>
- <div class="splash-hint">[ CLICK OR ENTER TO TRANSMIT ]</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,g=new THREE.Group(),t0=performance.now();
- 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.z=8;
- var b1=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(3.4,2.4,2.4)),new THREE.LineBasicMaterial({color:0xffffff}));
- var b2=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2.2,1.6,1.6)),new THREE.LineBasicMaterial({color:0xff2200}));
- b2.position.set(0.3,0.2,0.5);g.add(b1);g.add(b2);sc.add(g);sz();window.addEventListener('resize',sz);
- function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.x=t*0.51;g.rotation.y=t*0.73;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">TRANSMIT</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>
- // Brutalist WebGL: harsh slowly-rotating boxes — solid white and wireframe red.
- // No fog, no softness. Pure geometric violence against the black void.
- (function() {
- var _wild = false;
- var scene, camera, renderer, clock;
- var boxes = [];
-
- function initThree() {
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x000000);
-
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
- camera.position.set(0, 0, 40);
-
- renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- clock = new THREE.Clock();
-
- // Box configurations: [size, posX, posY, posZ, rotSpeedX, rotSpeedY, wireframe, color]
- var configs = [
- [10, 0, 0, 0, 0.002, 0.005, false, 0xffffff],
- [6, 18, -6, -8, 0.004, 0.003, true, 0xff2200],
- [7, -16, 5, -10, 0.003, 0.006, true, 0xff2200],
- [5, 8, 12, -5, 0.006, 0.002, false, 0xff2200],
- [4, -10,-10, -3, 0.005, 0.004, false, 0xffffff],
- ];
-
- configs.forEach(function(c) {
- var geo = new THREE.BoxGeometry(c[0], c[0], c[0]);
- var mat = new THREE.MeshBasicMaterial({ color: c[7], wireframe: c[6] });
- var mesh = new THREE.Mesh(geo, mat);
- mesh.position.set(c[1], c[2], c[3]);
- boxes.push({ mesh: mesh, rx: c[4], ry: c[5] });
- scene.add(mesh);
- });
-
- 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 sm = _wild ? 15 : 1;
- boxes.forEach(function(b) {
- b.mesh.rotation.x += b.rx * sm;
- b.mesh.rotation.y += b.ry * sm;
- // Wild mode: random jitter on positions
- if (_wild) { b.mesh.position.x += (Math.random()-0.5)*0.4; b.mesh.position.y += (Math.random()-0.5)*0.4; }
- });
- renderer.render(scene, camera);
- }
-
- initThree();
-
- // Brutalist nav/wild effects — violent shake on navigate, geometric chaos on wild
- window.snonuxOpenEffect = function() {
- // Expand violently from nothing — pure brutalist impact
- var modal = document.getElementById('post-modal');
- if (modal) { modal.classList.add('sno-modal-expand'); setTimeout(function() { modal.classList.remove('sno-modal-expand'); }, 420); }
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:997;pointer-events:none;background:rgba(255,34,0,0.22);transition:opacity 0.14s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 170); }, 15);
- };
- window.snonuxCloseEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 360); }
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- // Brutalist: harsh black-and-white hard edge
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,rgba(0,0,0,0.95),rgba(255,255,255,0.95),rgba(0,0,0,0.95));' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.25s ease,opacity 0.25s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
- setTimeout(function() { d.remove(); }, 320);
- };
- window.snonuxWildToggle = function() {
- _wild = !_wild;
- var b = document.getElementById('sno-wild-badge');
- if (b) b.classList.toggle('sno-wild-on', _wild);
- };
- window.snonuxNavEffect = function() {
- // Violent double shake + red flash
- var ov = document.querySelector('.overlay');
- if (ov) {
- ov.classList.add('sno-fx-shake');
- setTimeout(function() { ov.classList.remove('sno-fx-shake'); setTimeout(function() { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 380); }, 50); }, 400);
- }
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(255,34,0,0.28);transition:opacity 0.18s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 25);
- };
- window.snonuxPageEffect = function() {
- // Color inversion flash
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:#fff;mix-blend-mode:difference;transition:opacity 0.15s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 180); }, 20);
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/brutalist/meta.json b/internal/generator/templates/themes/brutalist/meta.json
new file mode 100644
index 0000000..b6dee75
--- /dev/null
+++ b/internal/generator/templates/themes/brutalist/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "SNONUX.FOO",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003eSNONUX.FOO\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003eMICROBLOG \u0026mdash; \u003ca href=\"https://foo.zone\"\u003eFOO.ZONE\u003c/a\u003e IS THE REAL BLOG\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner splash-frame\"\u003e\n \u003cdiv class=\"splash-title\"\u003eSNONUX.FOO\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eBrutalist theme\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003e[ CLICK OR ENTER TO TRANSMIT ]\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; NEWER",
+ "next_page_text": "OLDER \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/brutalist/theme.css b/internal/generator/templates/themes/brutalist/theme.css
new file mode 100644
index 0000000..4c8dac5
--- /dev/null
+++ b/internal/generator/templates/themes/brutalist/theme.css
@@ -0,0 +1,65 @@
+ :root { --red:#ff2200; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:Impact,'Arial Narrow',Arial,sans-serif;
+ background:#000; color:#fff; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { height:100vh; display:flex; flex-direction:column; position:relative; z-index:10; }
+ header { padding:14px 24px; background:#000; border-bottom:4px solid #fff;
+ display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:16px; }
+ .logo-mark { font-size:2.8rem; color:var(--red); line-height:1; }
+ .logo-title h1 { font-size:2rem; color:#fff; letter-spacing:0; line-height:1; }
+ .logo-title .subtitle { font-size:0.78rem; color:#888; margin-top:3px;
+ font-family:'Courier New',monospace; }
+ .logo-title .subtitle a { color:var(--red); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-decoration:underline; }
+ .transmit-btn { border:3px solid var(--red); color:var(--red); padding:10px 20px;
+ border-radius:0; text-decoration:none; font-family:Impact; font-size:1.05rem;
+ letter-spacing:2px; transition:all 0.1s; }
+ .transmit-btn:hover { background:var(--red); color:#000; }
+ a.header-feed-link { color:#aaa; font-family:'Courier New',monospace; font-size:0.78rem; }
+ a.header-feed-link:hover { color:var(--red); }
+ .nav-hints { background:#111; border-bottom:2px solid #333; color:#888;
+ padding:5px 24px; display:flex; gap:18px; font-family:'Courier New',monospace;
+ font-size:0.7rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:#000; border:1px solid #555; color:#fff;
+ border-radius:0; padding:0 5px; margin:0 2px; font-size:0.7rem; }
+ .content { flex:1; overflow-y:auto; padding:20px 24px;
+ scrollbar-width:thin; scrollbar-color:#fff #000; }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:3px solid #fff; color:#fff; padding:9px 22px;
+ border-radius:0; text-decoration:none; font-family:Impact;
+ font-size:1rem; letter-spacing:2px; }
+ .page-nav a:hover { background:#fff; color:#000; }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:#000; border-top:4px solid #fff; }
+ .post { background:#000; border:3px solid #fff; border-radius:0;
+ padding:20px 22px; margin-bottom:14px; cursor:pointer;
+ transition:border-color 0.1s,background 0.1s; }
+ .post:hover { border-color:var(--red); }
+ .post-active { border-color:var(--red) !important; background:#0d0000 !important;
+ border-left-width:8px !important; box-shadow:none !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; }
+ .post-time { color:#aaa; font-family:'Courier New',monospace; font-size:0.82rem; }
+ .post-text { font-family:'Arial',sans-serif; font-size:1rem; line-height:1.5; }
+ .post-text a { color:var(--red); text-decoration:underline; }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(0,0,0,0.98); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:780px; margin:0 auto; background:#000;
+ border:4px solid #fff; border-radius:0; padding:38px;
+ box-shadow:8px 8px 0 var(--red); }
+ .modal-close { float:right; background:none; border:none; color:var(--red);
+ font-family:Impact; font-size:1.3rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .logo-mark{font-size:2rem;} }
+ [data-sno-theme="brutalist"] .splash-overlay { background:#000; }
+ [data-sno-theme="brutalist"] .splash-frame {
+ border:6px solid #fff; padding:clamp(1.5rem,5vw,2.5rem) clamp(1.25rem,4vw,2rem);
+ box-shadow: 12px 12px 0 var(--red); animation: splashBrutalJolt 3s steps(2,end) infinite;
+ }
+ @keyframes splashBrutalJolt { 0%,100% { transform: translate(0,0); } 50% { transform: translate(2px,-2px); } }
+ [data-sno-theme="brutalist"] .splash-title { font-family:Impact,sans-serif; font-size:clamp(1.8rem,6vw,2.8rem); color:#fff; }
+ [data-sno-theme="brutalist"] .splash-tag { font-family:'Courier New',monospace; color:var(--red); }
+ [data-sno-theme="brutalist"] .splash-hint { font-family:'Courier New',monospace; color:#c8c8c8; }
+ [data-sno-theme="brutalist"] .splash-inner { text-shadow: 0 0 12px #000, 0 2px 8px #000; }
diff --git a/internal/generator/templates/themes/brutalist/theme.js b/internal/generator/templates/themes/brutalist/theme.js
new file mode 100644
index 0000000..227763a
--- /dev/null
+++ b/internal/generator/templates/themes/brutalist/theme.js
@@ -0,0 +1,132 @@
+
+ (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,g=new THREE.Group(),t0=performance.now();
+ 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.z=8;
+ var b1=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(3.4,2.4,2.4)),new THREE.LineBasicMaterial({color:0xffffff}));
+ var b2=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2.2,1.6,1.6)),new THREE.LineBasicMaterial({color:0xff2200}));
+ b2.position.set(0.3,0.2,0.5);g.add(b1);g.add(b2);sc.add(g);sz();window.addEventListener('resize',sz);
+ function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.x=t*0.51;g.rotation.y=t*0.73;ren.render(sc,ca);}
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ // Brutalist WebGL: harsh slowly-rotating boxes — solid white and wireframe red.
+ // No fog, no softness. Pure geometric violence against the black void.
+ (function() {
+ var _wild = false;
+ var scene, camera, renderer, clock;
+ var boxes = [];
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x000000);
+
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 0, 40);
+
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ clock = new THREE.Clock();
+
+ // Box configurations: [size, posX, posY, posZ, rotSpeedX, rotSpeedY, wireframe, color]
+ var configs = [
+ [10, 0, 0, 0, 0.002, 0.005, false, 0xffffff],
+ [6, 18, -6, -8, 0.004, 0.003, true, 0xff2200],
+ [7, -16, 5, -10, 0.003, 0.006, true, 0xff2200],
+ [5, 8, 12, -5, 0.006, 0.002, false, 0xff2200],
+ [4, -10,-10, -3, 0.005, 0.004, false, 0xffffff],
+ ];
+
+ configs.forEach(function(c) {
+ var geo = new THREE.BoxGeometry(c[0], c[0], c[0]);
+ var mat = new THREE.MeshBasicMaterial({ color: c[7], wireframe: c[6] });
+ var mesh = new THREE.Mesh(geo, mat);
+ mesh.position.set(c[1], c[2], c[3]);
+ boxes.push({ mesh: mesh, rx: c[4], ry: c[5] });
+ scene.add(mesh);
+ });
+
+ 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 sm = _wild ? 15 : 1;
+ boxes.forEach(function(b) {
+ b.mesh.rotation.x += b.rx * sm;
+ b.mesh.rotation.y += b.ry * sm;
+ // Wild mode: random jitter on positions
+ if (_wild) { b.mesh.position.x += (Math.random()-0.5)*0.4; b.mesh.position.y += (Math.random()-0.5)*0.4; }
+ });
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ // Brutalist nav/wild effects — violent shake on navigate, geometric chaos on wild
+ window.snonuxOpenEffect = function() {
+ // Expand violently from nothing — pure brutalist impact
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-expand'); setTimeout(function() { modal.classList.remove('sno-modal-expand'); }, 420); }
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:997;pointer-events:none;background:rgba(255,34,0,0.22);transition:opacity 0.14s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 170); }, 15);
+ };
+ window.snonuxCloseEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 360); }
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ // Brutalist: harsh black-and-white hard edge
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,rgba(0,0,0,0.95),rgba(255,255,255,0.95),rgba(0,0,0,0.95));' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.25s ease,opacity 0.25s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 320);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ };
+ window.snonuxNavEffect = function() {
+ // Violent double shake + red flash
+ var ov = document.querySelector('.overlay');
+ if (ov) {
+ ov.classList.add('sno-fx-shake');
+ setTimeout(function() { ov.classList.remove('sno-fx-shake'); setTimeout(function() { ov.classList.add('sno-fx-shake'); setTimeout(function() { ov.classList.remove('sno-fx-shake'); }, 380); }, 50); }, 400);
+ }
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(255,34,0,0.28);transition:opacity 0.18s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 25);
+ };
+ window.snonuxPageEffect = function() {
+ // Color inversion flash
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:#fff;mix-blend-mode:difference;transition:opacity 0.15s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 180); }, 20);
+ };
+ })();
diff --git a/internal/generator/templates/themes/cathedral/meta.json b/internal/generator/templates/themes/cathedral/meta.json
new file mode 100644
index 0000000..f4a81db
--- /dev/null
+++ b/internal/generator/templates/themes/cathedral/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // CATHEDRAL",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eReliquary\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-rose\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-pipes\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-incense\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eRitual Engine\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to cross the nave\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/cathedral/theme.css b/internal/generator/templates/themes/cathedral/theme.css
new file mode 100644
index 0000000..5dc6456
--- /dev/null
+++ b/internal/generator/templates/themes/cathedral/theme.css
@@ -0,0 +1,79 @@
+ :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;} }
+ [data-sno-theme="cathedral"] .splash-overlay {
+ background:
+ radial-gradient(circle at 50% 18%, rgba(224,196,127,0.18) 0%, transparent 30%),
+ linear-gradient(180deg, #17111b 0%, #09080d 100%);
+ }
+ [data-sno-theme="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); } }
+ [data-sno-theme="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; }
+ [data-sno-theme="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); } }
+ [data-sno-theme="cathedral"] .splash-title { font-family:'Cinzel',serif; font-size:clamp(1.7rem,5vw,2.5rem); color:#fff4d2; letter-spacing:0.08em; }
+ [data-sno-theme="cathedral"] .splash-tag { color:var(--gold); letter-spacing:0.26em; }
+ [data-sno-theme="cathedral"] .splash-hint { color:rgba(240,232,217,0.82); }
+ [data-sno-theme="cathedral"] .splash-inner { text-shadow:0 2px 28px rgba(0,0,0,0.94); }
diff --git a/internal/generator/templates/themes/cathedral.tmpl b/internal/generator/templates/themes/cathedral/theme.js
index fb519ce..6247200 100644
--- a/internal/generator/templates/themes/cathedral.tmpl
+++ b/internal/generator/templates/themes/cathedral/theme.js
@@ -1,110 +1,4 @@
-<!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');
@@ -129,46 +23,8 @@
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 = [];
@@ -297,7 +153,3 @@
if (b) b.classList.toggle('sno-wild-on', _wild);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/cosmos/meta.json b/internal/generator/templates/themes/cosmos/meta.json
new file mode 100644
index 0000000..7f7b880
--- /dev/null
+++ b/internal/generator/templates/themes/cosmos/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ✧ COSMOS",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-stars\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-orbit\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eCosmos gate\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eEngage — click or Enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/cosmos/theme.css b/internal/generator/templates/themes/cosmos/theme.css
new file mode 100644
index 0000000..eff1267
--- /dev/null
+++ b/internal/generator/templates/themes/cosmos/theme.css
@@ -0,0 +1,81 @@
+ :root { --gold:#ffd166; --purple:#9b5de5; --blue:#4cc9f0; --bg:#020214; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
+ color:#d4e8ff; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(2,2,20,0.78); backdrop-filter:blur(14px);
+ border-bottom:1px solid rgba(255,209,102,0.2); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800;
+ background:linear-gradient(90deg,var(--gold),var(--purple));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:#d4e8ff; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(212,232,255,0.5); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--gold); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--gold); }
+ .transmit-btn { border:1px solid var(--gold); color:var(--gold); padding:9px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--gold); color:var(--bg); }
+ a.header-feed-link { color:var(--blue); }
+ a.header-feed-link:hover { color:var(--gold); }
+ .nav-hints { background:rgba(2,2,20,0.6); border-bottom:1px solid rgba(255,209,102,0.12);
+ color:rgba(212,232,255,0.4); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(255,209,102,0.1); border:1px solid rgba(255,209,102,0.3);
+ color:var(--gold); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--purple) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--purple); color:var(--purple); padding:8px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--purple); color:#fff; }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(2,2,20,0.78); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(255,209,102,0.2); }
+ .post { background:rgba(5,5,30,0.72); border:1px solid rgba(155,93,229,0.22); border-radius:10px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(6px); }
+ .post:hover { border-color:var(--gold); box-shadow:0 0 22px rgba(255,209,102,0.18); transform:translateY(-2px); }
+ .post-active { border-color:var(--gold) !important; background:rgba(10,5,35,0.9) !important;
+ box-shadow:0 0 28px rgba(255,209,102,0.35),inset 3px 0 0 var(--gold) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--blue); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--blue); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--blue); }
+ .post-audio { width:100%; margin-top:10px; }
+ .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(5,5,30,0.92);
+ border:1px solid var(--gold); border-radius:12px;
+ box-shadow:0 0 60px rgba(255,209,102,0.25); padding:40px; backdrop-filter:blur(16px); }
+ .modal-close { float:right; background:none; border:none; color:var(--gold);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ [data-sno-theme="cosmos"] .splash-overlay { background: radial-gradient(ellipse 100% 80% at 50% 100%, rgba(155,93,229,0.2) 0%, transparent 55%), var(--bg); }
+ [data-sno-theme="cosmos"] .splash-stars {
+ position:absolute; inset:0; pointer-events:none; opacity:0.5;
+ background-image: radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.9), transparent),
+ radial-gradient(1px 1px at 80% 20%, rgba(255,209,102,0.8), transparent),
+ radial-gradient(1px 1px at 40% 70%, rgba(76,201,240,0.7), transparent),
+ radial-gradient(1px 1px at 65% 55%, rgba(255,255,255,0.6), transparent);
+ background-size: 100% 100%;
+ animation: splashTwinkle 4s ease-in-out infinite alternate;
+ }
+ @keyframes splashTwinkle { from { opacity:0.35; } to { opacity:0.65; } }
+ [data-sno-theme="cosmos"] .splash-inner { position:relative; z-index:1; }
+ [data-sno-theme="cosmos"] .splash-orbit {
+ width:72px; height:72px; margin:0 auto 1rem; border:2px solid rgba(255,209,102,0.5);
+ border-radius:50%; animation: splashOrbitSpin 12s linear infinite;
+ box-shadow: 0 0 30px rgba(155,93,229,0.4);
+ }
+ @keyframes splashOrbitSpin { to { transform: rotate(360deg); } }
+ [data-sno-theme="cosmos"] .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#d4e8ff; }
+ [data-sno-theme="cosmos"] .splash-tag {
+ background:linear-gradient(90deg,var(--gold),var(--purple));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
+ [data-sno-theme="cosmos"] .splash-hint { color:rgba(212,232,255,0.88); }
+ [data-sno-theme="cosmos"] .splash-stars { z-index:1; }
+ [data-sno-theme="cosmos"] .splash-inner { text-shadow: 0 2px 20px rgba(0,0,0,0.85); }
diff --git a/internal/generator/templates/themes/cosmos.tmpl b/internal/generator/templates/themes/cosmos/theme.js
index 594796c..be51bd7 100644
--- a/internal/generator/templates/themes/cosmos.tmpl
+++ b/internal/generator/templates/themes/cosmos/theme.js
@@ -1,108 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ✧ COSMOS</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --gold:#ffd166; --purple:#9b5de5; --blue:#4cc9f0; --bg:#020214; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
- color:#d4e8ff; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(2,2,20,0.78); backdrop-filter:blur(14px);
- border-bottom:1px solid rgba(255,209,102,0.2); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800;
- background:linear-gradient(90deg,var(--gold),var(--purple));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:#d4e8ff; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(212,232,255,0.5); margin-top:2px; }
- .logo-title .subtitle a { color:var(--gold); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--gold); }
- .transmit-btn { border:1px solid var(--gold); color:var(--gold); padding:9px 20px;
- border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--gold); color:var(--bg); }
- a.header-feed-link { color:var(--blue); }
- a.header-feed-link:hover { color:var(--gold); }
- .nav-hints { background:rgba(2,2,20,0.6); border-bottom:1px solid rgba(255,209,102,0.12);
- color:rgba(212,232,255,0.4); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(255,209,102,0.1); border:1px solid rgba(255,209,102,0.3);
- color:var(--gold); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--purple) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--purple); color:var(--purple); padding:8px 20px;
- border-radius:20px; text-decoration:none; font-size:0.82rem; }
- .page-nav a:hover { background:var(--purple); color:#fff; }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(2,2,20,0.78); backdrop-filter:blur(14px);
- border-top:1px solid rgba(255,209,102,0.2); }
- .post { background:rgba(5,5,30,0.72); border:1px solid rgba(155,93,229,0.22); border-radius:10px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(6px); }
- .post:hover { border-color:var(--gold); box-shadow:0 0 22px rgba(255,209,102,0.18); transform:translateY(-2px); }
- .post-active { border-color:var(--gold) !important; background:rgba(10,5,35,0.9) !important;
- box-shadow:0 0 28px rgba(255,209,102,0.35),inset 3px 0 0 var(--gold) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--blue); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--blue); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--blue); }
- .post-audio { width:100%; margin-top:10px; }
- .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(5,5,30,0.92);
- border:1px solid var(--gold); border-radius:12px;
- box-shadow:0 0 60px rgba(255,209,102,0.25); padding:40px; backdrop-filter:blur(16px); }
- .modal-close { float:right; background:none; border:none; color:var(--gold);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- .splash-overlay.splash-cosmos { background: radial-gradient(ellipse 100% 80% at 50% 100%, rgba(155,93,229,0.2) 0%, transparent 55%), var(--bg); }
- .splash-cosmos .splash-stars {
- position:absolute; inset:0; pointer-events:none; opacity:0.5;
- background-image: radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.9), transparent),
- radial-gradient(1px 1px at 80% 20%, rgba(255,209,102,0.8), transparent),
- radial-gradient(1px 1px at 40% 70%, rgba(76,201,240,0.7), transparent),
- radial-gradient(1px 1px at 65% 55%, rgba(255,255,255,0.6), transparent);
- background-size: 100% 100%;
- animation: splashTwinkle 4s ease-in-out infinite alternate;
- }
- @keyframes splashTwinkle { from { opacity:0.35; } to { opacity:0.65; } }
- .splash-cosmos .splash-inner { position:relative; z-index:1; }
- .splash-cosmos .splash-orbit {
- width:72px; height:72px; margin:0 auto 1rem; border:2px solid rgba(255,209,102,0.5);
- border-radius:50%; animation: splashOrbitSpin 12s linear infinite;
- box-shadow: 0 0 30px rgba(155,93,229,0.4);
- }
- @keyframes splashOrbitSpin { to { transform: rotate(360deg); } }
- .splash-cosmos .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#d4e8ff; }
- .splash-cosmos .splash-tag {
- background:linear-gradient(90deg,var(--gold),var(--purple));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
- .splash-cosmos .splash-hint { color:rgba(212,232,255,0.88); }
- .splash-cosmos .splash-stars { z-index:1; }
- .splash-cosmos .splash-inner { text-shadow: 0 2px 20px rgba(0,0,0,0.85); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-cosmos" 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-stars" aria-hidden="true"></div>
- <div class="splash-inner">
- <div class="splash-orbit" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Cosmos gate</div>
- <div class="splash-hint">Engage — click or Enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -121,46 +17,8 @@
moon.position.x=2.6*Math.cos(t*0.7);moon.position.z=2.6*Math.sin(t*0.7);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">Transmit</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>
+
+
// Cosmos WebGL: ringed planet, swirling nebula blobs, asteroid belt, and stars.
// The planet sits at lower-right and slowly rotates; asteroids orbit it;
// nebula clouds drift with additive blending for a deep-space glow.
@@ -379,7 +237,3 @@
setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 20);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/dos.tmpl b/internal/generator/templates/themes/dos.tmpl
deleted file mode 100644
index 622978c..0000000
--- a/internal/generator/templates/themes/dos.tmpl
+++ /dev/null
@@ -1,302 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SNONUX.FOO - DOS</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=VT323&display=swap" rel="stylesheet">
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --dos-blue:#0000aa; --dos-lblue:#5555ff; --dos-white:#aaaaaa;
- --dos-bwhite:#ffffff; --dos-yellow:#ffff55; --dos-cyan:#55ffff;
- --dos-red:#ff5555; --dos-bg:#000088; --dos-black:#000000; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'VT323','Courier New',monospace; background:var(--dos-blue);
- color:var(--dos-bwhite); overflow:hidden; height:100vh; font-size:18px; }
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:8px 20px; background:var(--dos-white); color:var(--dos-blue);
- display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:12px; }
- .logo-mark { font-size:1.8rem; color:var(--dos-blue); font-weight:bold; }
- .logo-title h1 { font-size:1.4rem; color:var(--dos-blue); font-weight:normal; letter-spacing:2px; }
- .logo-title .subtitle { font-size:0.85rem; color:var(--dos-blue); margin-top:1px; }
- .logo-title .subtitle a { color:var(--dos-blue); text-decoration:underline; }
- .logo-title .subtitle a:hover { color:var(--dos-black); }
- .transmit-btn { border:2px solid var(--dos-blue); color:var(--dos-blue); padding:4px 14px;
- text-decoration:none; font-size:1rem; letter-spacing:1px;
- transition:all 0.1s; }
- .transmit-btn:hover { background:var(--dos-blue); color:var(--dos-bwhite); }
- a.header-feed-link { color:var(--dos-blue); }
- a.header-feed-link:hover { color:var(--dos-black); }
- .nav-hints { background:var(--dos-blue); border-bottom:2px solid var(--dos-lblue);
- color:var(--dos-cyan); padding:4px 20px; display:flex; gap:16px;
- font-size:0.85rem; flex-wrap:wrap; }
- .nav-hints kbd { background:var(--dos-black); border:1px solid var(--dos-lblue);
- color:var(--dos-yellow); padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:12px 20px;
- scrollbar-width:thin; scrollbar-color:var(--dos-lblue) var(--dos-blue); }
- .page-nav { display:flex; justify-content:center; margin:10px 0; }
- .page-nav a { border:2px solid var(--dos-lblue); color:var(--dos-yellow); padding:6px 18px;
- text-decoration:none; font-size:1rem; letter-spacing:1px; }
- .page-nav a:hover { background:var(--dos-lblue); color:var(--dos-bwhite); }
- .page-nav-footer { flex-shrink:0; padding:6px 20px; display:flex; justify-content:center;
- background:var(--dos-white); color:var(--dos-blue); }
- .page-nav-footer .page-nav a { border-color:var(--dos-blue); color:var(--dos-blue); }
- .page-nav-footer .page-nav a:hover { background:var(--dos-blue); color:var(--dos-bwhite); }
- .post { background:var(--dos-black); border:2px solid var(--dos-lblue);
- padding:12px 14px; margin-bottom:8px; cursor:pointer;
- transition:border-color 0.1s; }
- .post:hover { border-color:var(--dos-yellow);
- box-shadow:0 0 0 1px var(--dos-yellow); }
- .post-active { border-color:var(--dos-yellow) !important;
- background:rgba(0,0,170,0.3) !important;
- box-shadow:0 0 0 2px var(--dos-yellow),inset 3px 0 0 var(--dos-yellow) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:8px; font-size:1rem; }
- .post-header strong { color:var(--dos-yellow); }
- .post-time { color:var(--dos-cyan); font-size:0.95rem; }
- .post-text { line-height:1.5; font-size:1.05rem; }
- .post-text a { color:var(--dos-cyan); text-decoration:underline; }
- .post-text a:hover { color:var(--dos-yellow); }
- .post-image { max-width:100%; margin-top:8px; border:2px solid var(--dos-lblue); }
- .post-audio { width:100%; margin-top:8px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(0,0,0,0.95); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:740px; margin:0 auto; background:var(--dos-black);
- border:2px solid var(--dos-yellow); padding:24px;
- box-shadow:0 0 20px rgba(85,85,255,0.4); }
- .modal-close { float:right; background:var(--dos-white); border:2px outset var(--dos-bwhite);
- color:var(--dos-blue); font-family:'VT323','Courier New',monospace;
- font-size:1rem; cursor:pointer; padding:2px 8px; }
- .modal-close:hover { background:var(--dos-blue); color:var(--dos-bwhite);
- border-style:inset; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:6px 12px;} .content{padding:8px 12px;} }
- .splash-overlay.splash-dos { background:var(--dos-blue); font-family:'VT323','Courier New',monospace; }
- .splash-dos .splash-inner { position:relative; z-index:1; }
- .splash-dos .splash-title {
- font-size:clamp(1.4rem,4.5vw,2rem); color:var(--dos-bwhite);
- letter-spacing:0.15em;
- animation: splashDosBlink 1s step-end infinite;
- }
- @keyframes splashDosBlink { 0%,100%{border-right:0.6em solid var(--dos-bwhite)} 50%{border-right:0.6em solid transparent} }
- .splash-dos .splash-tag { color:var(--dos-yellow); letter-spacing:0.15em; }
- .splash-dos .splash-hint { color:var(--dos-cyan); }
- .splash-dos .splash-inner {
- background:var(--dos-black); border:2px solid var(--dos-lblue);
- text-shadow:none; box-shadow:4px 4px 0 rgba(0,0,0,0.5);
- }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-dos" 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-inner">
- <div class="splash-title">C:\SNONUX&gt;</div>
- <div class="splash-tag">MS-DOS v6.22</div>
- <div class="splash-hint">Press any key to continue...</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,drops=[],t0=performance.now();
- 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:false,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(1);
- sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(50,1,0.1,80);ca.position.z=20;
- var geo=new THREE.PlaneGeometry(0.22,0.32);
- for(var i=0;i<60;i++){
- var mat=new THREE.MeshBasicMaterial({color:0x55ff55,transparent:true,opacity:0.3+Math.random()*0.4});
- var m=new THREE.Mesh(geo,mat);
- m.position.set((Math.random()-0.5)*28, Math.random()*22-11, (Math.random()-0.5)*5);
- m.userData.speed=0.5+Math.random()*1.5;
- sc.add(m); drops.push(m);
- }
- sz();window.addEventListener('resize',sz);
- function loop(now){raf=requestAnimationFrame(loop);
- for(var i=0;i<drops.length;i++){
- drops[i].position.y-=drops[i].userData.speed*0.06;
- if(drops[i].position.y<-12) drops[i].position.y=12;
- }
- ren.render(sc,ca);}
- raf=requestAnimationFrame(loop);
- })();
- </script>
- <canvas id="three-canvas"></canvas>
- <div class="overlay">
- <header>
- <div class="logo">
- <span class="logo-mark">C:\&gt;</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">ABOUT</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}}">&lt;-- NEWER</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
- (function() {
- var _wild = false, _snoTOffset = 0, _snoLastT = 0;
- var scene, camera, renderer, clock;
- var columns = [];
-
- function initThree() {
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x000088);
-
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
- camera.position.set(0, 0, 40);
-
- renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.setPixelRatio(1);
- clock = new THREE.Clock();
-
- var geo = new THREE.PlaneGeometry(0.35, 0.5);
-
- for (var c = 0; c < 30; c++) {
- var col = [];
- var x = (c - 15) * 2.2;
- var speed = 1.5 + Math.random() * 3;
- var startY = Math.random() * 60 - 30;
- for (var r = 0; r < 8; r++) {
- var brightness = 1.0 - (r / 8) * 0.7;
- var color = new THREE.Color(brightness * 0.33, brightness, brightness * 0.33);
- var mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: brightness * 0.5 });
- var mesh = new THREE.Mesh(geo, mat);
- mesh.position.set(x, startY - r * 0.7, 0);
- scene.add(mesh);
- col.push({ mesh: mesh, offset: r * 0.7 });
- }
- columns.push({ chars: col, x: x, speed: speed, y: startY });
- }
-
- 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 c = 0; c < columns.length; c++) {
- var col = columns[c];
- var y = col.y - t * col.speed;
- y = ((y % 60) + 60) % 60 - 30;
- for (var r = 0; r < col.chars.length; r++) {
- col.chars[r].mesh.position.y = y - col.chars[r].offset;
- // Wild: each char jitters horizontally like corrupted RAM
- if (_wild) { col.chars[r].mesh.position.x = col.x + (Math.random() - 0.5) * 2.5; }
- else { col.chars[r].mesh.position.x = col.x; }
- }
- }
- // Wild: camera lunges forward/back and sways like a CRT meltdown
- if (_wild) {
- camera.position.z = 40 + Math.sin(realT * 0.41) * 14;
- camera.position.x = Math.sin(realT * 0.37) * 8;
- camera.fov = 60 + Math.sin(realT * 0.53) * 16;
- camera.updateProjectionMatrix();
- } else {
- camera.position.z = 40;
- camera.position.x = 0;
- if (camera.fov !== 60) { camera.fov = 60; camera.updateProjectionMatrix(); }
- }
- renderer.render(scene, camera);
- }
-
- initThree();
-
- // DOS nav/wild effects — CRT glitch on navigate, system crash rain on wild
- window.snonuxOpenEffect = function() {
- // Slide in like a dialog box appearing on DOS screen
- var modal = document.getElementById('post-modal');
- if (modal) { modal.classList.add('sno-modal-slide'); setTimeout(function() { modal.classList.remove('sno-modal-slide'); }, 360); }
- // CRT scan flash from top
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;top:0;left:0;right:0;height:4px;z-index:997;pointer-events:none;background:rgba(85,255,255,0.7);box-shadow:0 0 8px rgba(85,255,255,0.5);transition:top 0.28s linear,opacity 0.1s 0.28s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.top='100vh'; setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 120); }, 280); }, 15);
- };
- window.snonuxCloseEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- // DOS/CRT: grey-to-white scanline
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,transparent,rgba(180,180,180,0.9),rgba(255,255,255,0.9),rgba(180,180,180,0.9),transparent);' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.28s ease,opacity 0.28s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
- setTimeout(function() { d.remove(); }, 360);
- };
- window.snonuxWildToggle = function() {
- _wild = !_wild;
- var b = document.getElementById('sno-wild-badge');
- if (b) b.classList.toggle('sno-wild-on', _wild);
- };
- window.snonuxNavEffect = function() {
- // CRT horizontal glitch
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 300); }
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(85,255,85,0.12);transition:opacity 0.15s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 180); }, 25);
- };
- window.snonuxPageEffect = function() {
- // System crash — scanline strobe
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); setTimeout(function() { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }, 40); }, 310); }
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/dos/meta.json b/internal/generator/templates/themes/dos/meta.json
new file mode 100644
index 0000000..fd59dd3
--- /dev/null
+++ b/internal/generator/templates/themes/dos/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "SNONUX.FOO - DOS",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eC:\\\u0026gt;\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003eSNONUX.FOO\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003eMICROBLOG \u0026mdash; \u003ca href=\"https://foo.zone\"\u003eFOO.ZONE\u003c/a\u003e IS THE REAL BLOG\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eABOUT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003eC:\\SNONUX\u0026gt;\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eMS-DOS v6.22\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003ePress any key to continue...\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026lt;-- NEWER",
+ "next_page_text": "OLDER --\u0026gt;"
+}
diff --git a/internal/generator/templates/themes/dos/theme.css b/internal/generator/templates/themes/dos/theme.css
new file mode 100644
index 0000000..ee06438
--- /dev/null
+++ b/internal/generator/templates/themes/dos/theme.css
@@ -0,0 +1,79 @@
+ :root { --dos-blue:#0000aa; --dos-lblue:#5555ff; --dos-white:#aaaaaa;
+ --dos-bwhite:#ffffff; --dos-yellow:#ffff55; --dos-cyan:#55ffff;
+ --dos-red:#ff5555; --dos-bg:#000088; --dos-black:#000000; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'VT323','Courier New',monospace; background:var(--dos-blue);
+ color:var(--dos-bwhite); overflow:hidden; height:100vh; font-size:18px; }
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:8px 20px; background:var(--dos-white); color:var(--dos-blue);
+ display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:12px; }
+ .logo-mark { font-size:1.8rem; color:var(--dos-blue); font-weight:bold; }
+ .logo-title h1 { font-size:1.4rem; color:var(--dos-blue); font-weight:normal; letter-spacing:2px; }
+ .logo-title .subtitle { font-size:0.85rem; color:var(--dos-blue); margin-top:1px; }
+ .logo-title .subtitle a { color:var(--dos-blue); text-decoration:underline; }
+ .logo-title .subtitle a:hover { color:var(--dos-black); }
+ .transmit-btn { border:2px solid var(--dos-blue); color:var(--dos-blue); padding:4px 14px;
+ text-decoration:none; font-size:1rem; letter-spacing:1px;
+ transition:all 0.1s; }
+ .transmit-btn:hover { background:var(--dos-blue); color:var(--dos-bwhite); }
+ a.header-feed-link { color:var(--dos-blue); }
+ a.header-feed-link:hover { color:var(--dos-black); }
+ .nav-hints { background:var(--dos-blue); border-bottom:2px solid var(--dos-lblue);
+ color:var(--dos-cyan); padding:4px 20px; display:flex; gap:16px;
+ font-size:0.85rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:var(--dos-black); border:1px solid var(--dos-lblue);
+ color:var(--dos-yellow); padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:12px 20px;
+ scrollbar-width:thin; scrollbar-color:var(--dos-lblue) var(--dos-blue); }
+ .page-nav { display:flex; justify-content:center; margin:10px 0; }
+ .page-nav a { border:2px solid var(--dos-lblue); color:var(--dos-yellow); padding:6px 18px;
+ text-decoration:none; font-size:1rem; letter-spacing:1px; }
+ .page-nav a:hover { background:var(--dos-lblue); color:var(--dos-bwhite); }
+ .page-nav-footer { flex-shrink:0; padding:6px 20px; display:flex; justify-content:center;
+ background:var(--dos-white); color:var(--dos-blue); }
+ .page-nav-footer .page-nav a { border-color:var(--dos-blue); color:var(--dos-blue); }
+ .page-nav-footer .page-nav a:hover { background:var(--dos-blue); color:var(--dos-bwhite); }
+ .post { background:var(--dos-black); border:2px solid var(--dos-lblue);
+ padding:12px 14px; margin-bottom:8px; cursor:pointer;
+ transition:border-color 0.1s; }
+ .post:hover { border-color:var(--dos-yellow);
+ box-shadow:0 0 0 1px var(--dos-yellow); }
+ .post-active { border-color:var(--dos-yellow) !important;
+ background:rgba(0,0,170,0.3) !important;
+ box-shadow:0 0 0 2px var(--dos-yellow),inset 3px 0 0 var(--dos-yellow) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:8px; font-size:1rem; }
+ .post-header strong { color:var(--dos-yellow); }
+ .post-time { color:var(--dos-cyan); font-size:0.95rem; }
+ .post-text { line-height:1.5; font-size:1.05rem; }
+ .post-text a { color:var(--dos-cyan); text-decoration:underline; }
+ .post-text a:hover { color:var(--dos-yellow); }
+ .post-image { max-width:100%; margin-top:8px; border:2px solid var(--dos-lblue); }
+ .post-audio { width:100%; margin-top:8px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(0,0,0,0.95); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:740px; margin:0 auto; background:var(--dos-black);
+ border:2px solid var(--dos-yellow); padding:24px;
+ box-shadow:0 0 20px rgba(85,85,255,0.4); }
+ .modal-close { float:right; background:var(--dos-white); border:2px outset var(--dos-bwhite);
+ color:var(--dos-blue); font-family:'VT323','Courier New',monospace;
+ font-size:1rem; cursor:pointer; padding:2px 8px; }
+ .modal-close:hover { background:var(--dos-blue); color:var(--dos-bwhite);
+ border-style:inset; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:6px 12px;} .content{padding:8px 12px;} }
+ [data-sno-theme="dos"] .splash-overlay { background:var(--dos-blue); font-family:'VT323','Courier New',monospace; }
+ [data-sno-theme="dos"] .splash-inner { position:relative; z-index:1; }
+ [data-sno-theme="dos"] .splash-title {
+ font-size:clamp(1.4rem,4.5vw,2rem); color:var(--dos-bwhite);
+ letter-spacing:0.15em;
+ animation: splashDosBlink 1s step-end infinite;
+ }
+ @keyframes splashDosBlink { 0%,100%{border-right:0.6em solid var(--dos-bwhite)} 50%{border-right:0.6em solid transparent} }
+ [data-sno-theme="dos"] .splash-tag { color:var(--dos-yellow); letter-spacing:0.15em; }
+ [data-sno-theme="dos"] .splash-hint { color:var(--dos-cyan); }
+ [data-sno-theme="dos"] .splash-inner {
+ background:var(--dos-black); border:2px solid var(--dos-lblue);
+ text-shadow:none; box-shadow:4px 4px 0 rgba(0,0,0,0.5);
+ }
diff --git a/internal/generator/templates/themes/dos/theme.js b/internal/generator/templates/themes/dos/theme.js
new file mode 100644
index 0000000..1d913ac
--- /dev/null
+++ b/internal/generator/templates/themes/dos/theme.js
@@ -0,0 +1,157 @@
+
+ (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,drops=[],t0=performance.now();
+ 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:false,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(1);
+ sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(50,1,0.1,80);ca.position.z=20;
+ var geo=new THREE.PlaneGeometry(0.22,0.32);
+ for(var i=0;i<60;i++){
+ var mat=new THREE.MeshBasicMaterial({color:0x55ff55,transparent:true,opacity:0.3+Math.random()*0.4});
+ var m=new THREE.Mesh(geo,mat);
+ m.position.set((Math.random()-0.5)*28, Math.random()*22-11, (Math.random()-0.5)*5);
+ m.userData.speed=0.5+Math.random()*1.5;
+ sc.add(m); drops.push(m);
+ }
+ sz();window.addEventListener('resize',sz);
+ function loop(now){raf=requestAnimationFrame(loop);
+ for(var i=0;i<drops.length;i++){
+ drops[i].position.y-=drops[i].userData.speed*0.06;
+ if(drops[i].position.y<-12) drops[i].position.y=12;
+ }
+ ren.render(sc,ca);}
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock;
+ var columns = [];
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x000088);
+
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 0, 40);
+
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(1);
+ clock = new THREE.Clock();
+
+ var geo = new THREE.PlaneGeometry(0.35, 0.5);
+
+ for (var c = 0; c < 30; c++) {
+ var col = [];
+ var x = (c - 15) * 2.2;
+ var speed = 1.5 + Math.random() * 3;
+ var startY = Math.random() * 60 - 30;
+ for (var r = 0; r < 8; r++) {
+ var brightness = 1.0 - (r / 8) * 0.7;
+ var color = new THREE.Color(brightness * 0.33, brightness, brightness * 0.33);
+ var mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: brightness * 0.5 });
+ var mesh = new THREE.Mesh(geo, mat);
+ mesh.position.set(x, startY - r * 0.7, 0);
+ scene.add(mesh);
+ col.push({ mesh: mesh, offset: r * 0.7 });
+ }
+ columns.push({ chars: col, x: x, speed: speed, y: startY });
+ }
+
+ 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 c = 0; c < columns.length; c++) {
+ var col = columns[c];
+ var y = col.y - t * col.speed;
+ y = ((y % 60) + 60) % 60 - 30;
+ for (var r = 0; r < col.chars.length; r++) {
+ col.chars[r].mesh.position.y = y - col.chars[r].offset;
+ // Wild: each char jitters horizontally like corrupted RAM
+ if (_wild) { col.chars[r].mesh.position.x = col.x + (Math.random() - 0.5) * 2.5; }
+ else { col.chars[r].mesh.position.x = col.x; }
+ }
+ }
+ // Wild: camera lunges forward/back and sways like a CRT meltdown
+ if (_wild) {
+ camera.position.z = 40 + Math.sin(realT * 0.41) * 14;
+ camera.position.x = Math.sin(realT * 0.37) * 8;
+ camera.fov = 60 + Math.sin(realT * 0.53) * 16;
+ camera.updateProjectionMatrix();
+ } else {
+ camera.position.z = 40;
+ camera.position.x = 0;
+ if (camera.fov !== 60) { camera.fov = 60; camera.updateProjectionMatrix(); }
+ }
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ // DOS nav/wild effects — CRT glitch on navigate, system crash rain on wild
+ window.snonuxOpenEffect = function() {
+ // Slide in like a dialog box appearing on DOS screen
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-slide'); setTimeout(function() { modal.classList.remove('sno-modal-slide'); }, 360); }
+ // CRT scan flash from top
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;top:0;left:0;right:0;height:4px;z-index:997;pointer-events:none;background:rgba(85,255,255,0.7);box-shadow:0 0 8px rgba(85,255,255,0.5);transition:top 0.28s linear,opacity 0.1s 0.28s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.top='100vh'; setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 120); }, 280); }, 15);
+ };
+ window.snonuxCloseEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ // DOS/CRT: grey-to-white scanline
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,transparent,rgba(180,180,180,0.9),rgba(255,255,255,0.9),rgba(180,180,180,0.9),transparent);' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.28s ease,opacity 0.28s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 360);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ };
+ window.snonuxNavEffect = function() {
+ // CRT horizontal glitch
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 300); }
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(85,255,85,0.12);transition:opacity 0.15s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 180); }, 25);
+ };
+ window.snonuxPageEffect = function() {
+ // System crash — scanline strobe
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); setTimeout(function() { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }, 40); }, 310); }
+ };
+ })();
diff --git a/internal/generator/templates/themes/matrix/meta.json b/internal/generator/templates/themes/matrix/meta.json
new file mode 100644
index 0000000..ac07e29
--- /dev/null
+++ b/internal/generator/templates/themes/matrix/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // MATRIX",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003eSNONUX.FOO\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003eMICROBLOG / \u003ca href=\"https://foo.zone\"\u003eFOO.ZONE\u003c/a\u003e IS THE REAL BLOG\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eatom.xml\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-rain\" aria-hidden=\"true\"\u003e01001110 01000101 01001111\n10101010 11001100 00110011\n01110011 01101110 01101111\n11001010 10100101 01011010\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003eSNONUX.FOO\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eFollow the signal\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003ewake up — click or enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026lt;-- NEWER",
+ "next_page_text": "OLDER --\u0026gt;"
+}
diff --git a/internal/generator/templates/themes/matrix/theme.css b/internal/generator/templates/themes/matrix/theme.css
new file mode 100644
index 0000000..8254d45
--- /dev/null
+++ b/internal/generator/templates/themes/matrix/theme.css
@@ -0,0 +1,76 @@
+ :root { --g:#00ff41; --g2:#008f11; --g3:#003b00; --bg:#000; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--g);
+ overflow:hidden; height:100vh; }
+ /* scanline overlay sits above WebGL */
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
+ background:repeating-linear-gradient(0deg,transparent,transparent 3px,
+ rgba(0,0,0,0.08) 3px,rgba(0,0,0,0.08) 4px); }
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
+ /* WebGL background canvas */
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:12px 24px; background:#000; border-bottom:1px solid var(--g2);
+ 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(--g); text-shadow:0 0 18px var(--g); letter-spacing:3px; }
+ /* blinking cursor after logo mark */
+ .logo-mark::after { content:'_'; animation:blink 1.2s step-start infinite; }
+ .logo-title h1 { font-size:1.2rem; color:var(--g); text-shadow:0 0 10px var(--g);
+ letter-spacing:4px; font-weight:normal; }
+ .logo-title .subtitle { font-size:0.72rem; color:var(--g2); margin-top:2px; letter-spacing:1px; }
+ .logo-title .subtitle a { color:var(--g); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--g); }
+ .transmit-btn { border:1px solid var(--g2); color:var(--g); padding:8px 18px;
+ text-decoration:none; font-size:0.82rem; letter-spacing:2px;
+ transition:all 0.1s; }
+ .transmit-btn:hover { background:var(--g); color:var(--bg); }
+ a.header-feed-link { color:var(--g2); }
+ a.header-feed-link:hover { color:var(--g); text-shadow:0 0 8px var(--g); }
+ .nav-hints { background:#000; border-bottom:1px solid var(--g3); color:var(--g2);
+ padding:4px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:transparent; border:1px solid var(--g3); color:var(--g);
+ padding:0 5px; font-size:0.68rem; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:14px 24px;
+ scrollbar-width:thin; scrollbar-color:var(--g2) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:12px 0; }
+ .page-nav a { border:1px solid var(--g2); color:var(--g); padding:7px 20px;
+ text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
+ .page-nav a:hover { background:var(--g); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:#000; border-top:1px solid var(--g2); }
+ .post { background:#000; border:1px solid var(--g3); padding:16px 18px;
+ margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
+ .post:hover { border-color:var(--g2); box-shadow:0 0 8px rgba(0,255,65,0.2); }
+ .post-active { border-color:var(--g) !important; background:rgba(0,255,65,0.03) !important;
+ box-shadow:0 0 14px rgba(0,255,65,0.3),inset 3px 0 0 var(--g) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.85rem; }
+ .post-time { color:var(--g2); font-size:0.78rem; }
+ .post-text { line-height:1.6; font-size:0.88rem; }
+ .post-text a { color:var(--g); text-decoration:underline; }
+ .post-image { max-width:100%; margin-top:10px; border:1px solid var(--g3); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(0,0,0,0.98); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:740px; margin:0 auto; background:#000;
+ border:1px solid var(--g); padding:36px;
+ box-shadow:0 0 40px rgba(0,255,65,0.25); }
+ .modal-close { float:right; background:none; border:none; color:var(--g2);
+ font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
+ [data-sno-theme="matrix"] .splash-overlay { background: #000; font-family:'Courier New',monospace; }
+ [data-sno-theme="matrix"] .splash-rain {
+ position:absolute; inset:0; overflow:hidden; pointer-events:none; opacity:0.35; z-index:1;
+ font-size:11px; line-height:14px; color:var(--g2); text-align:left; padding:8px;
+ white-space:pre; animation: splashMatrixScroll 16s linear infinite;
+ }
+ @keyframes splashMatrixScroll { to { transform: translateY(-24px); } }
+ [data-sno-theme="matrix"] .splash-title {
+ position:relative; z-index:1; font-size:clamp(1.1rem,3.5vw,1.5rem); color:var(--g);
+ text-shadow:0 0 20px var(--g); letter-spacing:0.35em;
+ animation: splashMatrixGlow 1.8s ease-in-out infinite alternate;
+ }
+ @keyframes splashMatrixGlow { from { opacity:0.85; } to { opacity:1; text-shadow:0 0 28px var(--g); } }
+ [data-sno-theme="matrix"] .splash-tag { position:relative; z-index:1; color:rgba(0,255,65,0.88); }
+ [data-sno-theme="matrix"] .splash-hint { position:relative; z-index:1; color:rgba(0,255,65,0.82); }
diff --git a/internal/generator/templates/themes/matrix.tmpl b/internal/generator/templates/themes/matrix/theme.js
index 40d74c1..7ca694f 100644
--- a/internal/generator/templates/themes/matrix.tmpl
+++ b/internal/generator/templates/themes/matrix/theme.js
@@ -1,105 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo // MATRIX</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --g:#00ff41; --g2:#008f11; --g3:#003b00; --bg:#000; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--g);
- overflow:hidden; height:100vh; }
- /* scanline overlay sits above WebGL */
- body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
- background:repeating-linear-gradient(0deg,transparent,transparent 3px,
- rgba(0,0,0,0.08) 3px,rgba(0,0,0,0.08) 4px); }
- @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
- /* WebGL background canvas */
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:12px 24px; background:#000; border-bottom:1px solid var(--g2);
- 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(--g); text-shadow:0 0 18px var(--g); letter-spacing:3px; }
- /* blinking cursor after logo mark */
- .logo-mark::after { content:'_'; animation:blink 1.2s step-start infinite; }
- .logo-title h1 { font-size:1.2rem; color:var(--g); text-shadow:0 0 10px var(--g);
- letter-spacing:4px; font-weight:normal; }
- .logo-title .subtitle { font-size:0.72rem; color:var(--g2); margin-top:2px; letter-spacing:1px; }
- .logo-title .subtitle a { color:var(--g); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--g); }
- .transmit-btn { border:1px solid var(--g2); color:var(--g); padding:8px 18px;
- text-decoration:none; font-size:0.82rem; letter-spacing:2px;
- transition:all 0.1s; }
- .transmit-btn:hover { background:var(--g); color:var(--bg); }
- a.header-feed-link { color:var(--g2); }
- a.header-feed-link:hover { color:var(--g); text-shadow:0 0 8px var(--g); }
- .nav-hints { background:#000; border-bottom:1px solid var(--g3); color:var(--g2);
- padding:4px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:transparent; border:1px solid var(--g3); color:var(--g);
- padding:0 5px; font-size:0.68rem; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:14px 24px;
- scrollbar-width:thin; scrollbar-color:var(--g2) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:12px 0; }
- .page-nav a { border:1px solid var(--g2); color:var(--g); padding:7px 20px;
- text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
- .page-nav a:hover { background:var(--g); color:var(--bg); }
- .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
- background:#000; border-top:1px solid var(--g2); }
- .post { background:#000; border:1px solid var(--g3); padding:16px 18px;
- margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
- .post:hover { border-color:var(--g2); box-shadow:0 0 8px rgba(0,255,65,0.2); }
- .post-active { border-color:var(--g) !important; background:rgba(0,255,65,0.03) !important;
- box-shadow:0 0 14px rgba(0,255,65,0.3),inset 3px 0 0 var(--g) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.85rem; }
- .post-time { color:var(--g2); font-size:0.78rem; }
- .post-text { line-height:1.6; font-size:0.88rem; }
- .post-text a { color:var(--g); text-decoration:underline; }
- .post-image { max-width:100%; margin-top:10px; border:1px solid var(--g3); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(0,0,0,0.98); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:740px; margin:0 auto; background:#000;
- border:1px solid var(--g); padding:36px;
- box-shadow:0 0 40px rgba(0,255,65,0.25); }
- .modal-close { float:right; background:none; border:none; color:var(--g2);
- font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
- .splash-overlay.splash-matrix { background: #000; font-family:'Courier New',monospace; }
- .splash-matrix .splash-rain {
- position:absolute; inset:0; overflow:hidden; pointer-events:none; opacity:0.35; z-index:1;
- font-size:11px; line-height:14px; color:var(--g2); text-align:left; padding:8px;
- white-space:pre; animation: splashMatrixScroll 16s linear infinite;
- }
- @keyframes splashMatrixScroll { to { transform: translateY(-24px); } }
- .splash-matrix .splash-title {
- position:relative; z-index:1; font-size:clamp(1.1rem,3.5vw,1.5rem); color:var(--g);
- text-shadow:0 0 20px var(--g); letter-spacing:0.35em;
- animation: splashMatrixGlow 1.8s ease-in-out infinite alternate;
- }
- @keyframes splashMatrixGlow { from { opacity:0.85; } to { opacity:1; text-shadow:0 0 28px var(--g); } }
- .splash-matrix .splash-tag { position:relative; z-index:1; color:rgba(0,255,65,0.88); }
- .splash-matrix .splash-hint { position:relative; z-index:1; color:rgba(0,255,65,0.82); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-matrix" 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-rain" aria-hidden="true">01001110 01000101 01001111
-10101010 11001100 00110011
-01110011 01101110 01101111
-11001010 10100101 01011010</div>
- <div class="splash-inner">
- <div class="splash-title">SNONUX.FOO</div>
- <div class="splash-tag">Follow the signal</div>
- <div class="splash-hint">wake up — click or enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -119,46 +18,8 @@
pos.needsUpdate=true;pts.rotation.y=t*0.15;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 / <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.xml</a>
- <a href="https://foo.zone/about" class="transmit-btn">TRANSMIT</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}}">&lt;-- NEWER</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
+
+
// Matrix WebGL scene: 80 columns of falling particles with per-vertex colour.
// Each column has a "head" that falls at a random speed; particles near the head
// are bright green and fade to near-black further behind, simulating digital rain.
@@ -309,7 +170,3 @@
if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 320); }
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/neon.tmpl b/internal/generator/templates/themes/neon.tmpl
deleted file mode 100644
index bf8bd89..0000000
--- a/internal/generator/templates/themes/neon.tmpl
+++ /dev/null
@@ -1,348 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo • NEON NEXUS</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
- <style>
- @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&display=swap');
- :root { --neon-cyan:#00f5ff; --neon-magenta:#ff00cc; --neon-yellow:#ffe700; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Orbitron',sans-serif; background:#0b001a; color:#e0f8ff; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:16px 30px; display:flex; align-items:center; justify-content:space-between;
- background:rgba(11,0,26,0.8); backdrop-filter:blur(12px);
- border-bottom:2px solid rgba(255,231,0,0.3); }
- .logo { display:flex; align-items:center; gap:12px; }
- #sn-logo { flex-shrink:0; }
- .logo-title h1 { font-size:2rem; font-weight:700; letter-spacing:-3px; text-shadow:0 0 25px var(--neon-cyan); }
- .logo-title .subtitle { font-size:0.68rem; opacity:0.6; letter-spacing:1px; margin-top:2px; }
- .logo-title .subtitle a { color:var(--neon-cyan); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--neon-cyan); }
- .nav { gap:16px; }
- a.header-feed-link { color:var(--neon-cyan); text-shadow:0 0 8px rgba(0,245,255,0.35); }
- .transmit-btn { background:transparent; border:3px solid var(--neon-yellow); color:var(--neon-yellow);
- padding:12px 28px; border-radius:9999px; font-weight:600; letter-spacing:1px;
- display:flex; align-items:center; gap:10px; box-shadow:0 0 30px var(--neon-yellow);
- transition:all 0.3s; text-decoration:none; font-family:'Orbitron',sans-serif; font-size:0.9rem; }
- .transmit-btn:hover { background:var(--neon-yellow); color:#0b001a; transform:scale(1.08); }
- .content { flex:1; padding:30px; overflow-y:auto; scrollbar-width:thin; scrollbar-color:#ffe700 #1a0033; }
- .page-nav { display:flex; justify-content:center; margin:18px 0; }
- .page-nav a { background:transparent; border:2px solid var(--neon-cyan); color:var(--neon-cyan);
- padding:10px 28px; border-radius:9999px; font-size:0.85rem; letter-spacing:2px;
- text-decoration:none; transition:all 0.3s; }
- .page-nav a:hover { background:var(--neon-cyan); color:#0b001a; }
- .page-nav-footer { flex-shrink:0; padding:8px 30px; display:flex; justify-content:center;
- background:rgba(11,0,26,0.8); backdrop-filter:blur(12px);
- border-top:2px solid rgba(255,231,0,0.3); }
- .post { background:rgba(20,5,45,0.9); border:2px solid transparent;
- border-image:linear-gradient(45deg,var(--neon-cyan),var(--neon-magenta)) 1;
- border-radius:24px; padding:28px; margin-bottom:28px;
- box-shadow:0 0 35px rgba(0,245,255,0.5);
- transition:all 0.4s cubic-bezier(0.23,1,0.32,1); cursor:pointer; }
- .post:hover { transform:translateY(-8px) rotate(1deg); box-shadow:0 0 50px rgba(255,231,0,0.6); }
- .post-active { border-image:none !important; border-color:var(--neon-yellow) !important;
- background:rgba(40,20,70,0.97) !important;
- box-shadow:0 0 0 2px var(--neon-yellow),0 0 30px rgba(255,231,0,0.7),
- 0 0 70px rgba(255,231,0,0.35),inset 4px 0 0 var(--neon-yellow) !important;
- transform:translateY(-6px) scale(1.012); }
- .post-header { display:flex; justify-content:space-between; margin-bottom:18px; font-size:0.95rem; }
- .post-time { font-family:monospace; color:var(--neon-yellow); text-shadow:0 0 12px var(--neon-yellow); }
- .post-text { font-size:1.1rem; line-height:1.55; }
- .post-text a { color:var(--neon-cyan); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--neon-cyan); }
- .post-image { max-width:100%; border-radius:12px; margin-top:12px; }
- .post-audio { width:100%; margin-top:12px; }
- .nav-hints { display:flex; gap:20px; justify-content:center; align-items:center;
- padding:6px 20px; background:rgba(11,0,26,0.7);
- border-bottom:1px solid rgba(0,245,255,0.15);
- font-size:0.68rem; letter-spacing:1.5px; color:rgba(224,248,255,0.5); flex-wrap:wrap; }
- .nav-hints kbd { display:inline-block; background:rgba(0,245,255,0.1);
- border:1px solid rgba(0,245,255,0.35); border-radius:4px; padding:1px 5px;
- color:var(--neon-cyan); font-family:monospace; font-size:0.72rem; margin:0 2px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(11,0,26,0.95); backdrop-filter:blur(16px);
- overflow-y:auto; padding:40px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:800px; margin:0 auto; background:rgba(20,5,45,0.98);
- border:2px solid transparent;
- border-image:linear-gradient(45deg,var(--neon-yellow),var(--neon-magenta)) 1;
- border-radius:24px; padding:40px; box-shadow:0 0 80px rgba(255,231,0,0.4); }
- .modal-close { float:right; background:none; border:none; color:var(--neon-cyan);
- font-size:1.4rem; cursor:pointer; font-family:'Orbitron',sans-serif; }
- @media(max-width:640px) {
- .logo-title h1 { font-size:1.6rem; } #sn-logo { width:44px; height:44px; }
- .post { padding:22px; margin-bottom:22px; } .content { padding:20px; }
- header { padding:14px 20px; } .transmit-btn { padding:9px 16px; font-size:0.8rem; }
- .nav-hints { display:none; } .modal-inner { padding:24px 16px; }
- }
- .splash-overlay.splash-neon {
- background: radial-gradient(ellipse 120% 80% at 50% 35%, rgba(0,245,255,0.14) 0%, transparent 55%),
- radial-gradient(ellipse 90% 55% at 75% 85%, rgba(255,0,204,0.12) 0%, transparent 50%),
- #0b001a;
- }
- .splash-neon .splash-deco {
- width:100px; height:100px; margin:0 auto 1.25rem; border-radius:50%;
- border:3px solid var(--neon-cyan); box-shadow:0 0 36px var(--neon-cyan), inset 0 0 26px rgba(0,245,255,0.15);
- animation: splashNeonSpin 5s linear infinite;
- }
- @keyframes splashNeonSpin { to { transform: rotate(360deg); } }
- .splash-neon .splash-title {
- font-size: clamp(1.5rem, 5vw, 2.35rem);
- animation: splashNeonPulse 2s ease-in-out infinite alternate;
- }
- @keyframes splashNeonPulse {
- from { text-shadow: 0 0 12px var(--neon-cyan), 0 0 24px rgba(255,0,204,0.4); }
- to { text-shadow: 0 0 26px var(--neon-cyan), 0 0 48px var(--neon-magenta); }
- }
- .splash-neon .splash-tag { color: var(--neon-yellow); }
- .splash-neon .splash-hint { color: rgba(224,248,255,0.9); font-family: 'Orbitron', sans-serif; }
- .splash-neon .splash-inner { text-shadow: 0 2px 24px rgba(0,0,0,0.85), 0 0 40px rgba(11,0,26,0.9); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-neon" 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-inner">
- <div class="splash-deco" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Neon Nexus</div>
- <div class="splash-hint">Click or Enter &mdash; establish link</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,g=new THREE.Group(),t0=performance.now();
- 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(52,1,0.1,100);ca.position.set(0,0.4,9);
- var cols=[0x00f5ff,0xff00cc,0xffe700],i,m;
- for(i=0;i<3;i++){m=new THREE.Mesh(new THREE.TorusGeometry(1.55+i*0.48,0.055,8,48),new THREE.MeshBasicMaterial({color:cols[i],transparent:true,opacity:0.92}));m.rotation.x=Math.PI/2;m.userData.sp=0.01+i*0.004;g.add(m);}
- g.add(new THREE.Mesh(new THREE.SphereGeometry(0.52,20,20),new THREE.MeshBasicMaterial({color:0xffe700,transparent:true,opacity:0.95})));
- sc.add(g);sz();window.addEventListener('resize',sz);
- function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.y=t*0.42;g.rotation.x=Math.sin(t*0.65)*0.12;g.children.forEach(function(c){if(c.userData.sp)c.rotation.z+=c.userData.sp;});ren.render(sc,ca);}
- raf=requestAnimationFrame(loop);
- })();
- </script>
- <canvas id="three-canvas"></canvas>
- <div class="overlay">
- <header>
- <div class="logo">
- <svg id="sn-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" width="56" height="56" aria-label="snonux logo">
- <defs>
- <linearGradient id="sn-grad" x1="0" y1="0" x2="1" y2="1">
- <stop offset="0%" stop-color="#ffe700"/><stop offset="100%" stop-color="#ff00cc"/>
- </linearGradient>
- <radialGradient id="sn-bg" cx="40%" cy="35%" r="70%">
- <stop offset="0%" stop-color="#2d0060"/><stop offset="100%" stop-color="#0b001a"/>
- </radialGradient>
- <filter id="sn-gc" x="-60%" y="-60%" width="220%" height="220%">
- <feGaussianBlur stdDeviation="2.5" result="b"/>
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
- </filter>
- <filter id="sn-gm" x="-60%" y="-60%" width="220%" height="220%">
- <feGaussianBlur stdDeviation="2.5" result="b"/>
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
- </filter>
- <filter id="sn-gh" x="-20%" y="-20%" width="140%" height="140%">
- <feGaussianBlur stdDeviation="3" result="b"/>
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
- </filter>
- </defs>
- <polygon points="55,28 41.5,51.4 14.5,51.4 1,28 14.5,4.6 41.5,4.6"
- fill="none" stroke="#ffe700" stroke-width="5" opacity="0.18" filter="url(#sn-gh)"/>
- <polygon points="55,28 41.5,51.4 14.5,51.4 1,28 14.5,4.6 41.5,4.6"
- fill="url(#sn-bg)" stroke="url(#sn-grad)" stroke-width="1.8"/>
- <line x1="34" y1="12" x2="22" y2="44" stroke="#ffe700" stroke-width="0.9" opacity="0.75"/>
- <rect x="32.5" y="10.5" width="3" height="3" transform="rotate(45 34 12)" fill="#ffe700" opacity="0.8"/>
- <rect x="20.5" y="42.5" width="3" height="3" transform="rotate(45 22 44)" fill="#ffe700" opacity="0.8"/>
- <text x="9" y="37" font-family="Orbitron,monospace" font-weight="700" font-size="20"
- fill="#00f5ff" filter="url(#sn-gc)">S</text>
- <text x="28" y="37" font-family="Orbitron,monospace" font-weight="700" font-size="20"
- fill="#ff00cc" filter="url(#sn-gm)">N</text>
- </svg>
- <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">
- <i class="fa-solid fa-feather-pointed"></i> TRANSMIT TO NEXUS
- </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 TRANSMISSIONS</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER TRANSMISSIONS &rarr;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
- // Three.js neon nexus scene — central orb, orbiting rings, particle field.
- let scene, camera, renderer, centralSphere, rings = [], particles;
- function initThree() {
- const canvas = document.getElementById('three-canvas');
- renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- scene = new THREE.Scene();
- scene.fog = new THREE.Fog(0x0b001a, 15, 80);
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
- camera.position.set(0, 12, 35);
- scene.add(new THREE.AmbientLight(0x00f5ff, 0.8));
- const coreLight = new THREE.PointLight(0xff00cc, 4, 100);
- coreLight.position.set(0,0,0); scene.add(coreLight);
- centralSphere = new THREE.Mesh(new THREE.SphereGeometry(6,64,64),
- new THREE.MeshPhongMaterial({color:0x00f5ff,emissive:0xff00cc,emissiveIntensity:1.8,
- shininess:100,transparent:true,opacity:0.95}));
- scene.add(centralSphere);
- scene.add(new THREE.Mesh(new THREE.SphereGeometry(4.5,64,64),
- new THREE.MeshBasicMaterial({color:0x00f5ff,transparent:true,opacity:0.4,blending:THREE.AdditiveBlending})));
- const rc=[0x00f5ff,0xff00cc,0x00f5ff,0xffe700];
- for(let i=0;i<14;i++){
- const ring=new THREE.Mesh(new THREE.TorusGeometry(12+i*2.2,0.35,32,128),
- new THREE.MeshPhongMaterial({color:rc[i%4],emissive:rc[i%4],emissiveIntensity:2.5,
- shininess:80,transparent:true,opacity:0.9,side:THREE.DoubleSide}));
- ring.rotation.x=Math.random()*Math.PI;
- ring.userData={speed:0.008+i*0.003,axisTilt:Math.random()*0.6};
- scene.add(ring); rings.push(ring);
- }
- const pCount=2200,pos=new Float32Array(pCount*3),col=new Float32Array(pCount*3);
- for(let i=0;i<pCount*3;i+=3){
- const r=30+Math.random()*40,t=Math.random()*Math.PI*2,p=Math.acos(2*Math.random()-1);
- pos[i]=r*Math.sin(p)*Math.cos(t);pos[i+1]=r*Math.sin(p)*Math.sin(t);pos[i+2]=r*Math.cos(p);
- const c=new THREE.Color().setHSL(Math.random()>0.5?0.55:0.8,1,1);
- col[i]=c.r;col[i+1]=c.g;col[i+2]=c.b;
- }
- const pg=new THREE.BufferGeometry();
- pg.setAttribute('position',new THREE.BufferAttribute(pos,3));
- pg.setAttribute('color',new THREE.BufferAttribute(col,3));
- particles=new THREE.Points(pg,new THREE.PointsMaterial(
- {size:0.22,vertexColors:true,transparent:true,opacity:0.9,blending:THREE.AdditiveBlending}));
- scene.add(particles);
- let mouseX=0;
- window.addEventListener('mousemove',e=>{mouseX=(e.clientX/window.innerWidth)*2-1;});
- (function animate(){
- requestAnimationFrame(animate);
- const time=Date.now()*0.0004;
- camera.position.x=Math.sin(time)*35+mouseX*6;
- camera.position.z=Math.cos(time)*35+10;
- camera.lookAt(0,4,0);
- centralSphere.rotation.y+=0.003;
- rings.forEach((ring,i)=>{
- ring.rotation.y+=ring.userData.speed;
- ring.rotation.x=Math.sin(time*1.5+i)*ring.userData.axisTilt;
- });
- particles.rotation.y+=window._snoNeonWild ? 0.012 : 0.0008;
- renderer.render(scene,camera);
- })();
- }
- window.addEventListener('resize',()=>{
- if(!camera||!renderer) return;
- camera.aspect=window.innerWidth/window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(window.innerWidth,window.innerHeight);
- });
- window.onload=initThree;
- </script>
- <script>
- // Neon nav/wild effects — lightning flash on navigate, ring frenzy on wild
- (function() {
- function flash(color, ms) {
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:' + color + ';transition:opacity ' + (ms||180) + 'ms';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity = '0'; setTimeout(function() { d.remove(); }, ms || 180); }, 30);
- }
- function fxOverlay(cls, ms) {
- var ov = document.querySelector('.overlay');
- if (!ov) return;
- ov.classList.add(cls);
- setTimeout(function() { ov.classList.remove(cls); }, ms || 380);
- }
- var _wild = false;
- window.snonuxOpenEffect = function(post) {
- // Modal burst from center with lightning ring
- var modal = document.getElementById('post-modal');
- if (modal) { modal.classList.add('sno-modal-expand'); setTimeout(function() { modal.classList.remove('sno-modal-expand'); }, 420); }
- // Cyan ring pulse radiating outward
- var ring = document.createElement('div');
- var r = post ? post.getBoundingClientRect() : {left: window.innerWidth/2, top: window.innerHeight/2};
- ring.style.cssText = 'position:fixed;top:' + (r.top+20) + 'px;left:' + (r.left+20) + 'px;z-index:997;pointer-events:none;width:10px;height:10px;border-radius:50%;border:3px solid rgba(0,245,255,0.9);transition:all 0.38s ease,opacity 0.38s';
- document.body.appendChild(ring);
- setTimeout(function() { ring.style.transform='scale(35)'; ring.style.opacity='0'; setTimeout(function() { ring.remove(); }, 420); }, 15);
- };
- window.snonuxCloseEffect = function() {
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(255,0,204,0.12);transition:opacity 0.18s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 15);
- };
- window.snonuxNavEffect = function() {
- flash('rgba(0,245,255,0.22)', 160);
- fxOverlay('sno-fx-shake', 380);
- };
- window.snonuxPageEffect = function() {
- flash('rgba(255,231,0,0.18)', 140);
- fxOverlay('sno-fx-zoom', 320);
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,transparent,rgba(0,245,255,0.9),rgba(255,0,204,0.9),transparent);' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.3s ease,opacity 0.3s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? '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);
- // Speed up all rings and particles when wild
- if (rings && rings.length) {
- rings.forEach(function(r, i) {
- r.userData.speed = _wild ? (0.008 + i * 0.003) * 14 : 0.008 + i * 0.003;
- });
- }
- // Store wild state for particle rotation boost in animate loop
- window._snoNeonWild = _wild;
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/neon/meta.json b/internal/generator/templates/themes/neon/meta.json
new file mode 100644
index 0000000..1ea8510
--- /dev/null
+++ b/internal/generator/templates/themes/neon/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo • NEON NEXUS",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003csvg id=\"sn-logo\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 56 56\" width=\"56\" height=\"56\" aria-label=\"snonux logo\"\u003e\n \u003cdefs\u003e\n \u003clinearGradient id=\"sn-grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"\u003e\n \u003cstop offset=\"0%\" stop-color=\"#ffe700\"/\u003e\u003cstop offset=\"100%\" stop-color=\"#ff00cc\"/\u003e\n \u003c/linearGradient\u003e\n \u003cradialGradient id=\"sn-bg\" cx=\"40%\" cy=\"35%\" r=\"70%\"\u003e\n \u003cstop offset=\"0%\" stop-color=\"#2d0060\"/\u003e\u003cstop offset=\"100%\" stop-color=\"#0b001a\"/\u003e\n \u003c/radialGradient\u003e\n \u003cfilter id=\"sn-gc\" x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\"\u003e\n \u003cfeGaussianBlur stdDeviation=\"2.5\" result=\"b\"/\u003e\n \u003cfeMerge\u003e\u003cfeMergeNode in=\"b\"/\u003e\u003cfeMergeNode in=\"SourceGraphic\"/\u003e\u003c/feMerge\u003e\n \u003c/filter\u003e\n \u003cfilter id=\"sn-gm\" x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\"\u003e\n \u003cfeGaussianBlur stdDeviation=\"2.5\" result=\"b\"/\u003e\n \u003cfeMerge\u003e\u003cfeMergeNode in=\"b\"/\u003e\u003cfeMergeNode in=\"SourceGraphic\"/\u003e\u003c/feMerge\u003e\n \u003c/filter\u003e\n \u003cfilter id=\"sn-gh\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"\u003e\n \u003cfeGaussianBlur stdDeviation=\"3\" result=\"b\"/\u003e\n \u003cfeMerge\u003e\u003cfeMergeNode in=\"b\"/\u003e\u003cfeMergeNode in=\"SourceGraphic\"/\u003e\u003c/feMerge\u003e\n \u003c/filter\u003e\n \u003c/defs\u003e\n \u003cpolygon points=\"55,28 41.5,51.4 14.5,51.4 1,28 14.5,4.6 41.5,4.6\"\n fill=\"none\" stroke=\"#ffe700\" stroke-width=\"5\" opacity=\"0.18\" filter=\"url(#sn-gh)\"/\u003e\n \u003cpolygon points=\"55,28 41.5,51.4 14.5,51.4 1,28 14.5,4.6 41.5,4.6\"\n fill=\"url(#sn-bg)\" stroke=\"url(#sn-grad)\" stroke-width=\"1.8\"/\u003e\n \u003cline x1=\"34\" y1=\"12\" x2=\"22\" y2=\"44\" stroke=\"#ffe700\" stroke-width=\"0.9\" opacity=\"0.75\"/\u003e\n \u003crect x=\"32.5\" y=\"10.5\" width=\"3\" height=\"3\" transform=\"rotate(45 34 12)\" fill=\"#ffe700\" opacity=\"0.8\"/\u003e\n \u003crect x=\"20.5\" y=\"42.5\" width=\"3\" height=\"3\" transform=\"rotate(45 22 44)\" fill=\"#ffe700\" opacity=\"0.8\"/\u003e\n \u003ctext x=\"9\" y=\"37\" font-family=\"Orbitron,monospace\" font-weight=\"700\" font-size=\"20\"\n fill=\"#00f5ff\" filter=\"url(#sn-gc)\"\u003eS\u003c/text\u003e\n \u003ctext x=\"28\" y=\"37\" font-family=\"Orbitron,monospace\" font-weight=\"700\" font-size=\"20\"\n fill=\"#ff00cc\" filter=\"url(#sn-gm)\"\u003eN\u003c/text\u003e\n \u003c/svg\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003e\n \u003ci class=\"fa-solid fa-feather-pointed\"\u003e\u003c/i\u003e TRANSMIT TO NEXUS\n \u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-deco\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eNeon Nexus\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter \u0026mdash; establish link\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; NEWER TRANSMISSIONS",
+ "next_page_text": "OLDER TRANSMISSIONS \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/neon/theme.css b/internal/generator/templates/themes/neon/theme.css
new file mode 100644
index 0000000..1db90e8
--- /dev/null
+++ b/internal/generator/templates/themes/neon/theme.css
@@ -0,0 +1,94 @@
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&display=swap');
+ :root { --neon-cyan:#00f5ff; --neon-magenta:#ff00cc; --neon-yellow:#ffe700; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Orbitron',sans-serif; background:#0b001a; color:#e0f8ff; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:16px 30px; display:flex; align-items:center; justify-content:space-between;
+ background:rgba(11,0,26,0.8); backdrop-filter:blur(12px);
+ border-bottom:2px solid rgba(255,231,0,0.3); }
+ .logo { display:flex; align-items:center; gap:12px; }
+ #sn-logo { flex-shrink:0; }
+ .logo-title h1 { font-size:2rem; font-weight:700; letter-spacing:-3px; text-shadow:0 0 25px var(--neon-cyan); }
+ .logo-title .subtitle { font-size:0.68rem; opacity:0.6; letter-spacing:1px; margin-top:2px; }
+ .logo-title .subtitle a { color:var(--neon-cyan); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--neon-cyan); }
+ .nav { gap:16px; }
+ a.header-feed-link { color:var(--neon-cyan); text-shadow:0 0 8px rgba(0,245,255,0.35); }
+ .transmit-btn { background:transparent; border:3px solid var(--neon-yellow); color:var(--neon-yellow);
+ padding:12px 28px; border-radius:9999px; font-weight:600; letter-spacing:1px;
+ display:flex; align-items:center; gap:10px; box-shadow:0 0 30px var(--neon-yellow);
+ transition:all 0.3s; text-decoration:none; font-family:'Orbitron',sans-serif; font-size:0.9rem; }
+ .transmit-btn:hover { background:var(--neon-yellow); color:#0b001a; transform:scale(1.08); }
+ .content { flex:1; padding:30px; overflow-y:auto; scrollbar-width:thin; scrollbar-color:#ffe700 #1a0033; }
+ .page-nav { display:flex; justify-content:center; margin:18px 0; }
+ .page-nav a { background:transparent; border:2px solid var(--neon-cyan); color:var(--neon-cyan);
+ padding:10px 28px; border-radius:9999px; font-size:0.85rem; letter-spacing:2px;
+ text-decoration:none; transition:all 0.3s; }
+ .page-nav a:hover { background:var(--neon-cyan); color:#0b001a; }
+ .page-nav-footer { flex-shrink:0; padding:8px 30px; display:flex; justify-content:center;
+ background:rgba(11,0,26,0.8); backdrop-filter:blur(12px);
+ border-top:2px solid rgba(255,231,0,0.3); }
+ .post { background:rgba(20,5,45,0.9); border:2px solid transparent;
+ border-image:linear-gradient(45deg,var(--neon-cyan),var(--neon-magenta)) 1;
+ border-radius:24px; padding:28px; margin-bottom:28px;
+ box-shadow:0 0 35px rgba(0,245,255,0.5);
+ transition:all 0.4s cubic-bezier(0.23,1,0.32,1); cursor:pointer; }
+ .post:hover { transform:translateY(-8px) rotate(1deg); box-shadow:0 0 50px rgba(255,231,0,0.6); }
+ .post-active { border-image:none !important; border-color:var(--neon-yellow) !important;
+ background:rgba(40,20,70,0.97) !important;
+ box-shadow:0 0 0 2px var(--neon-yellow),0 0 30px rgba(255,231,0,0.7),
+ 0 0 70px rgba(255,231,0,0.35),inset 4px 0 0 var(--neon-yellow) !important;
+ transform:translateY(-6px) scale(1.012); }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:18px; font-size:0.95rem; }
+ .post-time { font-family:monospace; color:var(--neon-yellow); text-shadow:0 0 12px var(--neon-yellow); }
+ .post-text { font-size:1.1rem; line-height:1.55; }
+ .post-text a { color:var(--neon-cyan); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--neon-cyan); }
+ .post-image { max-width:100%; border-radius:12px; margin-top:12px; }
+ .post-audio { width:100%; margin-top:12px; }
+ .nav-hints { display:flex; gap:20px; justify-content:center; align-items:center;
+ padding:6px 20px; background:rgba(11,0,26,0.7);
+ border-bottom:1px solid rgba(0,245,255,0.15);
+ font-size:0.68rem; letter-spacing:1.5px; color:rgba(224,248,255,0.5); flex-wrap:wrap; }
+ .nav-hints kbd { display:inline-block; background:rgba(0,245,255,0.1);
+ border:1px solid rgba(0,245,255,0.35); border-radius:4px; padding:1px 5px;
+ color:var(--neon-cyan); font-family:monospace; font-size:0.72rem; margin:0 2px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(11,0,26,0.95); backdrop-filter:blur(16px);
+ overflow-y:auto; padding:40px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:800px; margin:0 auto; background:rgba(20,5,45,0.98);
+ border:2px solid transparent;
+ border-image:linear-gradient(45deg,var(--neon-yellow),var(--neon-magenta)) 1;
+ border-radius:24px; padding:40px; box-shadow:0 0 80px rgba(255,231,0,0.4); }
+ .modal-close { float:right; background:none; border:none; color:var(--neon-cyan);
+ font-size:1.4rem; cursor:pointer; font-family:'Orbitron',sans-serif; }
+ @media(max-width:640px) {
+ .logo-title h1 { font-size:1.6rem; } #sn-logo { width:44px; height:44px; }
+ .post { padding:22px; margin-bottom:22px; } .content { padding:20px; }
+ header { padding:14px 20px; } .transmit-btn { padding:9px 16px; font-size:0.8rem; }
+ .nav-hints { display:none; } .modal-inner { padding:24px 16px; }
+ }
+ [data-sno-theme="neon"] .splash-overlay {
+ background: radial-gradient(ellipse 120% 80% at 50% 35%, rgba(0,245,255,0.14) 0%, transparent 55%),
+ radial-gradient(ellipse 90% 55% at 75% 85%, rgba(255,0,204,0.12) 0%, transparent 50%),
+ #0b001a;
+ }
+ [data-sno-theme="neon"] .splash-deco {
+ width:100px; height:100px; margin:0 auto 1.25rem; border-radius:50%;
+ border:3px solid var(--neon-cyan); box-shadow:0 0 36px var(--neon-cyan), inset 0 0 26px rgba(0,245,255,0.15);
+ animation: splashNeonSpin 5s linear infinite;
+ }
+ @keyframes splashNeonSpin { to { transform: rotate(360deg); } }
+ [data-sno-theme="neon"] .splash-title {
+ font-size: clamp(1.5rem, 5vw, 2.35rem);
+ animation: splashNeonPulse 2s ease-in-out infinite alternate;
+ }
+ @keyframes splashNeonPulse {
+ from { text-shadow: 0 0 12px var(--neon-cyan), 0 0 24px rgba(255,0,204,0.4); }
+ to { text-shadow: 0 0 26px var(--neon-cyan), 0 0 48px var(--neon-magenta); }
+ }
+ [data-sno-theme="neon"] .splash-tag { color: var(--neon-yellow); }
+ [data-sno-theme="neon"] .splash-hint { color: rgba(224,248,255,0.9); font-family: 'Orbitron', sans-serif; }
+ [data-sno-theme="neon"] .splash-inner { text-shadow: 0 2px 24px rgba(0,0,0,0.85), 0 0 40px rgba(11,0,26,0.9); }
diff --git a/internal/generator/templates/themes/neon/theme.js b/internal/generator/templates/themes/neon/theme.js
new file mode 100644
index 0000000..5247c76
--- /dev/null
+++ b/internal/generator/templates/themes/neon/theme.js
@@ -0,0 +1,155 @@
+
+ (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,g=new THREE.Group(),t0=performance.now();
+ 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(52,1,0.1,100);ca.position.set(0,0.4,9);
+ var cols=[0x00f5ff,0xff00cc,0xffe700],i,m;
+ for(i=0;i<3;i++){m=new THREE.Mesh(new THREE.TorusGeometry(1.55+i*0.48,0.055,8,48),new THREE.MeshBasicMaterial({color:cols[i],transparent:true,opacity:0.92}));m.rotation.x=Math.PI/2;m.userData.sp=0.01+i*0.004;g.add(m);}
+ g.add(new THREE.Mesh(new THREE.SphereGeometry(0.52,20,20),new THREE.MeshBasicMaterial({color:0xffe700,transparent:true,opacity:0.95})));
+ sc.add(g);sz();window.addEventListener('resize',sz);
+ function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.y=t*0.42;g.rotation.x=Math.sin(t*0.65)*0.12;g.children.forEach(function(c){if(c.userData.sp)c.rotation.z+=c.userData.sp;});ren.render(sc,ca);}
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ // Three.js neon nexus scene — central orb, orbiting rings, particle field.
+ let scene, camera, renderer, centralSphere, rings = [], particles;
+ function initThree() {
+ const canvas = document.getElementById('three-canvas');
+ renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ scene = new THREE.Scene();
+ scene.fog = new THREE.Fog(0x0b001a, 15, 80);
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 12, 35);
+ scene.add(new THREE.AmbientLight(0x00f5ff, 0.8));
+ const coreLight = new THREE.PointLight(0xff00cc, 4, 100);
+ coreLight.position.set(0,0,0); scene.add(coreLight);
+ centralSphere = new THREE.Mesh(new THREE.SphereGeometry(6,64,64),
+ new THREE.MeshPhongMaterial({color:0x00f5ff,emissive:0xff00cc,emissiveIntensity:1.8,
+ shininess:100,transparent:true,opacity:0.95}));
+ scene.add(centralSphere);
+ scene.add(new THREE.Mesh(new THREE.SphereGeometry(4.5,64,64),
+ new THREE.MeshBasicMaterial({color:0x00f5ff,transparent:true,opacity:0.4,blending:THREE.AdditiveBlending})));
+ const rc=[0x00f5ff,0xff00cc,0x00f5ff,0xffe700];
+ for(let i=0;i<14;i++){
+ const ring=new THREE.Mesh(new THREE.TorusGeometry(12+i*2.2,0.35,32,128),
+ new THREE.MeshPhongMaterial({color:rc[i%4],emissive:rc[i%4],emissiveIntensity:2.5,
+ shininess:80,transparent:true,opacity:0.9,side:THREE.DoubleSide}));
+ ring.rotation.x=Math.random()*Math.PI;
+ ring.userData={speed:0.008+i*0.003,axisTilt:Math.random()*0.6};
+ scene.add(ring); rings.push(ring);
+ }
+ const pCount=2200,pos=new Float32Array(pCount*3),col=new Float32Array(pCount*3);
+ for(let i=0;i<pCount*3;i+=3){
+ const r=30+Math.random()*40,t=Math.random()*Math.PI*2,p=Math.acos(2*Math.random()-1);
+ pos[i]=r*Math.sin(p)*Math.cos(t);pos[i+1]=r*Math.sin(p)*Math.sin(t);pos[i+2]=r*Math.cos(p);
+ const c=new THREE.Color().setHSL(Math.random()>0.5?0.55:0.8,1,1);
+ col[i]=c.r;col[i+1]=c.g;col[i+2]=c.b;
+ }
+ const pg=new THREE.BufferGeometry();
+ pg.setAttribute('position',new THREE.BufferAttribute(pos,3));
+ pg.setAttribute('color',new THREE.BufferAttribute(col,3));
+ particles=new THREE.Points(pg,new THREE.PointsMaterial(
+ {size:0.22,vertexColors:true,transparent:true,opacity:0.9,blending:THREE.AdditiveBlending}));
+ scene.add(particles);
+ let mouseX=0;
+ window.addEventListener('mousemove',e=>{mouseX=(e.clientX/window.innerWidth)*2-1;});
+ (function animate(){
+ requestAnimationFrame(animate);
+ const time=Date.now()*0.0004;
+ camera.position.x=Math.sin(time)*35+mouseX*6;
+ camera.position.z=Math.cos(time)*35+10;
+ camera.lookAt(0,4,0);
+ centralSphere.rotation.y+=0.003;
+ rings.forEach((ring,i)=>{
+ ring.rotation.y+=ring.userData.speed;
+ ring.rotation.x=Math.sin(time*1.5+i)*ring.userData.axisTilt;
+ });
+ particles.rotation.y+=window._snoNeonWild ? 0.012 : 0.0008;
+ renderer.render(scene,camera);
+ })();
+ }
+ window.addEventListener('resize',()=>{
+ if(!camera||!renderer) return;
+ camera.aspect=window.innerWidth/window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth,window.innerHeight);
+ });
+ window.onload=initThree;
+
+
+ // Neon nav/wild effects — lightning flash on navigate, ring frenzy on wild
+ (function() {
+ function flash(color, ms) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:' + color + ';transition:opacity ' + (ms||180) + 'ms';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity = '0'; setTimeout(function() { d.remove(); }, ms || 180); }, 30);
+ }
+ function fxOverlay(cls, ms) {
+ var ov = document.querySelector('.overlay');
+ if (!ov) return;
+ ov.classList.add(cls);
+ setTimeout(function() { ov.classList.remove(cls); }, ms || 380);
+ }
+ var _wild = false;
+ window.snonuxOpenEffect = function(post) {
+ // Modal burst from center with lightning ring
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-expand'); setTimeout(function() { modal.classList.remove('sno-modal-expand'); }, 420); }
+ // Cyan ring pulse radiating outward
+ var ring = document.createElement('div');
+ var r = post ? post.getBoundingClientRect() : {left: window.innerWidth/2, top: window.innerHeight/2};
+ ring.style.cssText = 'position:fixed;top:' + (r.top+20) + 'px;left:' + (r.left+20) + 'px;z-index:997;pointer-events:none;width:10px;height:10px;border-radius:50%;border:3px solid rgba(0,245,255,0.9);transition:all 0.38s ease,opacity 0.38s';
+ document.body.appendChild(ring);
+ setTimeout(function() { ring.style.transform='scale(35)'; ring.style.opacity='0'; setTimeout(function() { ring.remove(); }, 420); }, 15);
+ };
+ window.snonuxCloseEffect = function() {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(255,0,204,0.12);transition:opacity 0.18s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 15);
+ };
+ window.snonuxNavEffect = function() {
+ flash('rgba(0,245,255,0.22)', 160);
+ fxOverlay('sno-fx-shake', 380);
+ };
+ window.snonuxPageEffect = function() {
+ flash('rgba(255,231,0,0.18)', 140);
+ fxOverlay('sno-fx-zoom', 320);
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,transparent,rgba(0,245,255,0.9),rgba(255,0,204,0.9),transparent);' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.3s ease,opacity 0.3s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? '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);
+ // Speed up all rings and particles when wild
+ if (rings && rings.length) {
+ rings.forEach(function(r, i) {
+ r.userData.speed = _wild ? (0.008 + i * 0.003) * 14 : 0.008 + i * 0.003;
+ });
+ }
+ // Store wild state for particle rotation boost in animate loop
+ window._snoNeonWild = _wild;
+ };
+ })();
diff --git a/internal/generator/templates/themes/noir/meta.json b/internal/generator/templates/themes/noir/meta.json
new file mode 100644
index 0000000..2adf2ce
--- /dev/null
+++ b/internal/generator/templates/themes/noir/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // NOIR",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eCase File\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-blinds\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-city\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-sign\" aria-hidden=\"true\"\u003eVacancy\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eMidnight Edition\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to step under the streetlamp\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/noir/theme.css b/internal/generator/templates/themes/noir/theme.css
new file mode 100644
index 0000000..6fe8cdd
--- /dev/null
+++ b/internal/generator/templates/themes/noir/theme.css
@@ -0,0 +1,77 @@
+ :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;} }
+ [data-sno-theme="noir"] .splash-overlay {
+ 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%);
+ }
+ [data-sno-theme="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; }
+ [data-sno-theme="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; }
+ [data-sno-theme="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} }
+ [data-sno-theme="noir"] .splash-title { font-family:'Playfair Display',serif; font-size:clamp(1.7rem,5vw,2.5rem); color:var(--lamp); letter-spacing:0.08em; }
+ [data-sno-theme="noir"] .splash-tag { color:var(--silver); letter-spacing:0.24em; }
+ [data-sno-theme="noir"] .splash-hint { color:rgba(216,209,196,0.78); }
+ [data-sno-theme="noir"] .splash-inner { text-shadow:0 3px 22px rgba(0,0,0,0.95); }
diff --git a/internal/generator/templates/themes/noir.tmpl b/internal/generator/templates/themes/noir/theme.js
index 2d08979..eeff487 100644
--- a/internal/generator/templates/themes/noir.tmpl
+++ b/internal/generator/templates/themes/noir/theme.js
@@ -1,108 +1,4 @@
-<!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');
@@ -130,46 +26,8 @@
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;
@@ -302,7 +160,3 @@
if (b) b.classList.toggle('sno-wild-on', _wild);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/ocean/meta.json b/internal/generator/templates/themes/ocean/meta.json
new file mode 100644
index 0000000..2b4c2b2
--- /dev/null
+++ b/internal/generator/templates/themes/ocean/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ~ OCEAN",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-wave\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eDeep channel\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eSurface — click or Enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/ocean/theme.css b/internal/generator/templates/themes/ocean/theme.css
new file mode 100644
index 0000000..4a6e582
--- /dev/null
+++ b/internal/generator/templates/themes/ocean/theme.css
@@ -0,0 +1,69 @@
+ :root { --teal:#00b4d8; --aqua:#48cae4; --deep:#023e8a; --navy:#03045e; --foam:#caf0f8; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy);
+ color:var(--foam); overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(3,4,94,0.82); backdrop-filter:blur(12px);
+ border-bottom:1px solid rgba(0,180,216,0.3); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800; color:var(--aqua); text-shadow:0 0 16px var(--teal); }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:var(--foam); letter-spacing:1px; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(202,240,248,0.55); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--aqua); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--teal); }
+ .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--teal); color:var(--navy); }
+ a.header-feed-link { color:var(--aqua); }
+ a.header-feed-link:hover { color:var(--foam); }
+ .nav-hints { background:rgba(3,4,94,0.65); border-bottom:1px solid rgba(0,180,216,0.18);
+ color:rgba(202,240,248,0.45); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(0,180,216,0.12); border:1px solid rgba(0,180,216,0.35);
+ color:var(--aqua); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--teal) var(--navy); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--deep); color:var(--aqua); padding:8px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--teal); color:var(--navy); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(3,4,94,0.82); backdrop-filter:blur(12px);
+ border-top:1px solid rgba(0,180,216,0.3); }
+ .post { background:rgba(3,4,94,0.55); border:1px solid rgba(0,180,216,0.22); border-radius:10px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(6px); }
+ .post:hover { border-color:var(--teal); box-shadow:0 4px 24px rgba(0,180,216,0.22); transform:translateY(-2px); }
+ .post-active { border-color:var(--aqua) !important; background:rgba(0,100,150,0.55) !important;
+ box-shadow:0 0 22px rgba(72,202,228,0.35),inset 3px 0 0 var(--aqua) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--teal); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--aqua); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--teal); }
+ .post-audio { width:100%; margin-top:10px; }
+ .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(2,30,80,0.92);
+ border:1px solid var(--teal); border-radius:12px; backdrop-filter:blur(16px);
+ box-shadow:0 0 60px rgba(0,180,216,0.3); padding:40px; }
+ .modal-close { float:right; background:none; border:none; color:var(--teal);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ [data-sno-theme="ocean"] .splash-overlay {
+ background: linear-gradient(180deg, var(--navy) 0%, var(--deep) 45%, #001a3d 100%);
+ }
+ [data-sno-theme="ocean"] .splash-wave {
+ width:min(320px,88vw); height:14px; margin:0 auto 1.2rem; border-radius:50%;
+ background: radial-gradient(ellipse at 50% 0%, var(--aqua), transparent 70%);
+ opacity:0.7; animation: splashWaveBob 2.8s ease-in-out infinite;
+ box-shadow: 0 8px 40px rgba(0,180,216,0.35);
+ }
+ @keyframes splashWaveBob { 0%,100%{ transform: translateY(0) scaleX(1); } 50%{ transform: translateY(-6px) scaleX(1.05); } }
+ [data-sno-theme="ocean"] .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:var(--foam);
+ text-shadow:0 0 18px var(--teal); }
+ [data-sno-theme="ocean"] .splash-tag { color:var(--aqua); letter-spacing:0.2em; }
+ [data-sno-theme="ocean"] .splash-hint { color:rgba(202,240,248,0.88); }
+ [data-sno-theme="ocean"] .splash-inner { text-shadow: 0 2px 16px rgba(3,4,94,0.9); }
diff --git a/internal/generator/templates/themes/ocean.tmpl b/internal/generator/templates/themes/ocean/theme.js
index 8422398..58d2deb 100644
--- a/internal/generator/templates/themes/ocean.tmpl
+++ b/internal/generator/templates/themes/ocean/theme.js
@@ -1,95 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ~ OCEAN</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --teal:#00b4d8; --aqua:#48cae4; --deep:#023e8a; --navy:#03045e; --foam:#caf0f8; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy);
- color:var(--foam); overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(3,4,94,0.82); backdrop-filter:blur(12px);
- border-bottom:1px solid rgba(0,180,216,0.3); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800; color:var(--aqua); text-shadow:0 0 16px var(--teal); }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:var(--foam); letter-spacing:1px; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(202,240,248,0.55); margin-top:2px; }
- .logo-title .subtitle a { color:var(--aqua); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--teal); }
- .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px;
- border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--teal); color:var(--navy); }
- a.header-feed-link { color:var(--aqua); }
- a.header-feed-link:hover { color:var(--foam); }
- .nav-hints { background:rgba(3,4,94,0.65); border-bottom:1px solid rgba(0,180,216,0.18);
- color:rgba(202,240,248,0.45); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(0,180,216,0.12); border:1px solid rgba(0,180,216,0.35);
- color:var(--aqua); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--teal) var(--navy); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--deep); color:var(--aqua); padding:8px 20px;
- border-radius:20px; text-decoration:none; font-size:0.82rem; }
- .page-nav a:hover { background:var(--teal); color:var(--navy); }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(3,4,94,0.82); backdrop-filter:blur(12px);
- border-top:1px solid rgba(0,180,216,0.3); }
- .post { background:rgba(3,4,94,0.55); border:1px solid rgba(0,180,216,0.22); border-radius:10px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(6px); }
- .post:hover { border-color:var(--teal); box-shadow:0 4px 24px rgba(0,180,216,0.22); transform:translateY(-2px); }
- .post-active { border-color:var(--aqua) !important; background:rgba(0,100,150,0.55) !important;
- box-shadow:0 0 22px rgba(72,202,228,0.35),inset 3px 0 0 var(--aqua) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--teal); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--aqua); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--teal); }
- .post-audio { width:100%; margin-top:10px; }
- .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(2,30,80,0.92);
- border:1px solid var(--teal); border-radius:12px; backdrop-filter:blur(16px);
- box-shadow:0 0 60px rgba(0,180,216,0.3); padding:40px; }
- .modal-close { float:right; background:none; border:none; color:var(--teal);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- .splash-overlay.splash-ocean {
- background: linear-gradient(180deg, var(--navy) 0%, var(--deep) 45%, #001a3d 100%);
- }
- .splash-ocean .splash-wave {
- width:min(320px,88vw); height:14px; margin:0 auto 1.2rem; border-radius:50%;
- background: radial-gradient(ellipse at 50% 0%, var(--aqua), transparent 70%);
- opacity:0.7; animation: splashWaveBob 2.8s ease-in-out infinite;
- box-shadow: 0 8px 40px rgba(0,180,216,0.35);
- }
- @keyframes splashWaveBob { 0%,100%{ transform: translateY(0) scaleX(1); } 50%{ transform: translateY(-6px) scaleX(1.05); } }
- .splash-ocean .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:var(--foam);
- text-shadow:0 0 18px var(--teal); }
- .splash-ocean .splash-tag { color:var(--aqua); letter-spacing:0.2em; }
- .splash-ocean .splash-hint { color:rgba(202,240,248,0.88); }
- .splash-ocean .splash-inner { text-shadow: 0 2px 16px rgba(3,4,94,0.9); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-ocean" 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-inner">
- <div class="splash-wave" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Deep channel</div>
- <div class="splash-hint">Surface — click or Enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -108,46 +17,8 @@
g.children.forEach(function(c){if(c.userData.dy){c.position.y+=Math.sin(t*2+c.userData.x)*0.008;c.position.x=c.userData.x+Math.sin(t+c.userData.y0)*0.15;}});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">Transmit</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>
+
+
// Ocean WebGL: dramatic wave surface + sea rock spires + bioluminescent
// jellyfish + rising bubbles + a slow whale cruising the deep.
(function() {
@@ -381,7 +252,3 @@
if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/plasma/meta.json b/internal/generator/templates/themes/plasma/meta.json
new file mode 100644
index 0000000..42d2d7c
--- /dev/null
+++ b/internal/generator/templates/themes/plasma/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ◈ PLASMA",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-blobs\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003ePlasma lock\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eMerge — click or Enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/plasma/theme.css b/internal/generator/templates/themes/plasma/theme.css
new file mode 100644
index 0000000..cc36513
--- /dev/null
+++ b/internal/generator/templates/themes/plasma/theme.css
@@ -0,0 +1,78 @@
+ :root { --cyan:#00f0ff; --magenta:#ff00e0; --yellow:#ffee00; --bg:#050008; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
+ color:#e8e0ff; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(5,0,8,0.8); backdrop-filter:blur(14px);
+ border-bottom:1px solid rgba(0,240,255,0.2); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800;
+ background:linear-gradient(90deg,var(--cyan),var(--magenta));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:#e8e0ff; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(232,224,255,0.5); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--cyan); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--cyan); }
+ .transmit-btn { border:1px solid var(--magenta); color:var(--magenta); padding:9px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--magenta); color:var(--bg); }
+ a.header-feed-link { color:var(--cyan); }
+ a.header-feed-link:hover { color:var(--magenta); }
+ .nav-hints { background:rgba(5,0,8,0.65); border-bottom:1px solid rgba(0,240,255,0.12);
+ color:rgba(232,224,255,0.4); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(0,240,255,0.1); border:1px solid rgba(0,240,255,0.3);
+ color:var(--cyan); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--magenta) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--cyan); color:var(--cyan); padding:8px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--cyan); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(5,0,8,0.8); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(0,240,255,0.2); }
+ .post { background:rgba(10,0,20,0.75); border:1px solid rgba(0,240,255,0.18); border-radius:10px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(6px); }
+ .post:hover { border-color:var(--cyan); box-shadow:0 0 20px rgba(0,240,255,0.2); transform:translateY(-2px); }
+ .post-active { border-color:var(--magenta) !important; background:rgba(20,0,30,0.9) !important;
+ box-shadow:0 0 24px rgba(255,0,224,0.35),inset 3px 0 0 var(--magenta) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--cyan); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--cyan); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--cyan); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(5,0,8,0.96); backdrop-filter:blur(20px);
+ overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:rgba(10,0,25,0.98);
+ border:1px solid var(--magenta); border-radius:12px;
+ box-shadow:0 0 60px rgba(255,0,224,0.25); padding:40px; }
+ .modal-close { float:right; background:none; border:none; color:var(--cyan);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ [data-sno-theme="plasma"] .splash-overlay { background: var(--bg); overflow:hidden; }
+ [data-sno-theme="plasma"] .splash-blobs {
+ position:absolute; width:140%; height:140%; left:-20%; top:-20%; pointer-events:none;
+ background:
+ radial-gradient(ellipse at 30% 40%, rgba(0,240,255,0.25) 0%, transparent 45%),
+ radial-gradient(ellipse at 70% 60%, rgba(255,0,224,0.22) 0%, transparent 50%),
+ radial-gradient(ellipse at 50% 80%, rgba(255,238,0,0.12) 0%, transparent 40%);
+ animation: splashPlasmaDrift 10s ease-in-out infinite alternate;
+ filter: blur(2px);
+ }
+ @keyframes splashPlasmaDrift {
+ from { transform: translate(0,0) rotate(0deg); }
+ to { transform: translate(-4%,3%) rotate(8deg); }
+ }
+ [data-sno-theme="plasma"] .splash-inner { position:relative; z-index:1; }
+ [data-sno-theme="plasma"] .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#e8e0ff;
+ text-shadow:0 0 24px var(--cyan), 0 0 48px rgba(255,0,224,0.35); }
+ [data-sno-theme="plasma"] .splash-tag { color:var(--magenta); letter-spacing:0.18em; }
+ [data-sno-theme="plasma"] .splash-hint { color:rgba(232,224,255,0.86); }
+ [data-sno-theme="plasma"] .splash-blobs { z-index:1; }
+ [data-sno-theme="plasma"] .splash-inner { text-shadow: 0 2px 22px rgba(0,0,0,0.9); }
diff --git a/internal/generator/templates/themes/plasma.tmpl b/internal/generator/templates/themes/plasma/theme.js
index 332d07f..f6912b7 100644
--- a/internal/generator/templates/themes/plasma.tmpl
+++ b/internal/generator/templates/themes/plasma/theme.js
@@ -1,104 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ◈ PLASMA</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --cyan:#00f0ff; --magenta:#ff00e0; --yellow:#ffee00; --bg:#050008; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
- color:#e8e0ff; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(5,0,8,0.8); backdrop-filter:blur(14px);
- border-bottom:1px solid rgba(0,240,255,0.2); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800;
- background:linear-gradient(90deg,var(--cyan),var(--magenta));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:#e8e0ff; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(232,224,255,0.5); margin-top:2px; }
- .logo-title .subtitle a { color:var(--cyan); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--cyan); }
- .transmit-btn { border:1px solid var(--magenta); color:var(--magenta); padding:9px 20px;
- border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--magenta); color:var(--bg); }
- a.header-feed-link { color:var(--cyan); }
- a.header-feed-link:hover { color:var(--magenta); }
- .nav-hints { background:rgba(5,0,8,0.65); border-bottom:1px solid rgba(0,240,255,0.12);
- color:rgba(232,224,255,0.4); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(0,240,255,0.1); border:1px solid rgba(0,240,255,0.3);
- color:var(--cyan); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--magenta) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--cyan); color:var(--cyan); padding:8px 20px;
- border-radius:20px; text-decoration:none; font-size:0.82rem; }
- .page-nav a:hover { background:var(--cyan); color:var(--bg); }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(5,0,8,0.8); backdrop-filter:blur(14px);
- border-top:1px solid rgba(0,240,255,0.2); }
- .post { background:rgba(10,0,20,0.75); border:1px solid rgba(0,240,255,0.18); border-radius:10px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(6px); }
- .post:hover { border-color:var(--cyan); box-shadow:0 0 20px rgba(0,240,255,0.2); transform:translateY(-2px); }
- .post-active { border-color:var(--magenta) !important; background:rgba(20,0,30,0.9) !important;
- box-shadow:0 0 24px rgba(255,0,224,0.35),inset 3px 0 0 var(--magenta) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--cyan); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--cyan); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--cyan); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(5,0,8,0.96); backdrop-filter:blur(20px);
- overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:760px; margin:0 auto; background:rgba(10,0,25,0.98);
- border:1px solid var(--magenta); border-radius:12px;
- box-shadow:0 0 60px rgba(255,0,224,0.25); padding:40px; }
- .modal-close { float:right; background:none; border:none; color:var(--cyan);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- .splash-overlay.splash-plasma { background: var(--bg); overflow:hidden; }
- .splash-plasma .splash-blobs {
- position:absolute; width:140%; height:140%; left:-20%; top:-20%; pointer-events:none;
- background:
- radial-gradient(ellipse at 30% 40%, rgba(0,240,255,0.25) 0%, transparent 45%),
- radial-gradient(ellipse at 70% 60%, rgba(255,0,224,0.22) 0%, transparent 50%),
- radial-gradient(ellipse at 50% 80%, rgba(255,238,0,0.12) 0%, transparent 40%);
- animation: splashPlasmaDrift 10s ease-in-out infinite alternate;
- filter: blur(2px);
- }
- @keyframes splashPlasmaDrift {
- from { transform: translate(0,0) rotate(0deg); }
- to { transform: translate(-4%,3%) rotate(8deg); }
- }
- .splash-plasma .splash-inner { position:relative; z-index:1; }
- .splash-plasma .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#e8e0ff;
- text-shadow:0 0 24px var(--cyan), 0 0 48px rgba(255,0,224,0.35); }
- .splash-plasma .splash-tag { color:var(--magenta); letter-spacing:0.18em; }
- .splash-plasma .splash-hint { color:rgba(232,224,255,0.86); }
- .splash-plasma .splash-blobs { z-index:1; }
- .splash-plasma .splash-inner { text-shadow: 0 2px 22px rgba(0,0,0,0.9); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-plasma" 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-blobs" aria-hidden="true"></div>
- <div class="splash-inner">
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Plasma lock</div>
- <div class="splash-hint">Merge — click or Enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -117,46 +17,8 @@
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">Transmit</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>
+
+
// Plasma WebGL: 12 large translucent spheres drifting on independent sine
// paths with additive blending — overlapping blobs mix colours and pulse
// like a lava lamp or plasma ball. Dark bg, cyan/magenta/yellow palette.
@@ -300,7 +162,3 @@
if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/retro/meta.json b/internal/generator/templates/themes/retro/meta.json
new file mode 100644
index 0000000..8a14c24
--- /dev/null
+++ b/internal/generator/templates/themes/retro/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "SNONUX.FOO // RETRO",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003e[SN]\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003eSNONUX.FOO\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003eMICROBLOG / \u003ca href=\"https://foo.zone\"\u003eFOO.ZONE\u003c/a\u003e IS THE REAL BLOG\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003e*** SNONUX BBS ***\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eAmber phosphor mode\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003ePress Enter or click to connect\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026lt;-- NEWER",
+ "next_page_text": "OLDER --\u0026gt;"
+}
diff --git a/internal/generator/templates/themes/retro/theme.css b/internal/generator/templates/themes/retro/theme.css
new file mode 100644
index 0000000..cf0e235
--- /dev/null
+++ b/internal/generator/templates/themes/retro/theme.css
@@ -0,0 +1,79 @@
+ :root { --amber:#ffb000; --dim:#7a5200; --bg:#0a0800; --bg2:#050300; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--amber);
+ overflow:hidden; height:100vh; }
+ /* Phosphor scanlines overlay — sits above WebGL */
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
+ background:repeating-linear-gradient(0deg,transparent,transparent 2px,
+ rgba(0,0,0,0.15) 2px,rgba(0,0,0,0.15) 4px); }
+ /* Subtle glow flicker */
+ @keyframes amber-flicker { 0%,100%{opacity:1} 94%{opacity:0.98} 96%{opacity:0.93} }
+ body { animation:amber-flicker 11s infinite; }
+ /* WebGL background canvas */
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--amber);
+ display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.6rem; color:var(--amber); text-shadow:0 0 14px var(--amber);
+ letter-spacing:2px; }
+ .logo-title h1 { font-size:1.2rem; color:var(--amber); text-shadow:0 0 10px var(--amber);
+ letter-spacing:4px; font-weight:normal; }
+ .logo-title .subtitle { font-size:0.72rem; color:var(--dim); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--amber); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--amber); }
+ .transmit-btn { border:1px solid var(--amber); color:var(--amber); padding:8px 18px;
+ text-decoration:none; font-size:0.82rem; letter-spacing:2px;
+ transition:all 0.1s; }
+ .transmit-btn:hover { background:var(--amber); color:var(--bg); }
+ a.header-feed-link { color:var(--dim); }
+ a.header-feed-link:hover { color:var(--amber); text-shadow:0 0 6px var(--amber); }
+ .nav-hints { background:var(--bg2); border-bottom:1px solid var(--dim); color:var(--dim);
+ padding:4px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--amber);
+ padding:0 5px; font-size:0.68rem; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:14px 24px;
+ scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:12px 0; }
+ .page-nav a { border:1px solid var(--dim); color:var(--amber); padding:7px 20px;
+ text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
+ .page-nav a:hover { background:var(--amber); color:var(--bg); border-color:var(--amber); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:var(--bg2); border-top:2px solid var(--amber); }
+ .post { background:var(--bg); border:1px solid var(--dim); padding:16px 18px;
+ margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
+ .post:hover { border-color:var(--amber); box-shadow:0 0 8px rgba(255,176,0,0.25); }
+ .post-active { border-color:var(--amber) !important;
+ background:rgba(255,176,0,0.04) !important;
+ box-shadow:0 0 14px rgba(255,176,0,0.3),inset 3px 0 0 var(--amber) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.85rem; }
+ .post-time { color:var(--dim); font-size:0.78rem; }
+ .post-text { line-height:1.6; font-size:0.88rem; }
+ .post-text a { color:var(--amber); text-decoration:underline; }
+ .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim);
+ filter:sepia(60%) hue-rotate(-10deg); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(0,0,0,0.97); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:740px; margin:0 auto; background:var(--bg);
+ border:1px solid var(--amber); padding:36px;
+ box-shadow:0 0 40px rgba(255,176,0,0.2); }
+ .modal-close { float:right; background:none; border:none; color:var(--dim);
+ font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
+ [data-sno-theme="retro"] .splash-overlay { background: var(--bg); font-family:'Courier New',monospace; }
+ [data-sno-theme="retro"]::after {
+ content:''; position:absolute; inset:0; pointer-events:none; opacity:0.35;
+ background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px);
+ }
+ [data-sno-theme="retro"] .splash-inner { position:relative; z-index:1; }
+ [data-sno-theme="retro"] .splash-title {
+ font-size:clamp(1.15rem,3.8vw,1.55rem); color:var(--amber);
+ text-shadow:0 0 14px var(--amber); letter-spacing:0.3em;
+ animation: splashRetroFlicker 4s ease-in-out infinite;
+ }
+ @keyframes splashRetroFlicker { 0%,100%{opacity:1} 50%{opacity:0.92} }
+ [data-sno-theme="retro"] .splash-tag { color:#d4a020; }
+ [data-sno-theme="retro"] .splash-hint { color:#c99528; }
+ [data-sno-theme="retro"] .splash-inner { text-shadow: 0 0 10px #000, 0 2px 8px #000; }
diff --git a/internal/generator/templates/themes/retro.tmpl b/internal/generator/templates/themes/retro/theme.js
index d6487c2..69d72b1 100644
--- a/internal/generator/templates/themes/retro.tmpl
+++ b/internal/generator/templates/themes/retro/theme.js
@@ -1,104 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SNONUX.FOO // RETRO</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --amber:#ffb000; --dim:#7a5200; --bg:#0a0800; --bg2:#050300; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--amber);
- overflow:hidden; height:100vh; }
- /* Phosphor scanlines overlay — sits above WebGL */
- body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
- background:repeating-linear-gradient(0deg,transparent,transparent 2px,
- rgba(0,0,0,0.15) 2px,rgba(0,0,0,0.15) 4px); }
- /* Subtle glow flicker */
- @keyframes amber-flicker { 0%,100%{opacity:1} 94%{opacity:0.98} 96%{opacity:0.93} }
- body { animation:amber-flicker 11s infinite; }
- /* WebGL background canvas */
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--amber);
- display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:1.6rem; color:var(--amber); text-shadow:0 0 14px var(--amber);
- letter-spacing:2px; }
- .logo-title h1 { font-size:1.2rem; color:var(--amber); text-shadow:0 0 10px var(--amber);
- letter-spacing:4px; font-weight:normal; }
- .logo-title .subtitle { font-size:0.72rem; color:var(--dim); margin-top:2px; }
- .logo-title .subtitle a { color:var(--amber); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--amber); }
- .transmit-btn { border:1px solid var(--amber); color:var(--amber); padding:8px 18px;
- text-decoration:none; font-size:0.82rem; letter-spacing:2px;
- transition:all 0.1s; }
- .transmit-btn:hover { background:var(--amber); color:var(--bg); }
- a.header-feed-link { color:var(--dim); }
- a.header-feed-link:hover { color:var(--amber); text-shadow:0 0 6px var(--amber); }
- .nav-hints { background:var(--bg2); border-bottom:1px solid var(--dim); color:var(--dim);
- padding:4px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--amber);
- padding:0 5px; font-size:0.68rem; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:14px 24px;
- scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:12px 0; }
- .page-nav a { border:1px solid var(--dim); color:var(--amber); padding:7px 20px;
- text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
- .page-nav a:hover { background:var(--amber); color:var(--bg); border-color:var(--amber); }
- .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
- background:var(--bg2); border-top:2px solid var(--amber); }
- .post { background:var(--bg); border:1px solid var(--dim); padding:16px 18px;
- margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
- .post:hover { border-color:var(--amber); box-shadow:0 0 8px rgba(255,176,0,0.25); }
- .post-active { border-color:var(--amber) !important;
- background:rgba(255,176,0,0.04) !important;
- box-shadow:0 0 14px rgba(255,176,0,0.3),inset 3px 0 0 var(--amber) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.85rem; }
- .post-time { color:var(--dim); font-size:0.78rem; }
- .post-text { line-height:1.6; font-size:0.88rem; }
- .post-text a { color:var(--amber); text-decoration:underline; }
- .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim);
- filter:sepia(60%) hue-rotate(-10deg); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(0,0,0,0.97); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:740px; margin:0 auto; background:var(--bg);
- border:1px solid var(--amber); padding:36px;
- box-shadow:0 0 40px rgba(255,176,0,0.2); }
- .modal-close { float:right; background:none; border:none; color:var(--dim);
- font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
- .splash-overlay.splash-retro { background: var(--bg); font-family:'Courier New',monospace; }
- .splash-retro::after {
- content:''; position:absolute; inset:0; pointer-events:none; opacity:0.35;
- background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px);
- }
- .splash-retro .splash-inner { position:relative; z-index:1; }
- .splash-retro .splash-title {
- font-size:clamp(1.15rem,3.8vw,1.55rem); color:var(--amber);
- text-shadow:0 0 14px var(--amber); letter-spacing:0.3em;
- animation: splashRetroFlicker 4s ease-in-out infinite;
- }
- @keyframes splashRetroFlicker { 0%,100%{opacity:1} 50%{opacity:0.92} }
- .splash-retro .splash-tag { color:#d4a020; }
- .splash-retro .splash-hint { color:#c99528; }
- .splash-retro .splash-inner { text-shadow: 0 0 10px #000, 0 2px 8px #000; }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-retro" 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-inner">
- <div class="splash-title">*** SNONUX BBS ***</div>
- <div class="splash-tag">Amber phosphor mode</div>
- <div class="splash-hint">Press Enter or click to connect</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -115,46 +15,8 @@
function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.x=t*0.44;g.rotation.y=t*0.71;oc.rotation.z=t*0.9;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 / <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">TRANSMIT</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}}">&lt;-- NEWER</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
+
+
// Retro WebGL scene: amber demo-scene cube + orbiting octahedrons + star particles.
// Evokes classic 80s/90s PC demo aesthetics with amber phosphor colours.
(function() {
@@ -303,7 +165,3 @@
setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 20);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/retrofuture.tmpl b/internal/generator/templates/themes/retrofuture.tmpl
deleted file mode 100644
index f1b1fb7..0000000
--- a/internal/generator/templates/themes/retrofuture.tmpl
+++ /dev/null
@@ -1,374 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ◈ RETROFUTURE</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&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 { --pink:#ff6b9d; --purple:#00d9c0; --orange:#ff8c42; --bg:#0a0121; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Share Tech Mono',monospace; background:var(--bg);
- color:#f0efe4; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(10,1,33,0.85); backdrop-filter:blur(12px);
- border-bottom:2px solid var(--pink); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:1.8rem; font-family:'Orbitron',sans-serif; color:var(--purple);
- text-shadow:0 0 12px var(--purple),0 0 28px rgba(0,217,192,0.4); }
- .logo-title h1 { font-size:1.7rem; font-family:'Orbitron',sans-serif; color:#f0efe4;
- letter-spacing:3px; text-shadow:0 0 8px rgba(255,255,255,0.2); }
- .logo-title .subtitle { font-size:0.7rem; color:rgba(240,239,228,0.55); margin-top:2px;
- font-family:'Share Tech Mono',monospace; }
- .logo-title .subtitle a { color:var(--pink); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--pink); }
- .logo-title .logo-host { font-size:0.65rem; color:rgba(0,217,192,0.6); margin-top:2px;
- font-family:'Share Tech Mono',monospace; }
- .transmit-btn { border:2px solid var(--orange); color:var(--orange); padding:10px 22px;
- border-radius:22px; text-decoration:none; letter-spacing:1px;
- font-size:0.88rem; font-family:'Orbitron',sans-serif; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--orange); color:var(--bg); box-shadow:0 0 18px rgba(255,140,66,0.5); }
- a.header-feed-link { color:var(--pink); font-family:'Share Tech Mono',monospace; }
- .nav-hints { background:rgba(10,1,33,0.75); border-bottom:1px solid rgba(0,217,192,0.25);
- color:rgba(240,239,228,0.45); padding:5px 20px; display:flex; gap:18px;
- font-size:0.68rem; font-family:'Share Tech Mono',monospace; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(0,217,192,0.12); border:1px solid rgba(0,217,192,0.45);
- color:var(--purple); border-radius:3px; padding:0 5px; margin:0 2px; font-size:0.7rem; }
- .content { flex:1; overflow-y:auto; padding:22px 28px;
- scrollbar-width:thin; scrollbar-color:var(--purple) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:2px solid var(--pink); color:var(--pink); padding:8px 22px;
- border-radius:22px; text-decoration:none; letter-spacing:2px; font-size:0.82rem;
- font-family:'Orbitron',sans-serif; transition:all 0.2s; }
- .page-nav a:hover { background:var(--pink); color:var(--bg); }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(10,1,33,0.82); backdrop-filter:blur(10px);
- border-top:2px solid var(--pink); }
- .post { background:rgba(20,10,55,0.85); border:1px solid rgba(0,217,192,0.3);
- border-radius:12px; padding:22px; margin-bottom:18px; cursor:pointer; transition:all 0.25s;
- box-shadow:0 2px 16px rgba(0,0,0,0.4); }
- .post:hover { border-color:var(--pink); box-shadow:0 0 22px rgba(255,107,157,0.3),0 4px 24px rgba(0,0,0,0.5); transform:translateY(-3px); }
- .post-active { border-color:var(--orange) !important; background:rgba(30,15,60,0.96) !important;
- box-shadow:0 0 22px rgba(255,140,66,0.4),inset 3px 0 0 var(--orange) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:14px; }
- .post-time { color:var(--orange); font-family:'Share Tech Mono',monospace; font-size:0.85rem; }
- .post-text { line-height:1.6; font-size:0.95rem; font-family:'Share Tech Mono',monospace; }
- .post-text a { color:var(--pink); text-decoration:none; }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(10,1,33,0.96); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:780px; margin:0 auto; background:rgba(20,10,55,0.98);
- border:2px solid var(--pink); border-radius:12px;
- box-shadow:0 0 60px rgba(255,107,157,0.25); padding:38px; }
- .modal-close { float:right; background:none; border:none; color:var(--orange);
- font-family:'Orbitron',sans-serif; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} }
- .splash-overlay.splash-retrofuture {
- background: radial-gradient(ellipse at 50% 60%, #1a0840 0%, var(--bg) 55%, #050010 100%);
- }
- .splash-retrofuture .splash-starburst {
- position:absolute; inset:0; pointer-events:none; z-index:1;
- }
- .splash-retrofuture .splash-starburst span {
- position:absolute; top:50%; left:50%; width:3px; height:3px;
- border-radius:50%; background:var(--pink); opacity:0.5;
- box-shadow:0 0 6px var(--pink);
- animation: starTwinkle 3s ease-in-out infinite;
- }
- @keyframes starTwinkle {
- 0%,100%{opacity:0.3;transform:scale(1);}
- 50%{opacity:0.8;transform:scale(1.4);}
- }
- .splash-retrofuture .splash-atomic {
- width:min(100px,22vw); height:min(100px,22vw); margin:0 auto 1rem;
- border-radius:50%; border:3px solid var(--purple);
- box-shadow:0 0 20px var(--purple),0 0 40px rgba(0,217,192,0.3),inset 0 0 20px rgba(0,217,192,0.15);
- position:relative; animation:splashAtomicPulse 2.5s ease-in-out infinite alternate;
- }
- .splash-retrofuture .splash-atomic::before,
- .splash-retrofuture .splash-atomic::after {
- content:''; position:absolute; border:2px solid var(--purple); border-radius:50%;
- top:50%; left:50%; transform:translate(-50%,-50%);
- }
- .splash-retrofuture .splash-atomic::before {
- width:160%; height:30%; opacity:0.7;
- box-shadow:0 0 10px var(--purple);
- }
- .splash-retrofuture .splash-atomic::after {
- width:30%; height:160%; opacity:0.7;
- box-shadow:0 0 10px var(--purple);
- }
- @keyframes splashAtomicPulse {
- from{transform:scale(0.95);box-shadow:0 0 15px var(--purple),0 0 30px rgba(0,217,192,0.2),inset 0 0 15px rgba(0,217,192,0.1);}
- to{transform:scale(1.05);box-shadow:0 0 25px var(--purple),0 0 50px rgba(0,217,192,0.4),inset 0 0 25px rgba(0,217,192,0.2);}
- }
- .splash-retrofuture .splash-title {
- font-family:'Orbitron',sans-serif; font-size:clamp(1.4rem,4.5vw,2rem);
- color:#f0efe4; letter-spacing:4px; text-shadow:0 0 20px rgba(255,107,157,0.6);
- }
- .splash-retrofuture .splash-tag { font-family:'Share Tech Mono',monospace; color:var(--purple); }
- .splash-retrofuture .splash-hint { font-family:'Share Tech Mono',monospace; color:rgba(240,239,228,0.8); }
- .splash-retrofuture .splash-inner { text-shadow:0 2px 24px rgba(10,1,33,0.9); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-retrofuture" 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-starburst" aria-hidden="true">
- <span style="top:15%;left:20%;animation-delay:0s"></span>
- <span style="top:25%;left:75%;animation-delay:0.4s"></span>
- <span style="top:60%;left:10%;animation-delay:0.8s"></span>
- <span style="top:70%;left:85%;animation-delay:1.2s"></span>
- <span style="top:40%;left:50%;animation-delay:1.6s"></span>
- <span style="top:80%;left:35%;animation-delay:0.2s"></span>
- <span style="top:10%;left:60%;animation-delay:1.0s"></span>
- <span style="top:55%;left:90%;animation-delay:0.6s"></span>
- </div>
- <div class="splash-inner">
- <div class="splash-atomic" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Atomic age uplink</div>
- <div class="splash-hint">Click or Enter to tune in</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,g=new THREE.Group(),t0=performance.now();
- 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(58,1,0.1,120);ca.position.set(0,1.2,7);
- var atomic=new THREE.Mesh(new THREE.SphereGeometry(1.35,28,28),new THREE.MeshBasicMaterial({color:0x00d9c0,transparent:true,opacity:0.9}));
- atomic.position.y=0;g.add(atomic);
- var ringH=new THREE.Mesh(new THREE.TorusGeometry(2.1,0.06,8,64),new THREE.MeshBasicMaterial({color:0xff6b9d,transparent:true,opacity:0.8}));
- g.add(ringH);
- var ringV=new THREE.Mesh(new THREE.TorusGeometry(2.1,0.06,8,64),new THREE.MeshBasicMaterial({color:0xff6b9d,transparent:true,opacity:0.8}));
- ringV.rotation.x=Math.PI/2;g.add(ringV);
- var gr=new THREE.Mesh(new THREE.PlaneGeometry(28,28,20,20),new THREE.MeshBasicMaterial({color:0x00d9c0,wireframe:true,transparent:true,opacity:0.25}));
- gr.rotation.x=-Math.PI/2.4;gr.position.y=-2.8;g.add(gr);
- sc.add(g);sz();window.addEventListener('resize',sz);
- function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.y=Math.sin(t*0.3)*0.12;atomic.position.y=Math.sin(t*1.1)*0.12;ringH.scale.setScalar(1+Math.sin(t*2.2)*0.06);ringV.scale.setScalar(1+Math.cos(t*2.2)*0.06);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">TRANSMIT TO NEXUS</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>
- // Retrofuture WebGL: atomic orb with crossed electron rings, receding teal grid floor,
- // chrome metallic star particles, and a slow drifting camera orbit.
- (function() {
- var _wild = false, _snoTOffset = 0, _snoLastT = 0;
- var scene, camera, renderer, clock;
- var atomic, ringH, ringV, stars;
-
- function initThree() {
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x0a0121);
- scene.fog = new THREE.Fog(0x0a0121, 60, 180);
-
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 300);
- camera.position.set(0, 10, 45);
- camera.lookAt(0, -5, -10);
-
- 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();
-
- // Glowing atomic teal orb
- atomic = new THREE.Mesh(
- new THREE.SphereGeometry(12, 32, 16),
- new THREE.MeshBasicMaterial({ color: 0x00d9c0 })
- );
- atomic.position.set(0, -8, -55);
- scene.add(atomic);
-
- // Horizontal electron ring — pink
- ringH = new THREE.Mesh(
- new THREE.TorusGeometry(15, 0.15, 8, 64),
- new THREE.MeshBasicMaterial({ color: 0xff6b9d })
- );
- ringH.position.copy(atomic.position);
- scene.add(ringH);
-
- // Vertical electron ring — coral
- ringV = new THREE.Mesh(
- new THREE.TorusGeometry(15, 0.15, 8, 64),
- new THREE.MeshBasicMaterial({ color: 0xff8c42 })
- );
- ringV.position.copy(atomic.position);
- ringV.rotation.x = Math.PI / 2;
- scene.add(ringV);
-
- // Receding grid floor — teal
- var grid = new THREE.GridHelper(200, 40, 0x00d9c0, 0x1a0840);
- grid.position.set(0, -18, -30);
- scene.add(grid);
-
- // 1200 chrome star particles scattered in a sphere shell
- var starPos = new Float32Array(1200 * 3);
- for (var j = 0; j < 1200 * 3; j += 3) {
- var r = 80 + Math.random() * 40;
- var theta = Math.random() * Math.PI * 2;
- var phi = Math.acos(2 * Math.random() - 1);
- starPos[j] = r * Math.sin(phi) * Math.cos(theta);
- starPos[j+1] = r * Math.sin(phi) * Math.sin(theta);
- starPos[j+2] = r * Math.cos(phi);
- }
- var starGeo = new THREE.BufferGeometry();
- starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
- scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({
- color: 0xffd700, size: 0.22, transparent: true, opacity: 0.8
- })));
-
- 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;
- // Atomic orb pulses; wild meltdown mode makes it throb intensely
- var pulse = _wild ? 1 + 0.1 * Math.sin(t * 12) : 1 + 0.015 * Math.sin(t * 1.5);
- atomic.scale.setScalar(pulse);
- ringH.rotation.z = t * 0.4;
- ringV.rotation.z = -t * 0.3;
- // Rings tilt chaotically in wild — full 3D tumble
- if (_wild) {
- ringH.rotation.x = Math.sin(realT * 2.3) * 0.8;
- ringV.rotation.y = Math.cos(realT * 1.9) * 0.9;
- } else {
- ringH.rotation.x = 0;
- ringV.rotation.y = 0;
- }
- // Camera: meltdown spiral in wild, slow orbit otherwise
- if (_wild) {
- camera.position.x = Math.sin(realT * 0.36) * 16;
- camera.position.y = 10 + Math.sin(realT * 0.29) * 10;
- camera.position.z = 45 + Math.sin(realT * 0.22) * 16;
- camera.fov = 60 + Math.sin(realT * 0.47) * 18;
- camera.updateProjectionMatrix();
- } else {
- camera.position.x = Math.sin(t * 0.07) * 5;
- camera.position.y = 10 + Math.sin(t * 0.05) * 2;
- camera.position.z = 45;
- if (camera.fov !== 60) { camera.fov = 60; camera.updateProjectionMatrix(); }
- }
- camera.lookAt(0, -5, -10);
- renderer.render(scene, camera);
- }
-
- initThree();
-
- // Retrofuture nav/wild effects — atomic pulse on navigate, meltdown on wild
- window.snonuxOpenEffect = function(post) {
- // Atomic rings expand outward from post — zoom into modal
- var modal = document.getElementById('post-modal');
- if (modal) { modal.classList.add('sno-modal-zoom'); setTimeout(function() { modal.classList.remove('sno-modal-zoom'); }, 400); }
- var r = post ? post.getBoundingClientRect() : {left: window.innerWidth/2, top: window.innerHeight/2, width: 0, height: 0};
- [0, 80, 160].forEach(function(delay) {
- var ring = document.createElement('div');
- ring.style.cssText = 'position:fixed;top:' + (r.top+r.height/2-6) + 'px;left:' + (r.left+r.width/2-6) + 'px;z-index:997;pointer-events:none;width:12px;height:12px;border-radius:50%;border:2px solid rgba(0,217,192,0.7);transition:all 0.42s ease,opacity 0.42s';
- document.body.appendChild(ring);
- setTimeout(function() { ring.style.transform='scale(25)'; ring.style.opacity='0'; setTimeout(function() { ring.remove(); }, 460); }, delay + 15);
- });
- };
- window.snonuxCloseEffect = function() {
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,217,192,0.1);transition:opacity 0.2s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 15);
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- // Retrofuture: atomic orange-gold sweep
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,transparent,rgba(255,140,0,0.9),rgba(255,80,0,0.9),rgba(255,140,0,0.9),transparent);' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.3s ease,opacity 0.3s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? '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);
- };
- window.snonuxNavEffect = function() {
- // Atomic pulse — rings expand briefly as CSS overlay
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.1);z-index:998;pointer-events:none;width:100vmax;height:100vmax;border-radius:50%;border:3px solid rgba(0,217,192,0.7);transition:transform 0.3s ease,opacity 0.3s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform='translate(-50%,-50%) scale(1.2)'; d.style.opacity='0'; setTimeout(function() { d.remove(); }, 330); }, 15);
- };
- window.snonuxPageEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,217,192,0.15);transition:opacity 0.2s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 20);
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html> \ No newline at end of file
diff --git a/internal/generator/templates/themes/retrofuture/meta.json b/internal/generator/templates/themes/retrofuture/meta.json
new file mode 100644
index 0000000..e41fdc5
--- /dev/null
+++ b/internal/generator/templates/themes/retrofuture/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ◈ RETROFUTURE",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT TO NEXUS\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-starburst\" aria-hidden=\"true\"\u003e\n \u003cspan style=\"top:15%;left:20%;animation-delay:0s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:25%;left:75%;animation-delay:0.4s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:60%;left:10%;animation-delay:0.8s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:70%;left:85%;animation-delay:1.2s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:40%;left:50%;animation-delay:1.6s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:80%;left:35%;animation-delay:0.2s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:10%;left:60%;animation-delay:1.0s\"\u003e\u003c/span\u003e\n \u003cspan style=\"top:55%;left:90%;animation-delay:0.6s\"\u003e\u003c/span\u003e\n \u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-atomic\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eAtomic age uplink\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to tune in\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; NEWER",
+ "next_page_text": "OLDER \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/retrofuture/theme.css b/internal/generator/templates/themes/retrofuture/theme.css
new file mode 100644
index 0000000..6cc010f
--- /dev/null
+++ b/internal/generator/templates/themes/retrofuture/theme.css
@@ -0,0 +1,105 @@
+ :root { --pink:#ff6b9d; --purple:#00d9c0; --orange:#ff8c42; --bg:#0a0121; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Share Tech Mono',monospace; background:var(--bg);
+ color:#f0efe4; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(10,1,33,0.85); backdrop-filter:blur(12px);
+ border-bottom:2px solid var(--pink); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.8rem; font-family:'Orbitron',sans-serif; color:var(--purple);
+ text-shadow:0 0 12px var(--purple),0 0 28px rgba(0,217,192,0.4); }
+ .logo-title h1 { font-size:1.7rem; font-family:'Orbitron',sans-serif; color:#f0efe4;
+ letter-spacing:3px; text-shadow:0 0 8px rgba(255,255,255,0.2); }
+ .logo-title .subtitle { font-size:0.7rem; color:rgba(240,239,228,0.55); margin-top:2px;
+ font-family:'Share Tech Mono',monospace; }
+ .logo-title .subtitle a { color:var(--pink); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--pink); }
+ .logo-title .logo-host { font-size:0.65rem; color:rgba(0,217,192,0.6); margin-top:2px;
+ font-family:'Share Tech Mono',monospace; }
+ .transmit-btn { border:2px solid var(--orange); color:var(--orange); padding:10px 22px;
+ border-radius:22px; text-decoration:none; letter-spacing:1px;
+ font-size:0.88rem; font-family:'Orbitron',sans-serif; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--orange); color:var(--bg); box-shadow:0 0 18px rgba(255,140,66,0.5); }
+ a.header-feed-link { color:var(--pink); font-family:'Share Tech Mono',monospace; }
+ .nav-hints { background:rgba(10,1,33,0.75); border-bottom:1px solid rgba(0,217,192,0.25);
+ color:rgba(240,239,228,0.45); padding:5px 20px; display:flex; gap:18px;
+ font-size:0.68rem; font-family:'Share Tech Mono',monospace; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(0,217,192,0.12); border:1px solid rgba(0,217,192,0.45);
+ color:var(--purple); border-radius:3px; padding:0 5px; margin:0 2px; font-size:0.7rem; }
+ .content { flex:1; overflow-y:auto; padding:22px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--purple) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:2px solid var(--pink); color:var(--pink); padding:8px 22px;
+ border-radius:22px; text-decoration:none; letter-spacing:2px; font-size:0.82rem;
+ font-family:'Orbitron',sans-serif; transition:all 0.2s; }
+ .page-nav a:hover { background:var(--pink); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(10,1,33,0.82); backdrop-filter:blur(10px);
+ border-top:2px solid var(--pink); }
+ .post { background:rgba(20,10,55,0.85); border:1px solid rgba(0,217,192,0.3);
+ border-radius:12px; padding:22px; margin-bottom:18px; cursor:pointer; transition:all 0.25s;
+ box-shadow:0 2px 16px rgba(0,0,0,0.4); }
+ .post:hover { border-color:var(--pink); box-shadow:0 0 22px rgba(255,107,157,0.3),0 4px 24px rgba(0,0,0,0.5); transform:translateY(-3px); }
+ .post-active { border-color:var(--orange) !important; background:rgba(30,15,60,0.96) !important;
+ box-shadow:0 0 22px rgba(255,140,66,0.4),inset 3px 0 0 var(--orange) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:14px; }
+ .post-time { color:var(--orange); font-family:'Share Tech Mono',monospace; font-size:0.85rem; }
+ .post-text { line-height:1.6; font-size:0.95rem; font-family:'Share Tech Mono',monospace; }
+ .post-text a { color:var(--pink); text-decoration:none; }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(10,1,33,0.96); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:780px; margin:0 auto; background:rgba(20,10,55,0.98);
+ border:2px solid var(--pink); border-radius:12px;
+ box-shadow:0 0 60px rgba(255,107,157,0.25); padding:38px; }
+ .modal-close { float:right; background:none; border:none; color:var(--orange);
+ font-family:'Orbitron',sans-serif; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} }
+ [data-sno-theme="retrofuture"] .splash-overlay {
+ background: radial-gradient(ellipse at 50% 60%, #1a0840 0%, var(--bg) 55%, #050010 100%);
+ }
+ [data-sno-theme="retrofuture"] .splash-starburst {
+ position:absolute; inset:0; pointer-events:none; z-index:1;
+ }
+ [data-sno-theme="retrofuture"] .splash-starburst span {
+ position:absolute; top:50%; left:50%; width:3px; height:3px;
+ border-radius:50%; background:var(--pink); opacity:0.5;
+ box-shadow:0 0 6px var(--pink);
+ animation: starTwinkle 3s ease-in-out infinite;
+ }
+ @keyframes starTwinkle {
+ 0%,100%{opacity:0.3;transform:scale(1);}
+ 50%{opacity:0.8;transform:scale(1.4);}
+ }
+ [data-sno-theme="retrofuture"] .splash-atomic {
+ width:min(100px,22vw); height:min(100px,22vw); margin:0 auto 1rem;
+ border-radius:50%; border:3px solid var(--purple);
+ box-shadow:0 0 20px var(--purple),0 0 40px rgba(0,217,192,0.3),inset 0 0 20px rgba(0,217,192,0.15);
+ position:relative; animation:splashAtomicPulse 2.5s ease-in-out infinite alternate;
+ }
+ [data-sno-theme="retrofuture"] .splash-atomic::before,
+ [data-sno-theme="retrofuture"] .splash-atomic::after {
+ content:''; position:absolute; border:2px solid var(--purple); border-radius:50%;
+ top:50%; left:50%; transform:translate(-50%,-50%);
+ }
+ [data-sno-theme="retrofuture"] .splash-atomic::before {
+ width:160%; height:30%; opacity:0.7;
+ box-shadow:0 0 10px var(--purple);
+ }
+ [data-sno-theme="retrofuture"] .splash-atomic::after {
+ width:30%; height:160%; opacity:0.7;
+ box-shadow:0 0 10px var(--purple);
+ }
+ @keyframes splashAtomicPulse {
+ from{transform:scale(0.95);box-shadow:0 0 15px var(--purple),0 0 30px rgba(0,217,192,0.2),inset 0 0 15px rgba(0,217,192,0.1);}
+ to{transform:scale(1.05);box-shadow:0 0 25px var(--purple),0 0 50px rgba(0,217,192,0.4),inset 0 0 25px rgba(0,217,192,0.2);}
+ }
+ [data-sno-theme="retrofuture"] .splash-title {
+ font-family:'Orbitron',sans-serif; font-size:clamp(1.4rem,4.5vw,2rem);
+ color:#f0efe4; letter-spacing:4px; text-shadow:0 0 20px rgba(255,107,157,0.6);
+ }
+ [data-sno-theme="retrofuture"] .splash-tag { font-family:'Share Tech Mono',monospace; color:var(--purple); }
+ [data-sno-theme="retrofuture"] .splash-hint { font-family:'Share Tech Mono',monospace; color:rgba(240,239,228,0.8); }
+ [data-sno-theme="retrofuture"] .splash-inner { text-shadow:0 2px 24px rgba(10,1,33,0.9); }
diff --git a/internal/generator/templates/themes/retrofuture/theme.js b/internal/generator/templates/themes/retrofuture/theme.js
new file mode 100644
index 0000000..057de32
--- /dev/null
+++ b/internal/generator/templates/themes/retrofuture/theme.js
@@ -0,0 +1,193 @@
+
+ (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,g=new THREE.Group(),t0=performance.now();
+ 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(58,1,0.1,120);ca.position.set(0,1.2,7);
+ var atomic=new THREE.Mesh(new THREE.SphereGeometry(1.35,28,28),new THREE.MeshBasicMaterial({color:0x00d9c0,transparent:true,opacity:0.9}));
+ atomic.position.y=0;g.add(atomic);
+ var ringH=new THREE.Mesh(new THREE.TorusGeometry(2.1,0.06,8,64),new THREE.MeshBasicMaterial({color:0xff6b9d,transparent:true,opacity:0.8}));
+ g.add(ringH);
+ var ringV=new THREE.Mesh(new THREE.TorusGeometry(2.1,0.06,8,64),new THREE.MeshBasicMaterial({color:0xff6b9d,transparent:true,opacity:0.8}));
+ ringV.rotation.x=Math.PI/2;g.add(ringV);
+ var gr=new THREE.Mesh(new THREE.PlaneGeometry(28,28,20,20),new THREE.MeshBasicMaterial({color:0x00d9c0,wireframe:true,transparent:true,opacity:0.25}));
+ gr.rotation.x=-Math.PI/2.4;gr.position.y=-2.8;g.add(gr);
+ sc.add(g);sz();window.addEventListener('resize',sz);
+ function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.y=Math.sin(t*0.3)*0.12;atomic.position.y=Math.sin(t*1.1)*0.12;ringH.scale.setScalar(1+Math.sin(t*2.2)*0.06);ringV.scale.setScalar(1+Math.cos(t*2.2)*0.06);ren.render(sc,ca);}
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ // Retrofuture WebGL: atomic orb with crossed electron rings, receding teal grid floor,
+ // chrome metallic star particles, and a slow drifting camera orbit.
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock;
+ var atomic, ringH, ringV, stars;
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x0a0121);
+ scene.fog = new THREE.Fog(0x0a0121, 60, 180);
+
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 300);
+ camera.position.set(0, 10, 45);
+ camera.lookAt(0, -5, -10);
+
+ 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();
+
+ // Glowing atomic teal orb
+ atomic = new THREE.Mesh(
+ new THREE.SphereGeometry(12, 32, 16),
+ new THREE.MeshBasicMaterial({ color: 0x00d9c0 })
+ );
+ atomic.position.set(0, -8, -55);
+ scene.add(atomic);
+
+ // Horizontal electron ring — pink
+ ringH = new THREE.Mesh(
+ new THREE.TorusGeometry(15, 0.15, 8, 64),
+ new THREE.MeshBasicMaterial({ color: 0xff6b9d })
+ );
+ ringH.position.copy(atomic.position);
+ scene.add(ringH);
+
+ // Vertical electron ring — coral
+ ringV = new THREE.Mesh(
+ new THREE.TorusGeometry(15, 0.15, 8, 64),
+ new THREE.MeshBasicMaterial({ color: 0xff8c42 })
+ );
+ ringV.position.copy(atomic.position);
+ ringV.rotation.x = Math.PI / 2;
+ scene.add(ringV);
+
+ // Receding grid floor — teal
+ var grid = new THREE.GridHelper(200, 40, 0x00d9c0, 0x1a0840);
+ grid.position.set(0, -18, -30);
+ scene.add(grid);
+
+ // 1200 chrome star particles scattered in a sphere shell
+ var starPos = new Float32Array(1200 * 3);
+ for (var j = 0; j < 1200 * 3; j += 3) {
+ var r = 80 + Math.random() * 40;
+ var theta = Math.random() * Math.PI * 2;
+ var phi = Math.acos(2 * Math.random() - 1);
+ starPos[j] = r * Math.sin(phi) * Math.cos(theta);
+ starPos[j+1] = r * Math.sin(phi) * Math.sin(theta);
+ starPos[j+2] = r * Math.cos(phi);
+ }
+ var starGeo = new THREE.BufferGeometry();
+ starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
+ scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({
+ color: 0xffd700, size: 0.22, transparent: true, opacity: 0.8
+ })));
+
+ 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;
+ // Atomic orb pulses; wild meltdown mode makes it throb intensely
+ var pulse = _wild ? 1 + 0.1 * Math.sin(t * 12) : 1 + 0.015 * Math.sin(t * 1.5);
+ atomic.scale.setScalar(pulse);
+ ringH.rotation.z = t * 0.4;
+ ringV.rotation.z = -t * 0.3;
+ // Rings tilt chaotically in wild — full 3D tumble
+ if (_wild) {
+ ringH.rotation.x = Math.sin(realT * 2.3) * 0.8;
+ ringV.rotation.y = Math.cos(realT * 1.9) * 0.9;
+ } else {
+ ringH.rotation.x = 0;
+ ringV.rotation.y = 0;
+ }
+ // Camera: meltdown spiral in wild, slow orbit otherwise
+ if (_wild) {
+ camera.position.x = Math.sin(realT * 0.36) * 16;
+ camera.position.y = 10 + Math.sin(realT * 0.29) * 10;
+ camera.position.z = 45 + Math.sin(realT * 0.22) * 16;
+ camera.fov = 60 + Math.sin(realT * 0.47) * 18;
+ camera.updateProjectionMatrix();
+ } else {
+ camera.position.x = Math.sin(t * 0.07) * 5;
+ camera.position.y = 10 + Math.sin(t * 0.05) * 2;
+ camera.position.z = 45;
+ if (camera.fov !== 60) { camera.fov = 60; camera.updateProjectionMatrix(); }
+ }
+ camera.lookAt(0, -5, -10);
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ // Retrofuture nav/wild effects — atomic pulse on navigate, meltdown on wild
+ window.snonuxOpenEffect = function(post) {
+ // Atomic rings expand outward from post — zoom into modal
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-zoom'); setTimeout(function() { modal.classList.remove('sno-modal-zoom'); }, 400); }
+ var r = post ? post.getBoundingClientRect() : {left: window.innerWidth/2, top: window.innerHeight/2, width: 0, height: 0};
+ [0, 80, 160].forEach(function(delay) {
+ var ring = document.createElement('div');
+ ring.style.cssText = 'position:fixed;top:' + (r.top+r.height/2-6) + 'px;left:' + (r.left+r.width/2-6) + 'px;z-index:997;pointer-events:none;width:12px;height:12px;border-radius:50%;border:2px solid rgba(0,217,192,0.7);transition:all 0.42s ease,opacity 0.42s';
+ document.body.appendChild(ring);
+ setTimeout(function() { ring.style.transform='scale(25)'; ring.style.opacity='0'; setTimeout(function() { ring.remove(); }, 460); }, delay + 15);
+ });
+ };
+ window.snonuxCloseEffect = function() {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,217,192,0.1);transition:opacity 0.2s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 15);
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ // Retrofuture: atomic orange-gold sweep
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,transparent,rgba(255,140,0,0.9),rgba(255,80,0,0.9),rgba(255,140,0,0.9),transparent);' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.3s ease,opacity 0.3s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? '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);
+ };
+ window.snonuxNavEffect = function() {
+ // Atomic pulse — rings expand briefly as CSS overlay
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.1);z-index:998;pointer-events:none;width:100vmax;height:100vmax;border-radius:50%;border:3px solid rgba(0,217,192,0.7);transition:transform 0.3s ease,opacity 0.3s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform='translate(-50%,-50%) scale(1.2)'; d.style.opacity='0'; setTimeout(function() { d.remove(); }, 330); }, 15);
+ };
+ window.snonuxPageEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(0,217,192,0.15);transition:opacity 0.2s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 20);
+ };
+ })();
diff --git a/internal/generator/templates/themes/spaceage/meta.json b/internal/generator/templates/themes/spaceage/meta.json
new file mode 100644
index 0000000..008bc8a
--- /dev/null
+++ b/internal/generator/templates/themes/spaceage/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "SNONUX.FOO // SPACE AGE",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003e[SN]\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003eSNONUX.FOO\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003eMICROBLOG / \u003ca href=\"https://foo.zone\"\u003eFOO.ZONE\u003c/a\u003e IS THE REAL BLOG\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003eSTARBASE SNONUX\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eOrbital uplink established\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003ePress Enter or click to dock\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026lt;-- NEWER",
+ "next_page_text": "OLDER --\u0026gt;"
+}
diff --git a/internal/generator/templates/themes/spaceage/theme.css b/internal/generator/templates/themes/spaceage/theme.css
new file mode 100644
index 0000000..b659f55
--- /dev/null
+++ b/internal/generator/templates/themes/spaceage/theme.css
@@ -0,0 +1,80 @@
+ :root { --teal:#00e8e8; --dim:#1a4455; --red:#ff3320; --silver:#c8d8e0; --bg:#030a0f; --bg2:#020608; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Space Mono','Courier New',monospace; background:var(--bg); color:var(--silver);
+ overflow:hidden; height:100vh; }
+ /* Subtle horizontal scanlines — lighter than retro, cleaner space feel */
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
+ background:repeating-linear-gradient(0deg,transparent,transparent 3px,
+ rgba(0,0,0,0.08) 3px,rgba(0,0,0,0.08) 4px); }
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:12px 24px; background:rgba(2,6,8,0.88); backdrop-filter:blur(8px);
+ border-bottom:2px solid var(--teal);
+ display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.5rem; color:var(--teal); text-shadow:0 0 18px var(--teal);
+ letter-spacing:3px; font-weight:700; }
+ .logo-title h1 { font-size:1.1rem; color:var(--teal); text-shadow:0 0 12px var(--teal);
+ letter-spacing:5px; font-weight:700; }
+ .logo-title .subtitle { font-size:0.68rem; color:var(--dim); margin-top:3px; letter-spacing:1px; }
+ .logo-title .subtitle a { color:var(--teal); text-decoration:none; opacity:0.8; }
+ .logo-title .subtitle a:hover { opacity:1; text-shadow:0 0 6px var(--teal); }
+ /* HAL eye: red dot beside the transmit button */
+ .transmit-btn { position:relative; border:1px solid var(--teal); color:var(--teal); padding:8px 18px;
+ text-decoration:none; font-size:0.78rem; letter-spacing:3px;
+ transition:all 0.15s; }
+ .transmit-btn::before { content:'●'; color:var(--red); text-shadow:0 0 8px var(--red);
+ position:absolute; left:-18px; top:50%; transform:translateY(-50%);
+ font-size:0.65rem; animation:hal-blink 4s ease-in-out infinite; }
+ @keyframes hal-blink { 0%,90%,100%{opacity:1} 95%{opacity:0.2} }
+ .transmit-btn:hover { background:var(--teal); color:var(--bg); }
+ a.header-feed-link { color:var(--dim); font-size:0.78rem; letter-spacing:1px; }
+ a.header-feed-link:hover { color:var(--teal); }
+ .nav-hints { background:rgba(2,6,8,0.82); border-bottom:1px solid var(--dim);
+ color:var(--dim); padding:4px 24px; display:flex; gap:18px;
+ font-size:0.66rem; flex-wrap:wrap; letter-spacing:1px; }
+ .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--teal);
+ padding:0 5px; font-size:0.66rem; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:16px 24px;
+ scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:12px 0; }
+ .page-nav a { border:1px solid var(--dim); color:var(--teal); padding:7px 22px;
+ text-decoration:none; font-size:0.78rem; letter-spacing:3px; }
+ .page-nav a:hover { background:var(--teal); color:var(--bg); border-color:var(--teal); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:rgba(2,6,8,0.88); backdrop-filter:blur(8px); border-top:2px solid var(--teal); }
+ .post { background:rgba(3,10,15,0.82); border:1px solid var(--dim); padding:16px 18px;
+ margin-bottom:10px; cursor:pointer; transition:border-color 0.2s, box-shadow 0.2s; }
+ .post:hover { border-color:var(--teal); box-shadow:0 0 12px rgba(0,232,232,0.18); }
+ .post-active { border-color:var(--teal) !important;
+ background:rgba(0,232,232,0.04) !important;
+ box-shadow:0 0 18px rgba(0,232,232,0.28),inset 3px 0 0 var(--teal) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.82rem; }
+ .post-time { color:var(--dim); font-size:0.75rem; letter-spacing:1px; }
+ .post-text { line-height:1.65; font-size:0.86rem; color:var(--silver); }
+ .post-text a { color:var(--teal); text-decoration:underline; }
+ .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim);
+ filter:saturate(0.85) brightness(0.92); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(2,6,8,0.97); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:740px; margin:0 auto; background:var(--bg);
+ border:1px solid var(--teal); padding:36px;
+ box-shadow:0 0 50px rgba(0,232,232,0.18); }
+ .modal-close { float:right; background:none; border:none; color:var(--dim);
+ font-family:'Space Mono',monospace; font-size:0.85rem; cursor:pointer; letter-spacing:3px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
+ /* Splash screen: space age orbital */
+ [data-sno-theme="spaceage"] .splash-overlay { background:var(--bg); }
+ [data-sno-theme="spaceage"] .splash-inner { position:relative; z-index:1; }
+ [data-sno-theme="spaceage"] .splash-title {
+ font-family:'Space Mono',monospace; font-weight:700;
+ font-size:clamp(1.1rem,3.5vw,1.6rem); color:var(--teal);
+ text-shadow:0 0 20px var(--teal),0 0 40px rgba(0,232,232,0.4);
+ letter-spacing:0.35em;
+ animation: spaceagePulse 3s ease-in-out infinite;
+ }
+ @keyframes spaceagePulse { 0%,100%{text-shadow:0 0 20px var(--teal),0 0 40px rgba(0,232,232,0.4)} 50%{text-shadow:0 0 30px var(--teal),0 0 60px rgba(0,232,232,0.6)} }
+ [data-sno-theme="spaceage"] .splash-tag { color:var(--silver); letter-spacing:2px; font-family:'Space Mono',monospace; font-size:0.78rem; }
+ [data-sno-theme="spaceage"] .splash-hint { color:var(--dim); letter-spacing:2px; font-family:'Space Mono',monospace; font-size:0.72rem; }
diff --git a/internal/generator/templates/themes/spaceage.tmpl b/internal/generator/templates/themes/spaceage/theme.js
index a71f057..4bcad10 100644
--- a/internal/generator/templates/themes/spaceage.tmpl
+++ b/internal/generator/templates/themes/spaceage/theme.js
@@ -1,107 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SNONUX.FOO // SPACE AGE</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link href="https://fonts.googleapis.com/css2?family=Space+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 { --teal:#00e8e8; --dim:#1a4455; --red:#ff3320; --silver:#c8d8e0; --bg:#030a0f; --bg2:#020608; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Space Mono','Courier New',monospace; background:var(--bg); color:var(--silver);
- overflow:hidden; height:100vh; }
- /* Subtle horizontal scanlines — lighter than retro, cleaner space feel */
- body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
- background:repeating-linear-gradient(0deg,transparent,transparent 3px,
- rgba(0,0,0,0.08) 3px,rgba(0,0,0,0.08) 4px); }
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:12px 24px; background:rgba(2,6,8,0.88); backdrop-filter:blur(8px);
- border-bottom:2px solid var(--teal);
- display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:1.5rem; color:var(--teal); text-shadow:0 0 18px var(--teal);
- letter-spacing:3px; font-weight:700; }
- .logo-title h1 { font-size:1.1rem; color:var(--teal); text-shadow:0 0 12px var(--teal);
- letter-spacing:5px; font-weight:700; }
- .logo-title .subtitle { font-size:0.68rem; color:var(--dim); margin-top:3px; letter-spacing:1px; }
- .logo-title .subtitle a { color:var(--teal); text-decoration:none; opacity:0.8; }
- .logo-title .subtitle a:hover { opacity:1; text-shadow:0 0 6px var(--teal); }
- /* HAL eye: red dot beside the transmit button */
- .transmit-btn { position:relative; border:1px solid var(--teal); color:var(--teal); padding:8px 18px;
- text-decoration:none; font-size:0.78rem; letter-spacing:3px;
- transition:all 0.15s; }
- .transmit-btn::before { content:'●'; color:var(--red); text-shadow:0 0 8px var(--red);
- position:absolute; left:-18px; top:50%; transform:translateY(-50%);
- font-size:0.65rem; animation:hal-blink 4s ease-in-out infinite; }
- @keyframes hal-blink { 0%,90%,100%{opacity:1} 95%{opacity:0.2} }
- .transmit-btn:hover { background:var(--teal); color:var(--bg); }
- a.header-feed-link { color:var(--dim); font-size:0.78rem; letter-spacing:1px; }
- a.header-feed-link:hover { color:var(--teal); }
- .nav-hints { background:rgba(2,6,8,0.82); border-bottom:1px solid var(--dim);
- color:var(--dim); padding:4px 24px; display:flex; gap:18px;
- font-size:0.66rem; flex-wrap:wrap; letter-spacing:1px; }
- .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--teal);
- padding:0 5px; font-size:0.66rem; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:16px 24px;
- scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:12px 0; }
- .page-nav a { border:1px solid var(--dim); color:var(--teal); padding:7px 22px;
- text-decoration:none; font-size:0.78rem; letter-spacing:3px; }
- .page-nav a:hover { background:var(--teal); color:var(--bg); border-color:var(--teal); }
- .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
- background:rgba(2,6,8,0.88); backdrop-filter:blur(8px); border-top:2px solid var(--teal); }
- .post { background:rgba(3,10,15,0.82); border:1px solid var(--dim); padding:16px 18px;
- margin-bottom:10px; cursor:pointer; transition:border-color 0.2s, box-shadow 0.2s; }
- .post:hover { border-color:var(--teal); box-shadow:0 0 12px rgba(0,232,232,0.18); }
- .post-active { border-color:var(--teal) !important;
- background:rgba(0,232,232,0.04) !important;
- box-shadow:0 0 18px rgba(0,232,232,0.28),inset 3px 0 0 var(--teal) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.82rem; }
- .post-time { color:var(--dim); font-size:0.75rem; letter-spacing:1px; }
- .post-text { line-height:1.65; font-size:0.86rem; color:var(--silver); }
- .post-text a { color:var(--teal); text-decoration:underline; }
- .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim);
- filter:saturate(0.85) brightness(0.92); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(2,6,8,0.97); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:740px; margin:0 auto; background:var(--bg);
- border:1px solid var(--teal); padding:36px;
- box-shadow:0 0 50px rgba(0,232,232,0.18); }
- .modal-close { float:right; background:none; border:none; color:var(--dim);
- font-family:'Space Mono',monospace; font-size:0.85rem; cursor:pointer; letter-spacing:3px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:10px 16px;} }
- /* Splash screen: space age orbital */
- .splash-overlay.splash-spaceage { background:var(--bg); }
- .splash-spaceage .splash-inner { position:relative; z-index:1; }
- .splash-spaceage .splash-title {
- font-family:'Space Mono',monospace; font-weight:700;
- font-size:clamp(1.1rem,3.5vw,1.6rem); color:var(--teal);
- text-shadow:0 0 20px var(--teal),0 0 40px rgba(0,232,232,0.4);
- letter-spacing:0.35em;
- animation: spaceagePulse 3s ease-in-out infinite;
- }
- @keyframes spaceagePulse { 0%,100%{text-shadow:0 0 20px var(--teal),0 0 40px rgba(0,232,232,0.4)} 50%{text-shadow:0 0 30px var(--teal),0 0 60px rgba(0,232,232,0.6)} }
- .splash-spaceage .splash-tag { color:var(--silver); letter-spacing:2px; font-family:'Space Mono',monospace; font-size:0.78rem; }
- .splash-spaceage .splash-hint { color:var(--dim); letter-spacing:2px; font-family:'Space Mono',monospace; font-size:0.72rem; }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-spaceage" 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-inner">
- <div class="splash-title">STARBASE SNONUX</div>
- <div class="splash-tag">Orbital uplink established</div>
- <div class="splash-hint">Press Enter or click to dock</div>
- </div>
- </div>
- <script>
+
// Splash WebGL: slowly rotating torus (space station ring) + star field.
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
@@ -123,46 +20,8 @@
function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.x=t*0.28;g.rotation.y=t*0.45;hub.rotation.z=t*1.1;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 / <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">TRANSMIT</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}}">&lt;-- NEWER</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
+
+
// Space Age WebGL: toroidal space station ring + three satellite pods orbiting it
// + a slowly rotating planet sphere + dense star field. Teal wireframe throughout.
(function() {
@@ -344,7 +203,3 @@
setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 230); }, 20);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/surveillance.tmpl b/internal/generator/templates/themes/surveillance.tmpl
deleted file mode 100644
index 5d0df8d..0000000
--- a/internal/generator/templates/themes/surveillance.tmpl
+++ /dev/null
@@ -1,224 +0,0 @@
-<!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>
diff --git a/internal/generator/templates/themes/surveillance/meta.json b/internal/generator/templates/themes/surveillance/meta.json
new file mode 100644
index 0000000..7e948f9
--- /dev/null
+++ b/internal/generator/templates/themes/surveillance/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // SURVEILLANCE",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eOperator\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-grid\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eCamera Mesh Online\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to access the feed\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/surveillance/theme.css b/internal/generator/templates/themes/surveillance/theme.css
new file mode 100644
index 0000000..cd54913
--- /dev/null
+++ b/internal/generator/templates/themes/surveillance/theme.css
@@ -0,0 +1,50 @@
+ :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;} }
+ [data-sno-theme="surveillance"] .splash-overlay {
+ background:
+ radial-gradient(circle at 50% 22%, rgba(99,243,168,0.12) 0%, transparent 34%),
+ linear-gradient(180deg, #09100d 0%, #050907 100%);
+ }
+ [data-sno-theme="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; }
+ [data-sno-theme="surveillance"] .splash-title { font-size:clamp(1.45rem,4.8vw,2rem); color:var(--green); letter-spacing:0.28em; }
+ [data-sno-theme="surveillance"] .splash-tag { color:var(--grey); letter-spacing:0.22em; }
+ [data-sno-theme="surveillance"] .splash-hint { color:rgba(188,255,212,0.76); }
+ [data-sno-theme="surveillance"] .splash-inner { text-shadow:0 0 18px rgba(99,243,168,0.28); }
diff --git a/internal/generator/templates/themes/surveillance/theme.js b/internal/generator/templates/themes/surveillance/theme.js
new file mode 100644
index 0000000..e0e7474
--- /dev/null
+++ b/internal/generator/templates/themes/surveillance/theme.js
@@ -0,0 +1,107 @@
+
+ (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);
+ })();
+
+
+ (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);
+ };
+ })();
diff --git a/internal/generator/templates/themes/synthwave/meta.json b/internal/generator/templates/themes/synthwave/meta.json
new file mode 100644
index 0000000..f92f49f
--- /dev/null
+++ b/internal/generator/templates/themes/synthwave/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ⊕ SYNTHWAVE",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTRANSMIT TO NEXUS\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-grid\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-sun\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eSynthwave uplink\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eClick or Enter to ride the grid\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; NEWER",
+ "next_page_text": "OLDER \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/synthwave/theme.css b/internal/generator/templates/themes/synthwave/theme.css
new file mode 100644
index 0000000..0195e5f
--- /dev/null
+++ b/internal/generator/templates/themes/synthwave/theme.css
@@ -0,0 +1,85 @@
+ :root { --pink:#ff2d78; --purple:#bf3fff; --orange:#ff6b2b; --bg:#0d0221; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Russo One','Arial Black',sans-serif; background:var(--bg);
+ color:#fff; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(13,2,33,0.82); backdrop-filter:blur(10px);
+ border-bottom:2px solid var(--pink); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.8rem; background:linear-gradient(90deg,var(--pink),var(--purple));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
+ .logo-title h1 { font-size:1.7rem; background:linear-gradient(90deg,var(--pink),var(--orange));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; letter-spacing:2px; }
+ .logo-title .subtitle { font-size:0.7rem; color:rgba(255,255,255,0.55); margin-top:2px;
+ font-family:'Share Tech Mono',monospace; }
+ .logo-title .subtitle a { color:var(--pink); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--pink); }
+ .transmit-btn { border:2px solid var(--orange); color:var(--orange); padding:10px 22px;
+ border-radius:4px; text-decoration:none; letter-spacing:1px;
+ font-size:0.88rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--orange); color:var(--bg); }
+ a.header-feed-link { color:var(--pink); font-family:'Share Tech Mono',monospace; }
+ .nav-hints { background:rgba(13,2,33,0.75); border-bottom:1px solid rgba(255,45,120,0.3);
+ color:rgba(255,255,255,0.45); padding:5px 20px; display:flex; gap:18px;
+ font-size:0.68rem; font-family:'Share Tech Mono',monospace; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(255,45,120,0.15); border:1px solid rgba(255,45,120,0.4);
+ color:var(--pink); border-radius:3px; padding:0 5px; margin:0 2px; font-size:0.7rem; }
+ .content { flex:1; overflow-y:auto; padding:22px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--pink) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:2px solid var(--purple); color:var(--purple); padding:8px 22px;
+ border-radius:4px; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--purple); color:#fff; }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(13,2,33,0.82); backdrop-filter:blur(10px);
+ border-top:2px solid var(--pink); }
+ .post { background:rgba(20,5,50,0.85); border:1px solid var(--purple); border-radius:6px;
+ padding:22px; margin-bottom:18px; cursor:pointer; transition:all 0.25s; }
+ .post:hover { border-color:var(--pink); box-shadow:0 0 22px rgba(255,45,120,0.35); transform:translateY(-3px); }
+ .post-active { border-color:var(--orange) !important; background:rgba(30,8,60,0.96) !important;
+ box-shadow:0 0 22px rgba(255,107,43,0.45),inset 3px 0 0 var(--orange) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:14px; }
+ .post-time { color:var(--orange); font-family:'Share Tech Mono',monospace; font-size:0.85rem; }
+ .post-text { line-height:1.6; font-size:0.95rem; font-family:'Share Tech Mono',monospace; }
+ .post-text a { color:var(--pink); text-decoration:none; }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(13,2,33,0.96); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:780px; margin:0 auto; background:rgba(20,5,50,0.98);
+ border:2px solid var(--pink); border-radius:6px;
+ box-shadow:0 0 60px rgba(255,45,120,0.35); padding:38px; }
+ .modal-close { float:right; background:none; border:none; color:var(--orange);
+ font-family:'Russo One',sans-serif; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} }
+ [data-sno-theme="synthwave"] .splash-overlay {
+ background: linear-gradient(180deg, #2a0a3e 0%, var(--bg) 38%, #1a0630 100%);
+ }
+ [data-sno-theme="synthwave"] .splash-grid {
+ position:absolute; inset:0; opacity:0.35; pointer-events:none; z-index:1;
+ background: linear-gradient(90deg, rgba(255,45,120,0.08) 1px, transparent 1px) 0 0 / 48px 48px,
+ linear-gradient(rgba(191,63,255,0.06) 1px, transparent 1px) 0 0 / 48px 48px;
+ transform: perspective(280px) rotateX(68deg) scale(2.2);
+ transform-origin: 50% 85%;
+ animation: splashGridDrift 10s linear infinite;
+ }
+ @keyframes splashGridDrift { to { background-position: 48px 48px, 0 96px; } }
+ [data-sno-theme="synthwave"] .splash-sun {
+ width:min(140px,35vw); height:min(140px,35vw); margin:0 auto 1rem; border-radius:50%;
+ background: radial-gradient(circle, var(--orange) 0%, var(--pink) 45%, transparent 70%);
+ box-shadow: 0 0 60px var(--pink), 0 0 100px var(--orange);
+ animation: splashSunPulse 2.5s ease-in-out infinite alternate;
+ }
+ @keyframes splashSunPulse {
+ from { transform: scale(0.95); opacity: 0.85; }
+ to { transform: scale(1.05); opacity: 1; }
+ }
+ [data-sno-theme="synthwave"] .splash-title {
+ font-family:'Russo One',sans-serif; font-size:clamp(1.5rem,5vw,2.2rem);
+ background: linear-gradient(90deg,var(--pink),var(--orange));
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent;
+ }
+ [data-sno-theme="synthwave"] .splash-tag { font-family:'Share Tech Mono',monospace; color:var(--purple); }
+ [data-sno-theme="synthwave"] .splash-hint { font-family:'Share Tech Mono',monospace; color:rgba(255,255,255,0.88); }
+ [data-sno-theme="synthwave"] .splash-inner { text-shadow: 0 2px 20px rgba(13,2,33,0.95); }
diff --git a/internal/generator/templates/themes/synthwave.tmpl b/internal/generator/templates/themes/synthwave/theme.js
index f07103e..7be585e 100644
--- a/internal/generator/templates/themes/synthwave.tmpl
+++ b/internal/generator/templates/themes/synthwave/theme.js
@@ -1,114 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ⊕ SYNTHWAVE</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link href="https://fonts.googleapis.com/css2?family=Russo+One&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 { --pink:#ff2d78; --purple:#bf3fff; --orange:#ff6b2b; --bg:#0d0221; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Russo One','Arial Black',sans-serif; background:var(--bg);
- color:#fff; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(13,2,33,0.82); backdrop-filter:blur(10px);
- border-bottom:2px solid var(--pink); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:1.8rem; background:linear-gradient(90deg,var(--pink),var(--purple));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
- .logo-title h1 { font-size:1.7rem; background:linear-gradient(90deg,var(--pink),var(--orange));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; letter-spacing:2px; }
- .logo-title .subtitle { font-size:0.7rem; color:rgba(255,255,255,0.55); margin-top:2px;
- font-family:'Share Tech Mono',monospace; }
- .logo-title .subtitle a { color:var(--pink); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--pink); }
- .transmit-btn { border:2px solid var(--orange); color:var(--orange); padding:10px 22px;
- border-radius:4px; text-decoration:none; letter-spacing:1px;
- font-size:0.88rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--orange); color:var(--bg); }
- a.header-feed-link { color:var(--pink); font-family:'Share Tech Mono',monospace; }
- .nav-hints { background:rgba(13,2,33,0.75); border-bottom:1px solid rgba(255,45,120,0.3);
- color:rgba(255,255,255,0.45); padding:5px 20px; display:flex; gap:18px;
- font-size:0.68rem; font-family:'Share Tech Mono',monospace; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(255,45,120,0.15); border:1px solid rgba(255,45,120,0.4);
- color:var(--pink); border-radius:3px; padding:0 5px; margin:0 2px; font-size:0.7rem; }
- .content { flex:1; overflow-y:auto; padding:22px 28px;
- scrollbar-width:thin; scrollbar-color:var(--pink) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:2px solid var(--purple); color:var(--purple); padding:8px 22px;
- border-radius:4px; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
- .page-nav a:hover { background:var(--purple); color:#fff; }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(13,2,33,0.82); backdrop-filter:blur(10px);
- border-top:2px solid var(--pink); }
- .post { background:rgba(20,5,50,0.85); border:1px solid var(--purple); border-radius:6px;
- padding:22px; margin-bottom:18px; cursor:pointer; transition:all 0.25s; }
- .post:hover { border-color:var(--pink); box-shadow:0 0 22px rgba(255,45,120,0.35); transform:translateY(-3px); }
- .post-active { border-color:var(--orange) !important; background:rgba(30,8,60,0.96) !important;
- box-shadow:0 0 22px rgba(255,107,43,0.45),inset 3px 0 0 var(--orange) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:14px; }
- .post-time { color:var(--orange); font-family:'Share Tech Mono',monospace; font-size:0.85rem; }
- .post-text { line-height:1.6; font-size:0.95rem; font-family:'Share Tech Mono',monospace; }
- .post-text a { color:var(--pink); text-decoration:none; }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(13,2,33,0.96); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:780px; margin:0 auto; background:rgba(20,5,50,0.98);
- border:2px solid var(--pink); border-radius:6px;
- box-shadow:0 0 60px rgba(255,45,120,0.35); padding:38px; }
- .modal-close { float:right; background:none; border:none; color:var(--orange);
- font-family:'Russo One',sans-serif; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} }
- .splash-overlay.splash-synthwave {
- background: linear-gradient(180deg, #2a0a3e 0%, var(--bg) 38%, #1a0630 100%);
- }
- .splash-synthwave .splash-grid {
- position:absolute; inset:0; opacity:0.35; pointer-events:none; z-index:1;
- background: linear-gradient(90deg, rgba(255,45,120,0.08) 1px, transparent 1px) 0 0 / 48px 48px,
- linear-gradient(rgba(191,63,255,0.06) 1px, transparent 1px) 0 0 / 48px 48px;
- transform: perspective(280px) rotateX(68deg) scale(2.2);
- transform-origin: 50% 85%;
- animation: splashGridDrift 10s linear infinite;
- }
- @keyframes splashGridDrift { to { background-position: 48px 48px, 0 96px; } }
- .splash-synthwave .splash-sun {
- width:min(140px,35vw); height:min(140px,35vw); margin:0 auto 1rem; border-radius:50%;
- background: radial-gradient(circle, var(--orange) 0%, var(--pink) 45%, transparent 70%);
- box-shadow: 0 0 60px var(--pink), 0 0 100px var(--orange);
- animation: splashSunPulse 2.5s ease-in-out infinite alternate;
- }
- @keyframes splashSunPulse {
- from { transform: scale(0.95); opacity: 0.85; }
- to { transform: scale(1.05); opacity: 1; }
- }
- .splash-synthwave .splash-title {
- font-family:'Russo One',sans-serif; font-size:clamp(1.5rem,5vw,2.2rem);
- background: linear-gradient(90deg,var(--pink),var(--orange));
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
- }
- .splash-synthwave .splash-tag { font-family:'Share Tech Mono',monospace; color:var(--purple); }
- .splash-synthwave .splash-hint { font-family:'Share Tech Mono',monospace; color:rgba(255,255,255,0.88); }
- .splash-synthwave .splash-inner { text-shadow: 0 2px 20px rgba(13,2,33,0.95); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-synthwave" 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-sun" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Synthwave uplink</div>
- <div class="splash-hint">Click or Enter to ride the grid</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -127,46 +17,8 @@
function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;g.rotation.y=Math.sin(t*0.35)*0.08;sun.position.y=2.1+Math.sin(t*1.2)*0.08;sun.scale.setScalar(1+Math.sin(t*2)*0.04);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">TRANSMIT TO NEXUS</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>
+
+
// Synthwave WebGL: glowing sunset sphere with horizontal scan-line rings,
// a receding grid floor, and pink star particles. Replaces CSS sky/grid.
(function() {
@@ -320,7 +172,3 @@
setTimeout(function() { d.style.transform='scaleY(1.4)'; d.style.opacity='0'; setTimeout(function() { d.remove(); }, 250); }, 20);
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/terminal.tmpl b/internal/generator/templates/themes/terminal.tmpl
deleted file mode 100644
index 167f08d..0000000
--- a/internal/generator/templates/themes/terminal.tmpl
+++ /dev/null
@@ -1,275 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo // TERMINAL</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --p:#33ff33; --dim:#1a7a1a; --bg:#0a0a0a; --bg2:#050505; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--p);
- overflow:hidden; height:100vh; position:relative; }
- /* CRT scanlines sit above the WebGL canvas */
- body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
- background:repeating-linear-gradient(0deg,transparent,transparent 2px,
- rgba(0,0,0,0.12) 2px,rgba(0,0,0,0.12) 4px); }
- /* Subtle screen flicker */
- @keyframes flicker { 0%,100%{opacity:1} 93%{opacity:0.97} 95%{opacity:0.91} 97%{opacity:0.98} }
- body { animation:flicker 9s infinite; }
- /* WebGL background canvas — fills the viewport behind everything */
- #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
- .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
- header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--p);
- display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:1.6rem; color:var(--p); text-shadow:0 0 14px var(--p); letter-spacing:2px; }
- .logo-title h1 { font-size:1.3rem; color:var(--p); text-shadow:0 0 10px var(--p);
- letter-spacing:3px; font-weight:normal; }
- .logo-title .subtitle { font-size:0.72rem; color:var(--dim); margin-top:2px; }
- .logo-title .subtitle a { color:var(--p); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--p); }
- .nav a.transmit-btn { border:1px solid var(--p); color:var(--p); padding:8px 18px;
- border-radius:0; text-decoration:none; letter-spacing:2px; font-size:0.85rem;
- transition:all 0.2s; }
- .nav a.transmit-btn:hover { background:var(--p); color:var(--bg); }
- a.header-feed-link { color:var(--dim); }
- a.header-feed-link:hover { color:var(--p); }
- .nav-hints { background:var(--bg2); border-bottom:1px solid var(--dim); color:var(--dim);
- padding:5px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--p);
- border-radius:0; padding:0 5px; font-size:0.7rem; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:16px 24px;
- scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--dim); color:var(--p); padding:7px 20px;
- border-radius:0; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
- .page-nav a:hover { background:var(--p); color:var(--bg); border-color:var(--p); }
- .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
- background:var(--bg2); border-top:2px solid var(--p); }
- .post { background:var(--bg); border:1px solid var(--dim); border-radius:0;
- padding:18px 20px; margin-bottom:12px; cursor:pointer; transition:border-color 0.15s; }
- .post:hover { border-color:var(--p); box-shadow:0 0 8px rgba(51,255,51,0.3); }
- .post-active { border-color:var(--p) !important; background:rgba(51,255,51,0.04) !important;
- box-shadow:0 0 14px rgba(51,255,51,0.3),inset 3px 0 0 var(--p) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--dim); font-size:0.82rem; }
- .post-text { line-height:1.6; font-size:0.92rem; }
- .post-text a { color:var(--p); text-decoration:underline; }
- .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim); }
- .post-audio { width:100%; margin-top:10px; }
- .post-modal { display:none; position:fixed; inset:0; z-index:100;
- background:rgba(0,0,0,0.97); overflow-y:auto; padding:40px 20px; }
- .post-modal.active { display:block; }
- .modal-inner { max-width:760px; margin:0 auto; background:var(--bg);
- border:1px solid var(--p); border-radius:0;
- box-shadow:0 0 40px rgba(51,255,51,0.25); padding:36px; }
- .modal-close { float:right; background:none; border:none; color:var(--p);
- font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:12px 16px;} }
- .splash-overlay.splash-terminal { background: var(--bg); font-family:'Courier New',monospace; }
- .splash-terminal .splash-prompt { text-align:left; font-size:0.9rem; color:rgba(51,255,51,0.78); margin-bottom:0.5rem; }
- .splash-terminal .splash-title { font-size:clamp(1.2rem,4vw,1.65rem); color:var(--p);
- text-shadow:0 0 12px var(--p); letter-spacing:0.15em; }
- .splash-terminal .splash-cursor::after { content:'█'; animation: splashTermBlink 1s step-end infinite; color:var(--p); }
- @keyframes splashTermBlink { 0%,100%{opacity:1} 50%{opacity:0} }
- .splash-terminal .splash-tag { color:rgba(51,255,51,0.85); letter-spacing:0.25em; }
- .splash-terminal .splash-hint { color:rgba(51,255,51,0.8); }
- .splash-terminal .splash-inner { text-shadow: 0 0 8px #000, 0 2px 12px #000; }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-terminal" 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-inner">
- <div class="splash-prompt">&gt; ./snonux --boot</div>
- <div class="splash-title splash-cursor">LINK ESTABLISHED</div>
- <div class="splash-tag">TERMINAL SESSION</div>
- <div class="splash-hint">[ click / enter to continue ]</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,m,t0=performance.now();
- 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=7;
- m=new THREE.Mesh(new THREE.IcosahedronGeometry(2.3,1),new THREE.MeshBasicMaterial({color:0x33ff33,wireframe:true,transparent:true,opacity:0.88}));
- sc.add(m);sz();window.addEventListener('resize',sz);
- function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;m.rotation.x=t*0.62;m.rotation.y=t*0.88;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 / <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.xml</a>
- <a href="https://foo.zone/about" class="transmit-btn">&gt; TRANSMIT</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}}">&lt;-- NEWER</a>{{end}}
- {{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
- </div>
- </footer>
- {{end}}
- </div>
- {{template "navmodal" .}}
- <script>
- // Terminal WebGL scene: phosphor-green icosahedron wireframe + torus particle ring.
- // The scene sits behind the CRT scanline overlay (z-index:999) and the UI (z-index:10).
- (function() {
- var _wild = false, _snoTOffset = 0, _snoLastT = 0;
- var scene, camera, renderer, icosa, particles;
- var clock = new THREE.Clock();
-
- function initThree() {
- // Scene with pure-black background and distance fog
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x000000);
- scene.fog = new THREE.Fog(0x000000, 20, 80);
-
- // Perspective camera positioned in front of the orb
- camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
- camera.position.set(0, 0, 30);
-
- renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
-
- // Large green phosphor wireframe icosahedron — the central CRT orb
- var icoGeo = new THREE.IcosahedronGeometry(8, 2);
- var icoMat = new THREE.MeshBasicMaterial({ color: 0x33ff33, wireframe: true });
- icosa = new THREE.Mesh(icoGeo, icoMat);
- scene.add(icosa);
-
- // 400 dim particles arranged on a torus path around the icosahedron
- var torusGeo = new THREE.TorusGeometry(14, 3, 16, 100);
- var positions = torusGeo.attributes.position;
- var ptGeo = new THREE.BufferGeometry();
- var pts = new Float32Array(400 * 3);
- for (var i = 0; i < 400; i++) {
- // Sample vertices from the torus geometry to place particles on its surface
- var idx = Math.floor(Math.random() * positions.count);
- pts[i * 3] = positions.getX(idx);
- pts[i * 3 + 1] = positions.getY(idx);
- pts[i * 3 + 2] = positions.getZ(idx);
- }
- ptGeo.setAttribute('position', new THREE.BufferAttribute(pts, 3));
- var ptMat = new THREE.PointsMaterial({ color: 0x1a7a1a, size: 0.18 });
- particles = new THREE.Points(ptGeo, ptMat);
- scene.add(particles);
-
- 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;
- // Slow multi-axis rotation; wild mode overloads the phosphor orb
- icosa.rotation.x = t * 0.12;
- icosa.rotation.y = t * 0.18;
- icosa.rotation.z = t * 0.07;
- // Counter-rotate particles for visual contrast
- particles.rotation.y = -t * 0.08;
- particles.rotation.x = t * 0.04;
- renderer.render(scene, camera);
- }
-
- initThree();
-
- // Terminal nav/wild effects — cursor glitch on navigate, buffer overflow on wild
- window.snonuxOpenEffect = function() {
- // Slide in like terminal output being printed
- var modal = document.getElementById('post-modal');
- if (modal) { modal.classList.add('sno-modal-slide'); setTimeout(function() { modal.classList.remove('sno-modal-slide'); }, 360); }
- // Phosphor scan from top to bottom
- var scan = document.createElement('div');
- scan.style.cssText = 'position:fixed;top:0;left:0;right:0;height:2px;z-index:997;pointer-events:none;background:rgba(51,255,51,0.6);box-shadow:0 0 8px rgba(51,255,51,0.4);transition:top 0.3s linear,opacity 0.1s 0.3s';
- document.body.appendChild(scan);
- setTimeout(function() { scan.style.top='100vh'; setTimeout(function() { scan.style.opacity='0'; setTimeout(function() { scan.remove(); }, 120); }, 300); }, 15);
- };
- window.snonuxCloseEffect = function() {
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(51,255,51,0.1);transition:opacity 0.18s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 15);
- document.body.style.animationDuration = '9s';
- };
- window.snonuxScrollEffect = function(dir) {
- var isDown = dir === 'down';
- var thick = _wild ? '14px' : '5px';
- var d = document.createElement('div');
- // Terminal: phosphor green scan
- d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
- 'background:linear-gradient(90deg,transparent,rgba(57,255,20,0.9),rgba(20,200,10,0.9),rgba(57,255,20,0.9),transparent);' +
- (isDown ? 'top:0;' : 'bottom:0;') +
- 'transition:transform 0.28s ease,opacity 0.28s ease;';
- document.body.appendChild(d);
- setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
- setTimeout(function() { d.remove(); }, 360);
- };
- window.snonuxWildToggle = function() {
- _wild = !_wild;
- var b = document.getElementById('sno-wild-badge');
- if (b) b.classList.toggle('sno-wild-on', _wild);
- // Toggle intense scanline strobe in wild mode
- document.body.style.animationDuration = _wild ? '0.4s' : '9s';
- };
- window.snonuxNavEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 300); }
- var d = document.createElement('div');
- d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(51,255,51,0.13);transition:opacity 0.18s';
- document.body.appendChild(d);
- setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 210); }, 25);
- };
- window.snonuxPageEffect = function() {
- var ov = document.querySelector('.overlay');
- if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); setTimeout(function() { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }, 35); }, 300); }
- };
- })();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/terminal/meta.json b/internal/generator/templates/themes/terminal/meta.json
new file mode 100644
index 0000000..8fbc7ed
--- /dev/null
+++ b/internal/generator/templates/themes/terminal/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo // TERMINAL",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003e[SN]\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog / \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eatom.xml\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003e\u0026gt; TRANSMIT\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-prompt\"\u003e\u0026gt; ./snonux --boot\u003c/div\u003e\n \u003cdiv class=\"splash-title splash-cursor\"\u003eLINK ESTABLISHED\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eTERMINAL SESSION\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003e[ click / enter to continue ]\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026lt;-- NEWER",
+ "next_page_text": "OLDER --\u0026gt;"
+}
diff --git a/internal/generator/templates/themes/terminal/theme.css b/internal/generator/templates/themes/terminal/theme.css
new file mode 100644
index 0000000..afd4489
--- /dev/null
+++ b/internal/generator/templates/themes/terminal/theme.css
@@ -0,0 +1,70 @@
+ :root { --p:#33ff33; --dim:#1a7a1a; --bg:#0a0a0a; --bg2:#050505; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--p);
+ overflow:hidden; height:100vh; position:relative; }
+ /* CRT scanlines sit above the WebGL canvas */
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
+ background:repeating-linear-gradient(0deg,transparent,transparent 2px,
+ rgba(0,0,0,0.12) 2px,rgba(0,0,0,0.12) 4px); }
+ /* Subtle screen flicker */
+ @keyframes flicker { 0%,100%{opacity:1} 93%{opacity:0.97} 95%{opacity:0.91} 97%{opacity:0.98} }
+ body { animation:flicker 9s infinite; }
+ /* WebGL background canvas — fills the viewport behind everything */
+ #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--p);
+ display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.6rem; color:var(--p); text-shadow:0 0 14px var(--p); letter-spacing:2px; }
+ .logo-title h1 { font-size:1.3rem; color:var(--p); text-shadow:0 0 10px var(--p);
+ letter-spacing:3px; font-weight:normal; }
+ .logo-title .subtitle { font-size:0.72rem; color:var(--dim); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--p); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 6px var(--p); }
+ .nav a.transmit-btn { border:1px solid var(--p); color:var(--p); padding:8px 18px;
+ border-radius:0; text-decoration:none; letter-spacing:2px; font-size:0.85rem;
+ transition:all 0.2s; }
+ .nav a.transmit-btn:hover { background:var(--p); color:var(--bg); }
+ a.header-feed-link { color:var(--dim); }
+ a.header-feed-link:hover { color:var(--p); }
+ .nav-hints { background:var(--bg2); border-bottom:1px solid var(--dim); color:var(--dim);
+ padding:5px 24px; display:flex; gap:18px; font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:transparent; border:1px solid var(--dim); color:var(--p);
+ border-radius:0; padding:0 5px; font-size:0.7rem; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:16px 24px;
+ scrollbar-width:thin; scrollbar-color:var(--dim) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--dim); color:var(--p); padding:7px 20px;
+ border-radius:0; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--p); color:var(--bg); border-color:var(--p); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:var(--bg2); border-top:2px solid var(--p); }
+ .post { background:var(--bg); border:1px solid var(--dim); border-radius:0;
+ padding:18px 20px; margin-bottom:12px; cursor:pointer; transition:border-color 0.15s; }
+ .post:hover { border-color:var(--p); box-shadow:0 0 8px rgba(51,255,51,0.3); }
+ .post-active { border-color:var(--p) !important; background:rgba(51,255,51,0.04) !important;
+ box-shadow:0 0 14px rgba(51,255,51,0.3),inset 3px 0 0 var(--p) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--dim); font-size:0.82rem; }
+ .post-text { line-height:1.6; font-size:0.92rem; }
+ .post-text a { color:var(--p); text-decoration:underline; }
+ .post-image { max-width:100%; margin-top:10px; border:1px solid var(--dim); }
+ .post-audio { width:100%; margin-top:10px; }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100;
+ background:rgba(0,0,0,0.97); overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:var(--bg);
+ border:1px solid var(--p); border-radius:0;
+ box-shadow:0 0 40px rgba(51,255,51,0.25); padding:36px; }
+ .modal-close { float:right; background:none; border:none; color:var(--p);
+ font-family:monospace; font-size:0.9rem; cursor:pointer; letter-spacing:2px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:10px 16px;} .content{padding:12px 16px;} }
+ [data-sno-theme="terminal"] .splash-overlay { background: var(--bg); font-family:'Courier New',monospace; }
+ [data-sno-theme="terminal"] .splash-prompt { text-align:left; font-size:0.9rem; color:rgba(51,255,51,0.78); margin-bottom:0.5rem; }
+ [data-sno-theme="terminal"] .splash-title { font-size:clamp(1.2rem,4vw,1.65rem); color:var(--p);
+ text-shadow:0 0 12px var(--p); letter-spacing:0.15em; }
+ [data-sno-theme="terminal"] .splash-cursor::after { content:'█'; animation: splashTermBlink 1s step-end infinite; color:var(--p); }
+ @keyframes splashTermBlink { 0%,100%{opacity:1} 50%{opacity:0} }
+ [data-sno-theme="terminal"] .splash-tag { color:rgba(51,255,51,0.85); letter-spacing:0.25em; }
+ [data-sno-theme="terminal"] .splash-hint { color:rgba(51,255,51,0.8); }
+ [data-sno-theme="terminal"] .splash-inner { text-shadow: 0 0 8px #000, 0 2px 12px #000; }
diff --git a/internal/generator/templates/themes/terminal/theme.js b/internal/generator/templates/themes/terminal/theme.js
new file mode 100644
index 0000000..4400a7e
--- /dev/null
+++ b/internal/generator/templates/themes/terminal/theme.js
@@ -0,0 +1,141 @@
+
+ (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,m,t0=performance.now();
+ 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=7;
+ m=new THREE.Mesh(new THREE.IcosahedronGeometry(2.3,1),new THREE.MeshBasicMaterial({color:0x33ff33,wireframe:true,transparent:true,opacity:0.88}));
+ sc.add(m);sz();window.addEventListener('resize',sz);
+ function loop(now){raf=requestAnimationFrame(loop);var t=(now-t0)*0.001;m.rotation.x=t*0.62;m.rotation.y=t*0.88;ren.render(sc,ca);}
+ raf=requestAnimationFrame(loop);
+ })();
+
+
+ // Terminal WebGL scene: phosphor-green icosahedron wireframe + torus particle ring.
+ // The scene sits behind the CRT scanline overlay (z-index:999) and the UI (z-index:10).
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, icosa, particles;
+ var clock = new THREE.Clock();
+
+ function initThree() {
+ // Scene with pure-black background and distance fog
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x000000);
+ scene.fog = new THREE.Fog(0x000000, 20, 80);
+
+ // Perspective camera positioned in front of the orb
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
+ camera.position.set(0, 0, 30);
+
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+
+ // Large green phosphor wireframe icosahedron — the central CRT orb
+ var icoGeo = new THREE.IcosahedronGeometry(8, 2);
+ var icoMat = new THREE.MeshBasicMaterial({ color: 0x33ff33, wireframe: true });
+ icosa = new THREE.Mesh(icoGeo, icoMat);
+ scene.add(icosa);
+
+ // 400 dim particles arranged on a torus path around the icosahedron
+ var torusGeo = new THREE.TorusGeometry(14, 3, 16, 100);
+ var positions = torusGeo.attributes.position;
+ var ptGeo = new THREE.BufferGeometry();
+ var pts = new Float32Array(400 * 3);
+ for (var i = 0; i < 400; i++) {
+ // Sample vertices from the torus geometry to place particles on its surface
+ var idx = Math.floor(Math.random() * positions.count);
+ pts[i * 3] = positions.getX(idx);
+ pts[i * 3 + 1] = positions.getY(idx);
+ pts[i * 3 + 2] = positions.getZ(idx);
+ }
+ ptGeo.setAttribute('position', new THREE.BufferAttribute(pts, 3));
+ var ptMat = new THREE.PointsMaterial({ color: 0x1a7a1a, size: 0.18 });
+ particles = new THREE.Points(ptGeo, ptMat);
+ scene.add(particles);
+
+ 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;
+ // Slow multi-axis rotation; wild mode overloads the phosphor orb
+ icosa.rotation.x = t * 0.12;
+ icosa.rotation.y = t * 0.18;
+ icosa.rotation.z = t * 0.07;
+ // Counter-rotate particles for visual contrast
+ particles.rotation.y = -t * 0.08;
+ particles.rotation.x = t * 0.04;
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ // Terminal nav/wild effects — cursor glitch on navigate, buffer overflow on wild
+ window.snonuxOpenEffect = function() {
+ // Slide in like terminal output being printed
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-slide'); setTimeout(function() { modal.classList.remove('sno-modal-slide'); }, 360); }
+ // Phosphor scan from top to bottom
+ var scan = document.createElement('div');
+ scan.style.cssText = 'position:fixed;top:0;left:0;right:0;height:2px;z-index:997;pointer-events:none;background:rgba(51,255,51,0.6);box-shadow:0 0 8px rgba(51,255,51,0.4);transition:top 0.3s linear,opacity 0.1s 0.3s';
+ document.body.appendChild(scan);
+ setTimeout(function() { scan.style.top='100vh'; setTimeout(function() { scan.style.opacity='0'; setTimeout(function() { scan.remove(); }, 120); }, 300); }, 15);
+ };
+ window.snonuxCloseEffect = function() {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(51,255,51,0.1);transition:opacity 0.18s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 200); }, 15);
+ document.body.style.animationDuration = '9s';
+ };
+ window.snonuxScrollEffect = function(dir) {
+ var isDown = dir === 'down';
+ var thick = _wild ? '14px' : '5px';
+ var d = document.createElement('div');
+ // Terminal: phosphor green scan
+ d.style.cssText = 'position:fixed;left:0;right:0;height:' + thick + ';z-index:9000;pointer-events:none;' +
+ 'background:linear-gradient(90deg,transparent,rgba(57,255,20,0.9),rgba(20,200,10,0.9),rgba(57,255,20,0.9),transparent);' +
+ (isDown ? 'top:0;' : 'bottom:0;') +
+ 'transition:transform 0.28s ease,opacity 0.28s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = isDown ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 360);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ // Toggle intense scanline strobe in wild mode
+ document.body.style.animationDuration = _wild ? '0.4s' : '9s';
+ };
+ window.snonuxNavEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 300); }
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;background:rgba(51,255,51,0.13);transition:opacity 0.18s';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, 210); }, 25);
+ };
+ window.snonuxPageEffect = function() {
+ var ov = document.querySelector('.overlay');
+ if (ov) { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); setTimeout(function() { ov.classList.add('sno-fx-glitch'); setTimeout(function() { ov.classList.remove('sno-fx-glitch'); }, 280); }, 35); }, 300); }
+ };
+ })();
diff --git a/internal/generator/templates/themes/tropicale/meta.json b/internal/generator/templates/themes/tropicale/meta.json
new file mode 100644
index 0000000..292b1ed
--- /dev/null
+++ b/internal/generator/templates/themes/tropicale/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ~ TROPICALE",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-sun-glow\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-wave-bar\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eIsland transmission\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eRide the wave — click or Enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/tropicale/theme.css b/internal/generator/templates/themes/tropicale/theme.css
new file mode 100644
index 0000000..cc93504
--- /dev/null
+++ b/internal/generator/templates/themes/tropicale/theme.css
@@ -0,0 +1,81 @@
+ :root { --sand:#e8c97a; --sky:#38c9d8; --lagoon:#0e7490; --sun:#fbbf24; --coral:#f97316; --dusk:#0a1e2e; --cream:#fef9e7; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--dusk);
+ color:var(--cream); overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(10,30,46,0.82); backdrop-filter:blur(12px);
+ border-bottom:1px solid rgba(56,201,216,0.3); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800; color:var(--sun); text-shadow:0 0 18px rgba(251,191,36,0.7); }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:var(--cream); letter-spacing:1px; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(254,249,231,0.55); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--sky); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--sky); }
+ .transmit-btn { border:1px solid var(--sky); color:var(--sky); padding:9px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--sky); color:var(--dusk); }
+ a.header-feed-link { color:var(--sand); }
+ a.header-feed-link:hover { color:var(--cream); }
+ .nav-hints { background:rgba(10,30,46,0.65); border-bottom:1px solid rgba(56,201,216,0.18);
+ color:rgba(254,249,231,0.45); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(56,201,216,0.12); border:1px solid rgba(56,201,216,0.35);
+ color:var(--sky); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--sky) var(--dusk); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--lagoon); color:var(--sky); padding:8px 20px;
+ border-radius:20px; text-decoration:none; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--sky); color:var(--dusk); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(10,30,46,0.82); backdrop-filter:blur(12px);
+ border-top:1px solid rgba(56,201,216,0.3); }
+ .post { background:rgba(10,40,60,0.55); border:1px solid rgba(56,201,216,0.22); border-radius:10px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(6px); }
+ .post:hover { border-color:var(--sky); box-shadow:0 4px 24px rgba(56,201,216,0.22); transform:translateY(-2px); }
+ .post-active { border-color:var(--coral) !important; background:rgba(60,20,10,0.55) !important;
+ box-shadow:0 0 22px rgba(249,115,22,0.35),inset 3px 0 0 var(--coral) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--sand); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--sky); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--sky); }
+ .post-audio { width:100%; margin-top:10px; }
+ .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,25,40,0.94);
+ border:1px solid var(--sky); border-radius:12px; backdrop-filter:blur(16px);
+ box-shadow:0 0 60px rgba(56,201,216,0.3); padding:40px; }
+ .modal-close { float:right; background:none; border:none; color:var(--sky);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ /* Splash screen — tropical sunset gradient with radial sun glow */
+ [data-sno-theme="tropicale"] .splash-overlay {
+ background:
+ radial-gradient(ellipse 55% 40% at 75% 35%, rgba(251,191,36,0.22) 0%, transparent 60%),
+ linear-gradient(175deg, #0a1e2e 0%, #0e3a50 30%, #1a5c70 55%, #0e3a50 80%, #0a1e2e 100%);
+ }
+ [data-sno-theme="tropicale"] .splash-sun-glow {
+ position:absolute; right:22%; top:18%; width:clamp(80px,14vw,130px); height:clamp(80px,14vw,130px);
+ border-radius:50%; pointer-events:none; z-index:0;
+ background:radial-gradient(circle, rgba(251,191,36,0.9) 30%, rgba(249,115,22,0.5) 60%, transparent 80%);
+ box-shadow:0 0 60px 30px rgba(251,191,36,0.35);
+ animation: splashSunPulse 3.5s ease-in-out infinite;
+ }
+ @keyframes splashSunPulse { 0%,100%{ opacity:0.85; transform:scale(1); } 50%{ opacity:1; transform:scale(1.06); } }
+ [data-sno-theme="tropicale"] .splash-wave-bar {
+ position:absolute; bottom:0; left:0; right:0; height:18px;
+ background: linear-gradient(90deg, transparent, rgba(56,201,216,0.6), rgba(232,201,122,0.4), rgba(56,201,216,0.6), transparent);
+ animation: splashWaveSweep 2.8s ease-in-out infinite;
+ }
+ @keyframes splashWaveSweep { 0%,100%{ transform:scaleX(1) translateY(0); } 50%{ transform:scaleX(1.04) translateY(-3px); } }
+ [data-sno-theme="tropicale"] .splash-title {
+ font-size:clamp(1.45rem,4.5vw,2rem); color:var(--cream);
+ text-shadow:0 0 24px rgba(251,191,36,0.55);
+ }
+ [data-sno-theme="tropicale"] .splash-tag { color:var(--sky); letter-spacing:0.2em; }
+ [data-sno-theme="tropicale"] .splash-hint { color:rgba(254,249,231,0.88); }
+ [data-sno-theme="tropicale"] .splash-inner { position:relative; z-index:2; text-shadow:0 2px 16px rgba(10,30,46,0.9); }
diff --git a/internal/generator/templates/themes/tropicale.tmpl b/internal/generator/templates/themes/tropicale/theme.js
index 920498b..7f7b600 100644
--- a/internal/generator/templates/themes/tropicale.tmpl
+++ b/internal/generator/templates/themes/tropicale/theme.js
@@ -1,108 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ~ TROPICALE</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --sand:#e8c97a; --sky:#38c9d8; --lagoon:#0e7490; --sun:#fbbf24; --coral:#f97316; --dusk:#0a1e2e; --cream:#fef9e7; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--dusk);
- color:var(--cream); overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(10,30,46,0.82); backdrop-filter:blur(12px);
- border-bottom:1px solid rgba(56,201,216,0.3); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800; color:var(--sun); text-shadow:0 0 18px rgba(251,191,36,0.7); }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:var(--cream); letter-spacing:1px; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(254,249,231,0.55); margin-top:2px; }
- .logo-title .subtitle a { color:var(--sky); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--sky); }
- .transmit-btn { border:1px solid var(--sky); color:var(--sky); padding:9px 20px;
- border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--sky); color:var(--dusk); }
- a.header-feed-link { color:var(--sand); }
- a.header-feed-link:hover { color:var(--cream); }
- .nav-hints { background:rgba(10,30,46,0.65); border-bottom:1px solid rgba(56,201,216,0.18);
- color:rgba(254,249,231,0.45); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(56,201,216,0.12); border:1px solid rgba(56,201,216,0.35);
- color:var(--sky); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--sky) var(--dusk); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--lagoon); color:var(--sky); padding:8px 20px;
- border-radius:20px; text-decoration:none; font-size:0.82rem; }
- .page-nav a:hover { background:var(--sky); color:var(--dusk); }
- .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
- background:rgba(10,30,46,0.82); backdrop-filter:blur(12px);
- border-top:1px solid rgba(56,201,216,0.3); }
- .post { background:rgba(10,40,60,0.55); border:1px solid rgba(56,201,216,0.22); border-radius:10px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(6px); }
- .post:hover { border-color:var(--sky); box-shadow:0 4px 24px rgba(56,201,216,0.22); transform:translateY(-2px); }
- .post-active { border-color:var(--coral) !important; background:rgba(60,20,10,0.55) !important;
- box-shadow:0 0 22px rgba(249,115,22,0.35),inset 3px 0 0 var(--coral) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--sand); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--sky); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--sky); }
- .post-audio { width:100%; margin-top:10px; }
- .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,25,40,0.94);
- border:1px solid var(--sky); border-radius:12px; backdrop-filter:blur(16px);
- box-shadow:0 0 60px rgba(56,201,216,0.3); padding:40px; }
- .modal-close { float:right; background:none; border:none; color:var(--sky);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- /* Splash screen — tropical sunset gradient with radial sun glow */
- .splash-overlay.splash-tropicale {
- background:
- radial-gradient(ellipse 55% 40% at 75% 35%, rgba(251,191,36,0.22) 0%, transparent 60%),
- linear-gradient(175deg, #0a1e2e 0%, #0e3a50 30%, #1a5c70 55%, #0e3a50 80%, #0a1e2e 100%);
- }
- .splash-tropicale .splash-sun-glow {
- position:absolute; right:22%; top:18%; width:clamp(80px,14vw,130px); height:clamp(80px,14vw,130px);
- border-radius:50%; pointer-events:none; z-index:0;
- background:radial-gradient(circle, rgba(251,191,36,0.9) 30%, rgba(249,115,22,0.5) 60%, transparent 80%);
- box-shadow:0 0 60px 30px rgba(251,191,36,0.35);
- animation: splashSunPulse 3.5s ease-in-out infinite;
- }
- @keyframes splashSunPulse { 0%,100%{ opacity:0.85; transform:scale(1); } 50%{ opacity:1; transform:scale(1.06); } }
- .splash-tropicale .splash-wave-bar {
- position:absolute; bottom:0; left:0; right:0; height:18px;
- background: linear-gradient(90deg, transparent, rgba(56,201,216,0.6), rgba(232,201,122,0.4), rgba(56,201,216,0.6), transparent);
- animation: splashWaveSweep 2.8s ease-in-out infinite;
- }
- @keyframes splashWaveSweep { 0%,100%{ transform:scaleX(1) translateY(0); } 50%{ transform:scaleX(1.04) translateY(-3px); } }
- .splash-tropicale .splash-title {
- font-size:clamp(1.45rem,4.5vw,2rem); color:var(--cream);
- text-shadow:0 0 24px rgba(251,191,36,0.55);
- }
- .splash-tropicale .splash-tag { color:var(--sky); letter-spacing:0.2em; }
- .splash-tropicale .splash-hint { color:rgba(254,249,231,0.88); }
- .splash-tropicale .splash-inner { position:relative; z-index:2; text-shadow:0 2px 16px rgba(10,30,46,0.9); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-tropicale" 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-sun-glow" aria-hidden="true"></div>
- <div class="splash-wave-bar" aria-hidden="true"></div>
- <div class="splash-inner">
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Island transmission</div>
- <div class="splash-hint">Ride the wave — click or Enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -154,46 +50,8 @@
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">Transmit</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>
+
+
// Tropicale WebGL: tropical sunset beach — rolling ocean waves, a glowing sun
// sinking toward the horizon, drifting seagulls, a palm tree silhouette on
// the shore, and golden sparkle particles on the water surface.
@@ -569,7 +427,3 @@
if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/templates/themes/volcano/meta.json b/internal/generator/templates/themes/volcano/meta.json
new file mode 100644
index 0000000..2998f64
--- /dev/null
+++ b/internal/generator/templates/themes/volcano/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "snonux.foo ▲ VOLCANO",
+ "header_html": "\u003cdiv class=\"logo\"\u003e\n \u003cspan class=\"logo-mark\"\u003eSN\u003c/span\u003e\n \u003cdiv class=\"logo-title\"\u003e\n \u003ch1\u003esnonux.foo\u003c/h1\u003e\n \u003cp class=\"subtitle\"\u003emicroblog \u0026mdash; \u003ca href=\"https://foo.zone\"\u003efoo.zone\u003c/a\u003e is the real blog\u003c/p\u003e\n \u003cp class=\"logo-host\"\u003eServed by NetBSD on a Raspberry Pi 3\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cdiv class=\"nav\"\u003e\n \u003ca href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\"\u003eAtom feed\u003c/a\u003e\n \u003ca href=\"https://foo.zone/about\" class=\"transmit-btn\"\u003eTransmit\u003c/a\u003e\n \u003c/div\u003e",
+ "splash_inner_html": "\u003ccanvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"\u003e\u003c/canvas\u003e\n \u003cdiv class=\"splash-inner\"\u003e\n \u003cdiv class=\"splash-ember\" aria-hidden=\"true\"\u003e\u003c/div\u003e\n \u003cdiv class=\"splash-title\"\u003esnonux.foo\u003c/div\u003e\n \u003cdiv class=\"splash-tag\"\u003eVolcano vent\u003c/div\u003e\n \u003cdiv class=\"splash-hint\"\u003eErupt into feed — click or Enter\u003c/div\u003e\n \u003c/div\u003e",
+ "prev_page_text": "\u0026larr; Newer",
+ "next_page_text": "Older \u0026rarr;"
+}
diff --git a/internal/generator/templates/themes/volcano/theme.css b/internal/generator/templates/themes/volcano/theme.css
new file mode 100644
index 0000000..6bf109a
--- /dev/null
+++ b/internal/generator/templates/themes/volcano/theme.css
@@ -0,0 +1,69 @@
+ :root { --lava:#ff4400; --ember:#ff8c00; --hot:#ffcc00; --bg:#0d0802; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
+ color:#ffe8cc; overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; top:0; left: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(13,8,2,0.82); backdrop-filter:blur(12px);
+ border-bottom:1px solid rgba(255,68,0,0.3); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:2rem; font-weight:800; color:var(--ember); text-shadow:0 0 16px var(--lava); }
+ .logo-title h1 { font-size:1.5rem; font-weight:700; color:#ffe8cc; }
+ .logo-title .subtitle { font-size:0.75rem; color:rgba(255,232,204,0.5); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--ember); text-decoration:none; }
+ .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--lava); }
+ .transmit-btn { border:1px solid var(--lava); color:var(--lava); padding:9px 20px;
+ border-radius:4px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
+ .transmit-btn:hover { background:var(--lava); color:var(--bg); }
+ a.header-feed-link { color:var(--ember); }
+ a.header-feed-link:hover { color:var(--hot); text-shadow:0 0 8px var(--lava); }
+ .nav-hints { background:rgba(13,8,2,0.7); border-bottom:1px solid rgba(255,68,0,0.15);
+ color:rgba(255,232,204,0.4); padding:5px 28px; display:flex; gap:18px;
+ font-size:0.68rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(255,68,0,0.12); border:1px solid rgba(255,68,0,0.35);
+ color:var(--ember); border-radius:3px; padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px;
+ scrollbar-width:thin; scrollbar-color:var(--lava) var(--bg); }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid var(--ember); color:var(--ember); padding:8px 20px;
+ border-radius:4px; text-decoration:none; font-size:0.82rem; }
+ .page-nav a:hover { background:var(--lava); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:7px 28px; display:flex; justify-content:center;
+ background:rgba(13,8,2,0.82); backdrop-filter:blur(12px);
+ border-top:1px solid rgba(255,68,0,0.3); }
+ .post { background:rgba(20,8,2,0.72); border:1px solid rgba(255,68,0,0.2); border-radius:8px;
+ padding:20px; margin-bottom:14px; cursor:pointer;
+ transition:all 0.25s; backdrop-filter:blur(4px); }
+ .post:hover { border-color:var(--ember); box-shadow:0 0 20px rgba(255,68,0,0.25); transform:translateY(-2px); }
+ .post-active { border-color:var(--hot) !important; background:rgba(30,8,2,0.9) !important;
+ box-shadow:0 0 24px rgba(255,140,0,0.4),inset 3px 0 0 var(--hot) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-time { color:var(--ember); font-family:monospace; font-size:0.8rem; }
+ .post-text { line-height:1.65; font-size:0.95rem; }
+ .post-text a { color:var(--ember); text-decoration:none; }
+ .post-text a:hover { text-shadow:0 0 8px var(--lava); }
+ .post-audio { width:100%; margin-top:10px; }
+ .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(20,8,2,0.92);
+ border:1px solid var(--lava); border-radius:10px; backdrop-filter:blur(16px);
+ box-shadow:0 0 60px rgba(255,68,0,0.3); padding:40px; }
+ .modal-close { float:right; background:none; border:none; color:var(--ember);
+ font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
+ [data-sno-theme="volcano"] .splash-overlay {
+ background: radial-gradient(ellipse 80% 60% at 50% 100%, rgba(255,68,0,0.35) 0%, transparent 50%), var(--bg);
+ }
+ [data-sno-theme="volcano"] .splash-ember {
+ width:min(200px,55vw); height:4px; margin:0 auto 1.3rem; border-radius:2px;
+ background: linear-gradient(90deg, transparent, var(--lava), var(--hot), var(--ember), transparent);
+ animation: splashEmberPulse 1.6s ease-in-out infinite alternate;
+ box-shadow: 0 0 20px var(--lava), 0 6px 30px rgba(255,68,0,0.4);
+ }
+ @keyframes splashEmberPulse { from { opacity:0.6; transform: scaleX(0.9); } to { opacity:1; transform: scaleX(1); } }
+ [data-sno-theme="volcano"] .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#ffe8cc;
+ text-shadow:0 0 20px var(--lava); }
+ [data-sno-theme="volcano"] .splash-tag { color:var(--ember); letter-spacing:0.15em; }
+ [data-sno-theme="volcano"] .splash-hint { color:rgba(255,232,204,0.88); }
+ [data-sno-theme="volcano"] .splash-inner { text-shadow: 0 2px 18px rgba(0,0,0,0.85); }
diff --git a/internal/generator/templates/themes/volcano.tmpl b/internal/generator/templates/themes/volcano/theme.js
index 982eed8..41de88a 100644
--- a/internal/generator/templates/themes/volcano.tmpl
+++ b/internal/generator/templates/themes/volcano/theme.js
@@ -1,95 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>snonux.foo ▲ VOLCANO</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
- <style>
- :root { --lava:#ff4400; --ember:#ff8c00; --hot:#ffcc00; --bg:#0d0802; }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);
- color:#ffe8cc; overflow:hidden; height:100vh; }
- #three-canvas { position:fixed; top:0; left: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(13,8,2,0.82); backdrop-filter:blur(12px);
- border-bottom:1px solid rgba(255,68,0,0.3); display:flex; align-items:center; justify-content:space-between; }
- .logo { display:flex; align-items:center; gap:14px; }
- .logo-mark { font-size:2rem; font-weight:800; color:var(--ember); text-shadow:0 0 16px var(--lava); }
- .logo-title h1 { font-size:1.5rem; font-weight:700; color:#ffe8cc; }
- .logo-title .subtitle { font-size:0.75rem; color:rgba(255,232,204,0.5); margin-top:2px; }
- .logo-title .subtitle a { color:var(--ember); text-decoration:none; }
- .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--lava); }
- .transmit-btn { border:1px solid var(--lava); color:var(--lava); padding:9px 20px;
- border-radius:4px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; }
- .transmit-btn:hover { background:var(--lava); color:var(--bg); }
- a.header-feed-link { color:var(--ember); }
- a.header-feed-link:hover { color:var(--hot); text-shadow:0 0 8px var(--lava); }
- .nav-hints { background:rgba(13,8,2,0.7); border-bottom:1px solid rgba(255,68,0,0.15);
- color:rgba(255,232,204,0.4); padding:5px 28px; display:flex; gap:18px;
- font-size:0.68rem; flex-wrap:wrap; }
- .nav-hints kbd { background:rgba(255,68,0,0.12); border:1px solid rgba(255,68,0,0.35);
- color:var(--ember); border-radius:3px; padding:0 5px; margin:0 2px; }
- .content { flex:1; overflow-y:auto; padding:20px 28px;
- scrollbar-width:thin; scrollbar-color:var(--lava) var(--bg); }
- .page-nav { display:flex; justify-content:center; margin:14px 0; }
- .page-nav a { border:1px solid var(--ember); color:var(--ember); padding:8px 20px;
- border-radius:4px; text-decoration:none; font-size:0.82rem; }
- .page-nav a:hover { background:var(--lava); color:var(--bg); }
- .page-nav-footer { flex-shrink:0; padding:7px 28px; display:flex; justify-content:center;
- background:rgba(13,8,2,0.82); backdrop-filter:blur(12px);
- border-top:1px solid rgba(255,68,0,0.3); }
- .post { background:rgba(20,8,2,0.72); border:1px solid rgba(255,68,0,0.2); border-radius:8px;
- padding:20px; margin-bottom:14px; cursor:pointer;
- transition:all 0.25s; backdrop-filter:blur(4px); }
- .post:hover { border-color:var(--ember); box-shadow:0 0 20px rgba(255,68,0,0.25); transform:translateY(-2px); }
- .post-active { border-color:var(--hot) !important; background:rgba(30,8,2,0.9) !important;
- box-shadow:0 0 24px rgba(255,140,0,0.4),inset 3px 0 0 var(--hot) !important; }
- .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
- .post-time { color:var(--ember); font-family:monospace; font-size:0.8rem; }
- .post-text { line-height:1.65; font-size:0.95rem; }
- .post-text a { color:var(--ember); text-decoration:none; }
- .post-text a:hover { text-shadow:0 0 8px var(--lava); }
- .post-audio { width:100%; margin-top:10px; }
- .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(20,8,2,0.92);
- border:1px solid var(--lava); border-radius:10px; backdrop-filter:blur(16px);
- box-shadow:0 0 60px rgba(255,68,0,0.3); padding:40px; }
- .modal-close { float:right; background:none; border:none; color:var(--ember);
- font-size:0.9rem; cursor:pointer; letter-spacing:1px; }
- @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} }
- .splash-overlay.splash-volcano {
- background: radial-gradient(ellipse 80% 60% at 50% 100%, rgba(255,68,0,0.35) 0%, transparent 50%), var(--bg);
- }
- .splash-volcano .splash-ember {
- width:min(200px,55vw); height:4px; margin:0 auto 1.3rem; border-radius:2px;
- background: linear-gradient(90deg, transparent, var(--lava), var(--hot), var(--ember), transparent);
- animation: splashEmberPulse 1.6s ease-in-out infinite alternate;
- box-shadow: 0 0 20px var(--lava), 0 6px 30px rgba(255,68,0,0.4);
- }
- @keyframes splashEmberPulse { from { opacity:0.6; transform: scaleX(0.9); } to { opacity:1; transform: scaleX(1); } }
- .splash-volcano .splash-title { font-size:clamp(1.45rem,4.5vw,2rem); color:#ffe8cc;
- text-shadow:0 0 20px var(--lava); }
- .splash-volcano .splash-tag { color:var(--ember); letter-spacing:0.15em; }
- .splash-volcano .splash-hint { color:rgba(255,232,204,0.88); }
- .splash-volcano .splash-inner { text-shadow: 0 2px 18px rgba(0,0,0,0.85); }
-{{template "navSharedCSSInner"}}
- </style>
-</head>
-<body>
- {{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-volcano" 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-inner">
- <div class="splash-ember" aria-hidden="true"></div>
- <div class="splash-title">snonux.foo</div>
- <div class="splash-tag">Volcano vent</div>
- <div class="splash-hint">Erupt into feed — click or Enter</div>
- </div>
- </div>
- <script>
+
(function(){
if(document.documentElement.classList.contains('sno-splash-skip'))return;
var cv=document.getElementById('splash-gl-canvas');
@@ -111,46 +20,8 @@
pos.needsUpdate=true;cone.rotation.y=t*0.25;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">Transmit</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>
+
+
// Volcano WebGL: glowing lava floor, molten rock boulders, smoke plumes,
// underground furnace glow sphere, and 3000 rising ember particles.
(function() {
@@ -394,7 +265,3 @@
if (ov) { ov.classList.add('sno-fx-zoom'); setTimeout(function() { ov.classList.remove('sno-fx-zoom'); }, 330); }
};
})();
- </script>
- {{template "navscript" .}}
-</body>
-</html>
diff --git a/internal/generator/themes.go b/internal/generator/themes.go
index e7fd3a3..a3a9881 100644
--- a/internal/generator/themes.go
+++ b/internal/generator/themes.go
@@ -6,12 +6,12 @@ import (
"codeberg.org/snonux/snonux/internal/generator/templates"
)
-// fallbackThemeName is returned by getTheme when an unknown name is requested,
-// matching the previous behaviour of the hand-maintained themeRegistry map.
+// fallbackThemeName is used when an unknown name is requested for default
+// metadata, matching the previous behaviour of the per-theme registry.
const fallbackThemeName = "neon"
// themeSet caches the list of theme names available in the embedded template FS
-// so ListThemes and getTheme do not re-read the directory on every call.
+// so ListThemes does not re-read the directory on every call.
var themeSet = loadThemeSet()
func loadThemeSet() map[string]struct{} {
@@ -19,7 +19,7 @@ func loadThemeSet() map[string]struct{} {
if err != nil {
// At build time the embed //go:embed directive guarantees the FS is
// populated, so this should never happen; log and continue with an
- // empty set so getTheme() falls back cleanly.
+ // empty set so callers can fall back cleanly.
log.Printf("warning: could not enumerate themes from embedded FS: %v", err)
return map[string]struct{}{}
}
@@ -31,25 +31,14 @@ func loadThemeSet() map[string]struct{} {
return out
}
-// getTheme returns the HTML template body for the given theme name, loading it
-// from the embedded template FS. It falls back to the neon theme if the name
-// is unknown (preserving previous behaviour of the hand-maintained map).
-func getTheme(name string) string {
- if _, ok := themeSet[name]; !ok {
- name = fallbackThemeName
+// validThemeName returns name if it is a known theme, otherwise the fallback.
+// Callers use this to coerce CLI input ("--theme random" already resolves
+// upstream) so downstream lookups never miss.
+func validThemeName(name string) string {
+ if _, ok := themeSet[name]; ok {
+ return name
}
-
- body, err := templates.Theme(name)
- if err != nil {
- // Last-resort fallback: try neon. If that also fails, return an empty
- // string; template.Parse will then produce a diagnostic error.
- if body, err = templates.Theme(fallbackThemeName); err != nil {
- log.Printf("warning: could not load fallback theme %q: %v", fallbackThemeName, err)
- return ""
- }
- }
-
- return body
+ return fallbackThemeName
}
// ListThemes returns a sorted list of all available theme names.
diff --git a/internal/version/version.go b/internal/version/version.go
index 89feed0..a75ed1e 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -2,4 +2,4 @@
package version
// Version is the application version (semantic versioning).
-const Version = "0.11.0"
+const Version = "0.12.0"