Drop turtle, honor ?template= URL param, add warp easter egg

- 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 "↯ <name>" 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.
This commit is contained in:
Joel Brock
2026-05-16 09:35:45 -07:00
parent c16ee37096
commit ff0abee349
5 changed files with 102 additions and 63 deletions

View File

@@ -439,6 +439,21 @@ To switch:
Choice is also cached in `localStorage` (`dlstack-template`) so reloads Choice is also cached in `localStorage` (`dlstack-template`) so reloads
don't flash the wrong template before JSON parses. 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 `↯ <name>` 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) ## Theme (light / dark / auto)
A small pill in the top-right cycles **Auto → Light → Dark → Auto** on A small pill in the top-right cycles **Auto → Light → Dark → Auto** on

View File

@@ -153,43 +153,6 @@
transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1); 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 */ /* CSS-only static fallback nebula when WebGL is unavailable */
:root[data-template="cosmos"].cosmos-static body { :root[data-template="cosmos"].cosmos-static body {
@@ -775,18 +738,13 @@
:root[data-template="cosmos"] .hero__asterism, :root[data-template="cosmos"] .hero__asterism,
:root[data-template="cosmos"] .section__head::after, :root[data-template="cosmos"] .section__head::after,
:root[data-template="cosmos"] .card::before, :root[data-template="cosmos"] .card::before,
:root[data-template="cosmos"] .cosmos-comet, :root[data-template="cosmos"] .cosmos-comet {
:root[data-template="cosmos"] .cosmos-turtle {
animation: none !important; animation: none !important;
transition: none !important; transition: none !important;
} }
:root[data-template="cosmos"] .cosmos-bg, :root[data-template="cosmos"] .cosmos-bg,
:root[data-template="cosmos"] .cosmos-halo, :root[data-template="cosmos"] .cosmos-halo,
:root[data-template="cosmos"] .cosmos-comet { display: none; } :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__name,
:root[data-template="cosmos"] .hero__tagline, :root[data-template="cosmos"] .hero__tagline,
:root[data-template="cosmos"] .hero__asterism { transform: none !important; } :root[data-template="cosmos"] .hero__asterism { transform: none !important; }

View File

@@ -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; } .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); }
}

View File

@@ -284,10 +284,9 @@
}); });
} }
function bootCosmos(theme) { function bootCosmos() {
const root = document.documentElement; const root = document.documentElement;
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches; const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
attachCosmosTurtle(theme);
if (reducedMotion) { root.classList.add("cosmos-static"); return; } if (reducedMotion) { root.classList.add("cosmos-static"); return; }
attachCosmosCursor(); attachCosmosCursor();
attachCosmosComets(); attachCosmosComets();
@@ -566,19 +565,44 @@
}); });
} }
function attachCosmosTurtle(theme) { function attachWarpEgg() {
const src = (theme && theme.turtle) || "assets/img/turtle.png"; const order = ["editorial", "swiss", "cosmos"];
const probe = new Image(); let buf = "";
probe.onload = () => { const KEY = "warp";
const t = document.createElement("img");
t.src = src; document.addEventListener("keydown", (e) => {
t.alt = ""; const tgt = e.target;
t.className = "cosmos-turtle"; if (tgt && tgt.matches && tgt.matches("input, textarea, [contenteditable]")) return;
t.setAttribute("aria-hidden", "true"); if (!e.key || e.key.length !== 1) return;
document.body.appendChild(t); buf = (buf + e.key.toLowerCase()).slice(-KEY.length);
}; if (buf !== KEY) return;
probe.onerror = () => {}; buf = "";
probe.src = src; 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) { function attachCarousels(root) {
@@ -676,10 +700,14 @@
document.documentElement.style.setProperty("--accent", data.theme.accent); document.documentElement.style.setProperty("--accent", data.theme.accent);
} }
const validTpl = new Set(["editorial", "swiss", "cosmos"]); 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; document.documentElement.dataset.template = tpl;
try { localStorage.setItem("dlstack-template", tpl); } catch (e) {} try { localStorage.setItem("dlstack-template", tpl); } catch (e) {}
if (tpl === "cosmos") bootCosmos(data.theme || {}); if (tpl === "cosmos") bootCosmos();
const p = data.profile || {}; const p = data.profile || {};
const sections = data.sections || []; const sections = data.sections || [];
@@ -719,6 +747,7 @@
attachCarousels(app); attachCarousels(app);
attachReveal(app); attachReveal(app);
attachTheme(); attachTheme();
attachWarpEgg();
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {

View File

@@ -22,12 +22,19 @@
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;900&family=DM+Serif+Display:ital@0;1&family=Fraunces:opsz,wght,SOFT,WONK@9..144,300..600,30..100,0..1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&family=Orbitron:wght@400;500;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;900&family=DM+Serif+Display:ital@0;1&family=Fraunces:opsz,wght,SOFT,WONK@9..144,300..600,30..100,0..1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&family=Orbitron:wght@400;500;700;900&display=swap" rel="stylesheet">
<script> <script>
// Apply saved theme + template before paint to avoid flash // Apply saved theme + template before paint to avoid flash.
// URL ?template=… wins over the cached value so deep links work.
try { try {
var t = localStorage.getItem("dlstack-theme"); var t = localStorage.getItem("dlstack-theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t; if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
var tpl = localStorage.getItem("dlstack-template"); var qtpl = null;
if (tpl === "swiss" || tpl === "editorial" || tpl === "cosmos") document.documentElement.dataset.template = tpl; try { qtpl = new URL(location.href).searchParams.get("template"); } catch (e) {}
if (qtpl === "swiss" || qtpl === "editorial" || qtpl === "cosmos") {
document.documentElement.dataset.template = qtpl;
} else {
var tpl = localStorage.getItem("dlstack-template");
if (tpl === "swiss" || tpl === "editorial" || tpl === "cosmos") document.documentElement.dataset.template = tpl;
}
} catch (e) {} } catch (e) {}
</script> </script>