65ba1ec7dc
Chakra Petch site-wide (300-700) + JetBrains Mono variable, all self-hosted (CSP zero-external holds). Token layer gains the mock-exact text tiers, glass panel, cyan-tinted borders, glows, tighter radii; light 'blueprint' variants included. New Constellation.astro: the six real nodes as IC chips on dual-trace PCB paths (cyan out / magenta back, dasharray packets), radar ring on the cave hub, vias at bends - pure SVG + scoped CSS keyframes, full reduced-motion fallback. Hero rebuilt: scrim stack + masked grid over the board art, two-line gradient nameplate, typing bar cycling six real fleet facts via the external site.js (facts from src/data/fleet.ts), scroll indicator. Facts wrap, never clip.
89 lines
3.3 KiB
JavaScript
89 lines
3.3 KiB
JavaScript
/* Single external script (no inline JS anywhere → strict script-src 'self').
|
|
Loaded blocking in <head> so the theme is set before first paint (no flash). */
|
|
(function () {
|
|
// --- Pre-paint theme -----------------------------------------------------
|
|
try {
|
|
var stored = localStorage.getItem("theme");
|
|
var theme =
|
|
stored ||
|
|
(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
|
document.documentElement.dataset.theme = theme;
|
|
} catch (e) {
|
|
document.documentElement.dataset.theme = "dark";
|
|
}
|
|
|
|
// Mark JS as live BEFORE first paint. CSS only hides the pre-reveal animation
|
|
// state under `html.js`, so with JS off every element stays visible (the
|
|
// Motion layer in /_astro reveals them once it runs). Progressive enhancement.
|
|
document.documentElement.classList.add("js");
|
|
|
|
function onReady(fn) {
|
|
if (document.readyState !== "loading") fn();
|
|
else document.addEventListener("DOMContentLoaded", fn);
|
|
}
|
|
|
|
onReady(function () {
|
|
// --- Theme toggle ------------------------------------------------------
|
|
var btn = document.getElementById("theme-toggle");
|
|
if (btn) {
|
|
btn.addEventListener("click", function () {
|
|
var root = document.documentElement;
|
|
var next = root.dataset.theme === "light" ? "dark" : "light";
|
|
root.dataset.theme = next;
|
|
try {
|
|
localStorage.setItem("theme", next);
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Hero typing loop (fleet facts) -------------------------------------
|
|
// Facts arrive via a data attribute (no inline JS → script-src 'self' holds).
|
|
// Reduced motion or JS-off: the first fact renders statically from markup.
|
|
var factBox = document.getElementById("fleet-fact");
|
|
var factText = document.getElementById("fleet-fact-text");
|
|
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (factBox && factText && !reduced) {
|
|
var facts;
|
|
try {
|
|
facts = JSON.parse(factBox.dataset.facts || "[]");
|
|
} catch (e) {
|
|
facts = [];
|
|
}
|
|
if (facts.length > 1) {
|
|
var fi = 0; // current fact
|
|
var ci = facts[0].length; // start fully typed (matches SSR markup)
|
|
var deleting = false;
|
|
var tick = function () {
|
|
var fact = facts[fi];
|
|
if (!deleting && ci === fact.length) {
|
|
// hold at end of fact, then start the next
|
|
deleting = true;
|
|
setTimeout(tick, 3200);
|
|
return;
|
|
}
|
|
if (deleting) {
|
|
// swap instantly to the next fact (clean cut, like a terminal)
|
|
deleting = false;
|
|
fi = (fi + 1) % facts.length;
|
|
ci = 0;
|
|
factText.textContent = "";
|
|
setTimeout(tick, 260);
|
|
return;
|
|
}
|
|
ci++;
|
|
factText.textContent = facts[fi].slice(0, ci);
|
|
setTimeout(tick, 45);
|
|
};
|
|
setTimeout(tick, 3200);
|
|
}
|
|
}
|
|
|
|
// Scroll reveals, hero entrance, backdrop parallax, scroll-progress bar,
|
|
// nav underline and button springs all live in the Motion layer
|
|
// (src/scripts/anim.ts → bundled /_astro/*.js). This file owns only the
|
|
// pre-paint theme + the typing loop so it stays a tiny blocking <head> script.
|
|
});
|
|
})();
|