summaryrefslogtreecommitdiff
path: root/internal/generator/templates
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 /internal/generator/templates
parentcc9c69ce748416ad1d9b28592ec463434023d63c (diff)
can swap themes dynamicallyv0.12.0
Diffstat (limited to 'internal/generator/templates')
-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
71 files changed, 5219 insertions, 5745 deletions
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 step