From ff0abee349b56bebf589c6f1594bc68c964457c1 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Sat, 16 May 2026 09:35:45 -0700 Subject: [PATCH] Drop turtle, honor ?template= URL param, add warp easter egg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed the cosmos turtle (CSS rules + auto-load probe + JS hook). Reduced-motion turtle branch removed too. - ?template=editorial|swiss|cosmos URL param now wins over the value in data/links.json (and the cached localStorage fallback). Wired in both the pre-paint boot script and main() so deep links work without flashing the wrong template. - New hidden easter egg: type the letters w-a-r-p anywhere on the page (no input focused) and the page reloads on the next template in the cycle editorial -> swiss -> cosmos -> editorial. The URL is updated with the new ?template= param so the override survives refresh and is shareable. A small "↯ " toast pops up after the reload via sessionStorage handoff, fades after ~2.5s, respects prefers-reduced-motion. - README documents both the URL param and the warp keyword. --- README.md | 15 +++++++++++ assets/css/cosmos.css | 44 +----------------------------- assets/css/styles.css | 30 +++++++++++++++++++++ assets/js/app.js | 63 +++++++++++++++++++++++++++++++------------ index.html | 13 ++++++--- 5 files changed, 102 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index b8c973c..8495103 100644 --- a/README.md +++ b/README.md @@ -439,6 +439,21 @@ To switch: Choice is also cached in `localStorage` (`dlstack-template`) so reloads don't flash the wrong template before JSON parses. +### Overriding the template at view time + +Two ways to ride a different template without touching `links.json`: + +1. **URL parameter** — `?template=editorial`, `?template=swiss`, or + `?template=cosmos` always wins over the JSON value, so links like + `https://your.site/?template=cosmos` are shareable. +2. **Easter egg** — type `warp` anywhere on the page (no input field + focused). The page reloads on the next template in the cycle and a + small `↯ ` toast confirms it. The URL is updated with the new + `?template=` param. + +Drop the URL param (or refresh from a clean URL) to return to whatever +`theme.template` says in `data/links.json`. + ## Theme (light / dark / auto) A small pill in the top-right cycles **Auto → Light → Dark → Auto** on diff --git a/assets/css/cosmos.css b/assets/css/cosmos.css index b13b6d1..931e9fb 100644 --- a/assets/css/cosmos.css +++ b/assets/css/cosmos.css @@ -153,43 +153,6 @@ transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1); } -/* galactic space turtle — floats in from the left, dips, then back out */ -:root[data-template="cosmos"] .cosmos-turtle { - position: fixed; - top: clamp(48px, 9vh, 110px); - left: 0; - width: clamp(140px, 16vw, 240px); - height: auto; - z-index: 3; - pointer-events: none; - opacity: 0; - filter: - drop-shadow(0 0 22px rgba(122, 247, 255, 0.55)) - drop-shadow(0 0 48px rgba(176, 122, 255, 0.45)) - drop-shadow(0 0 80px rgba(255, 78, 205, 0.25)); - animation: - cosmos-turtle-cycle 28s 2s linear infinite, - cosmos-turtle-bob 4s ease-in-out infinite; - will-change: transform, opacity; -} -@keyframes cosmos-turtle-cycle { - 0% { transform: translate(-25vw, 0) rotate(-6deg); opacity: 0; } - 6% { opacity: 1; } - 45% { transform: translate(45vw, -4vh) rotate(4deg); opacity: 1; } - 88% { transform: translate(112vw, 0) rotate(8deg); opacity: 1; } - 93% { opacity: 0; } - 100% { transform: translate(112vw, 0) rotate(8deg); opacity: 0; } -} -@keyframes cosmos-turtle-bob { - 0%, 100% { filter: - drop-shadow(0 0 22px rgba(122, 247, 255, 0.55)) - drop-shadow(0 0 48px rgba(176, 122, 255, 0.45)) - drop-shadow(0 0 80px rgba(255, 78, 205, 0.25)); } - 50% { filter: - drop-shadow(0 0 32px rgba(122, 247, 255, 0.75)) - drop-shadow(0 0 64px rgba(176, 122, 255, 0.60)) - drop-shadow(0 0 110px rgba(255, 78, 205, 0.40)); } -} /* CSS-only static fallback nebula when WebGL is unavailable */ :root[data-template="cosmos"].cosmos-static body { @@ -775,18 +738,13 @@ :root[data-template="cosmos"] .hero__asterism, :root[data-template="cosmos"] .section__head::after, :root[data-template="cosmos"] .card::before, - :root[data-template="cosmos"] .cosmos-comet, - :root[data-template="cosmos"] .cosmos-turtle { + :root[data-template="cosmos"] .cosmos-comet { animation: none !important; transition: none !important; } :root[data-template="cosmos"] .cosmos-bg, :root[data-template="cosmos"] .cosmos-halo, :root[data-template="cosmos"] .cosmos-comet { display: none; } - :root[data-template="cosmos"] .cosmos-turtle { - opacity: 1; - transform: translate(2vw, 0) rotate(-2deg); - } :root[data-template="cosmos"] .hero__name, :root[data-template="cosmos"] .hero__tagline, :root[data-template="cosmos"] .hero__asterism { transform: none !important; } diff --git a/assets/css/styles.css b/assets/css/styles.css index 7a464d9..94a02d3 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -696,3 +696,33 @@ body::before { } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } + +/* warp toast — surfaces the active template after the easter egg fires */ +.warp-toast { + position: fixed; + bottom: clamp(1.25rem, 3vw, 2.25rem); + left: 50%; + z-index: 1000; + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 1.1rem; + background: var(--ink); + color: var(--paper); + font: 600 var(--fs-sm)/1 var(--mono); + text-transform: uppercase; + letter-spacing: 0.18em; + border-radius: 999px; + box-shadow: 0 12px 30px -16px rgba(0, 0, 0, 0.5); + opacity: 0; + transform: translate(-50%, 18px); + animation: warp-in 360ms cubic-bezier(0.22, 1, 0.36, 1) forwards; + pointer-events: none; +} +.warp-toast__arrow { color: var(--accent); font-size: 1.1em; } +.warp-toast--out { animation: warp-out 600ms ease forwards; } +@keyframes warp-in { to { opacity: 1; transform: translate(-50%, 0); } } +@keyframes warp-out { to { opacity: 0; transform: translate(-50%, 18px); } } +@media (prefers-reduced-motion: reduce) { + .warp-toast { animation: none; opacity: 1; transform: translate(-50%, 0); } +} diff --git a/assets/js/app.js b/assets/js/app.js index 9e335bc..cbb5e53 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -284,10 +284,9 @@ }); } - function bootCosmos(theme) { + function bootCosmos() { const root = document.documentElement; const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches; - attachCosmosTurtle(theme); if (reducedMotion) { root.classList.add("cosmos-static"); return; } attachCosmosCursor(); attachCosmosComets(); @@ -566,19 +565,44 @@ }); } - function attachCosmosTurtle(theme) { - const src = (theme && theme.turtle) || "assets/img/turtle.png"; - const probe = new Image(); - probe.onload = () => { - const t = document.createElement("img"); - t.src = src; - t.alt = ""; - t.className = "cosmos-turtle"; - t.setAttribute("aria-hidden", "true"); - document.body.appendChild(t); - }; - probe.onerror = () => {}; - probe.src = src; + function attachWarpEgg() { + const order = ["editorial", "swiss", "cosmos"]; + let buf = ""; + const KEY = "warp"; + + document.addEventListener("keydown", (e) => { + const tgt = e.target; + if (tgt && tgt.matches && tgt.matches("input, textarea, [contenteditable]")) return; + if (!e.key || e.key.length !== 1) return; + buf = (buf + e.key.toLowerCase()).slice(-KEY.length); + if (buf !== KEY) return; + buf = ""; + const cur = document.documentElement.dataset.template || "editorial"; + const next = order[(order.indexOf(cur) + 1) % order.length]; + try { sessionStorage.setItem("dlstack-warp-toast", next); } catch (err) {} + const url = new URL(location.href); + url.searchParams.set("template", next); + location.replace(url.toString()); + }); + + let pending = null; + try { pending = sessionStorage.getItem("dlstack-warp-toast"); } catch (err) {} + if (!pending) return; + try { sessionStorage.removeItem("dlstack-warp-toast"); } catch (err) {} + const toast = document.createElement("div"); + toast.className = "warp-toast"; + toast.setAttribute("role", "status"); + const arrow = document.createElement("span"); + arrow.className = "warp-toast__arrow"; + arrow.setAttribute("aria-hidden", "true"); + arrow.textContent = "↯"; + const label = document.createElement("span"); + label.className = "warp-toast__label"; + label.textContent = pending; + toast.append(arrow, label); + document.body.appendChild(toast); + setTimeout(() => toast.classList.add("warp-toast--out"), 1600); + setTimeout(() => toast.remove(), 2500); } function attachCarousels(root) { @@ -676,10 +700,14 @@ document.documentElement.style.setProperty("--accent", data.theme.accent); } const validTpl = new Set(["editorial", "swiss", "cosmos"]); - const tpl = validTpl.has(data.theme?.template) ? data.theme.template : "editorial"; + let urlTpl = null; + try { urlTpl = new URL(location.href).searchParams.get("template"); } catch (e) {} + const tpl = validTpl.has(urlTpl) ? urlTpl + : validTpl.has(data.theme?.template) ? data.theme.template + : "editorial"; document.documentElement.dataset.template = tpl; try { localStorage.setItem("dlstack-template", tpl); } catch (e) {} - if (tpl === "cosmos") bootCosmos(data.theme || {}); + if (tpl === "cosmos") bootCosmos(); const p = data.profile || {}; const sections = data.sections || []; @@ -719,6 +747,7 @@ attachCarousels(app); attachReveal(app); attachTheme(); + attachWarpEgg(); } if (document.readyState === "loading") { diff --git a/index.html b/index.html index 6fe2451..87db073 100644 --- a/index.html +++ b/index.html @@ -22,12 +22,19 @@