facelift: fleet constellation hero + Bat-Computer type/token layer

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.
This commit is contained in:
2026-07-03 18:58:49 +10:00
parent 08bab080b1
commit 65ba1ec7dc
12 changed files with 736 additions and 61 deletions
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+43 -1
View File
@@ -38,9 +38,51 @@
});
}
// --- 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 so it can stay a tiny blocking <head> script.
// pre-paint theme + the typing loop so it stays a tiny blocking <head> script.
});
})();
+42
View File
@@ -0,0 +1,42 @@
import { chromium } from "playwright";
import { preview } from "astro";
const server = await preview({ server: { port: 4399 } });
const base = "http://localhost:4399";
const browser = await chromium.launch();
async function shot(name, { theme = "dark", scrollY = 0, selector = null, path = "/", fullPage = false } = {}) {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 2,
});
await page.addInitScript((t) => {
try { localStorage.setItem("theme", t); } catch {}
}, theme);
await page.goto(base + path, { waitUntil: "networkidle" });
await page.evaluate(
(t) => document.documentElement.setAttribute("data-theme", t),
theme
);
if (scrollY) {
await page.evaluate((y) => window.scrollTo(0, y), scrollY);
await page.waitForTimeout(400);
}
await page.waitForTimeout(2700);
if (selector) {
const el = await page.$(selector);
await el.scrollIntoViewIfNeeded();
await page.waitForTimeout(2700);
await el.screenshot({ path: `/tmp/site-${name}.png` });
} else {
await page.screenshot({ path: `/tmp/site-${name}.png`, fullPage });
}
await page.close();
console.log("wrote /tmp/site-" + name + ".png");
}
await shot("light-hero", { theme: "light" });
await shot("dark-hero", { theme: "dark" });
await browser.close();
await server.stop();
+243
View File
@@ -0,0 +1,243 @@
---
// The fleet constellation — 6 real nodes as IC chips on PCB traces.
// Pure SVG + scoped CSS keyframes (zero JS; reduced-motion turns every animation off).
// Anatomy per the design handoff: each route is TWO parallel traces (offset ±(2,-3)) —
// one carries a cyan packet outbound, the other a magenta packet inbound. Packets are
// stroke-dasharray 9/127 sliding via stroke-dashoffset. Hub = cave (56px chip, radar ring).
---
<svg
class="constellation"
viewBox="0 0 760 640"
role="img"
aria-label="Live topology of the six-node homelab fleet: hub cave reconciling alfred, robin, kate, wgirl and elfastc"
>
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="var(--accent)"></stop>
<stop offset="1" stop-color="var(--accent-2)"></stop>
</linearGradient>
</defs>
<!-- ── PCB traces: dual parallel lines per route (out = cyan, back = magenta) ── -->
<!-- alfred -->
<g transform="translate(2,-3)">
<path class="t-glow t-cyan" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-base t-cyan" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-pkt pkt-fwd" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
</g>
<g transform="translate(-2,3)">
<path class="t-glow t-mag" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-base t-mag" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-pkt t-pkt--mag pkt-rev-slow" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
</g>
<!-- robin -->
<g transform="translate(2,-3)">
<path class="t-glow t-cyan" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-base t-cyan" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-pkt pkt-fwd-slow" d="M380 310 H470 L540 380 H602 L640 420"></path>
</g>
<g transform="translate(-2,3)">
<path class="t-glow t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-base t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-pkt t-pkt--mag pkt-rev" d="M380 310 H470 L540 380 H602 L640 420"></path>
</g>
<!-- kate (standby — dimmer) -->
<g transform="translate(2,-3)" class="standby">
<path class="t-glow t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-base t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-pkt t-pkt--solid pkt-fwd-slow" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
</g>
<g transform="translate(-2,3)" class="standby">
<path class="t-glow t-mag" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-base t-mag" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
</g>
<!-- wgirl (standby) -->
<g transform="translate(2,-3)" class="standby">
<path class="t-glow t-cyan" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-base t-cyan" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-pkt t-pkt--solid pkt-fwd" d="M380 310 H310 L244 376 V446 L214 474"></path>
</g>
<g transform="translate(-2,3)" class="standby">
<path class="t-glow t-mag" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-base t-mag" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev-slow" d="M380 310 H310 L244 376 V446 L214 474"></path>
</g>
<!-- elfastc (standby) -->
<g transform="translate(3,2)" class="standby">
<path class="t-glow t-cyan" d="M380 310 V400 L452 472 V532"></path>
<path class="t-base t-cyan" d="M380 310 V400 L452 472 V532"></path>
<path class="t-pkt t-pkt--solid pkt-fwd-slow" d="M380 310 V400 L452 472 V532"></path>
</g>
<g transform="translate(-3,-2)" class="standby">
<path class="t-glow t-mag" d="M380 310 V400 L452 472 V532"></path>
<path class="t-base t-mag" d="M380 310 V400 L452 472 V532"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev" d="M380 310 V400 L452 472 V532"></path>
</g>
<!-- vias at trace bends -->
<circle class="via via--cyan" cx="460" cy="310" r="3"></circle>
<circle class="via via--cyan" cx="520" cy="250" r="3"></circle>
<circle class="via via--cyan" cx="520" cy="180" r="3"></circle>
<circle class="via via--mag" cx="470" cy="310" r="3"></circle>
<circle class="via via--mag" cx="540" cy="380" r="3"></circle>
<circle class="via via--dim" cx="300" cy="310" r="3"></circle>
<circle class="via via--dim" cx="240" cy="250" r="3"></circle>
<circle class="via via--dim" cx="310" cy="310" r="3"></circle>
<circle class="via via--dim" cx="244" cy="376" r="3"></circle>
<circle class="via via--dim" cx="380" cy="400" r="3"></circle>
<circle class="via via--dim" cx="452" cy="472" r="3"></circle>
<!-- ── hub: cave ── -->
<circle class="ring" cx="380" cy="310" r="46" fill="none" stroke="url(#lgA)" stroke-width="1.5"></circle>
<circle cx="380" cy="310" r="46" fill="rgba(63,186,245,0.05)" stroke="rgba(63,186,245,0.3)" stroke-width="1"></circle>
<!-- pin stubs -->
<g class="pins-cyan">
<line x1="352" y1="296" x2="340" y2="296"></line><line x1="352" y1="310" x2="340" y2="310"></line>
<line x1="352" y1="324" x2="340" y2="324"></line><line x1="366" y1="282" x2="366" y2="270"></line>
<line x1="380" y1="282" x2="380" y2="270"></line><line x1="366" y1="338" x2="366" y2="350"></line>
<line x1="380" y1="338" x2="380" y2="350"></line>
</g>
<g class="pins-mag">
<line x1="408" y1="296" x2="420" y2="296"></line><line x1="408" y1="310" x2="420" y2="310"></line>
<line x1="408" y1="324" x2="420" y2="324"></line><line x1="394" y1="282" x2="394" y2="270"></line>
<line x1="394" y1="338" x2="394" y2="350"></line>
</g>
<rect class="chip chip--hub" x="352" y="282" width="56" height="56" rx="8" stroke="url(#lgA)"></rect>
<rect x="364" y="294" width="32" height="32" rx="4" fill="none" stroke="rgba(63,186,245,0.4)" stroke-width="1"></rect>
<circle class="core core--cyan pulse" cx="380" cy="310" r="5"></circle>
<rect class="plate plate--hub" x="338" y="352" width="84" height="24" rx="4"></rect>
<text class="lbl lbl--hub" x="380" y="368" text-anchor="middle">cave</text>
<text class="cap cap--hub" x="380" y="392" text-anchor="middle">139 apps · reconciling</text>
<!-- ── awake: alfred ── -->
<circle cx="612" cy="128" r="24" fill="rgba(63,186,245,0.07)"></circle>
<rect class="chip chip--cyan" x="595" y="111" width="34" height="34" rx="5"></rect>
<circle class="core core--cyan pulse" cx="612" cy="128" r="4"></circle>
<rect class="plate plate--cyan" x="576" y="152" width="72" height="20" rx="3"></rect>
<text class="lbl" x="612" y="166" text-anchor="middle">alfred</text>
<!-- ── awake: robin ── -->
<circle cx="656" cy="420" r="24" fill="rgba(232,121,249,0.07)"></circle>
<rect class="chip chip--mag" x="639" y="403" width="34" height="34" rx="5"></rect>
<circle class="core core--mag pulse-slow" cx="656" cy="420" r="4"></circle>
<rect class="plate plate--mag" x="620" y="444" width="72" height="20" rx="3"></rect>
<text class="lbl" x="656" y="458" text-anchor="middle">robin</text>
<!-- ── standby: kate ── -->
<rect class="chip chip--cyan" x="145" y="135" width="30" height="30" rx="5"></rect>
<circle class="core core--cyan pulse-slow" cx="160" cy="150" r="3.5"></circle>
<rect class="plate plate--dim" x="126" y="172" width="68" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="160" y="186" text-anchor="middle">kate</text>
<text class="cap cap--dim" x="160" y="206" text-anchor="middle">standby · 33 apps known</text>
<!-- ── standby: wgirl ── -->
<rect class="chip chip--mag" x="199" y="471" width="30" height="30" rx="5"></rect>
<circle class="core core--mag pulse" cx="214" cy="486" r="3.5"></circle>
<rect class="plate plate--dim" x="180" y="508" width="68" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="214" y="522" text-anchor="middle">wgirl</text>
<!-- ── standby: elfastc ── -->
<rect class="chip chip--cyan" x="437" y="533" width="30" height="30" rx="5"></rect>
<circle class="core core--cyan pulse-slow" cx="452" cy="548" r="3.5"></circle>
<rect class="plate plate--dim" x="412" y="570" width="80" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="452" y="584" text-anchor="middle">elfastc</text>
</svg>
<style>
.constellation {
display: block;
width: 100%;
height: auto;
}
/* trace layers */
.t-glow { fill: none; stroke-width: 7; }
.t-glow.t-cyan { stroke: rgba(63, 186, 245, 0.12); }
.t-glow.t-mag { stroke: rgba(232, 121, 249, 0.1); }
.standby .t-glow.t-cyan { stroke: rgba(63, 186, 245, 0.1); }
.standby .t-glow.t-mag { stroke: rgba(232, 121, 249, 0.08); }
.t-base { fill: none; stroke-width: 2; }
.t-base.t-cyan { stroke: rgba(63, 186, 245, 0.45); }
.t-base.t-mag { stroke: rgba(232, 121, 249, 0.38); }
.standby .t-base.t-cyan { stroke: rgba(63, 186, 245, 0.35); }
.standby .t-base.t-mag { stroke: rgba(232, 121, 249, 0.3); }
/* the moving packets */
.t-pkt {
fill: none;
stroke: url(#lgA);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 9 127;
filter: drop-shadow(0 0 6px rgba(63, 186, 245, 0.9));
}
.t-pkt--mag { filter: drop-shadow(0 0 6px rgba(232, 121, 249, 0.9)); }
.t-pkt--solid { stroke: rgba(63, 186, 245, 0.85); }
.t-pkt--solid.t-pkt--mag { stroke: rgba(232, 121, 249, 0.85); }
.pkt-fwd { animation: bzDash 8s linear infinite; }
.pkt-fwd-slow { animation: bzDash 11s linear infinite; }
.pkt-rev { animation: bzDashRev 9s linear infinite; }
.pkt-rev-slow { animation: bzDashRev 13s linear infinite; }
/* vias */
.via { fill: var(--panel-solid); stroke-width: 1; }
.via--cyan { stroke: rgba(63, 186, 245, 0.6); }
.via--mag { stroke: rgba(232, 121, 249, 0.6); }
.via--dim { stroke: rgba(126, 139, 161, 0.5); }
/* chips + cores + plates + labels */
.chip { fill: var(--panel-solid); }
.chip--hub { stroke-width: 2; filter: drop-shadow(0 0 10px rgba(63, 186, 245, 0.45)); }
.chip--cyan { stroke: var(--accent); stroke-width: 1.5; filter: drop-shadow(0 0 8px rgba(63, 186, 245, 0.55)); }
.chip--mag { stroke: var(--accent-2); stroke-width: 1.5; filter: drop-shadow(0 0 8px rgba(232, 121, 249, 0.55)); }
.core--cyan { fill: var(--accent); filter: drop-shadow(0 0 6px rgba(63, 186, 245, 1)); }
.core--mag { fill: var(--accent-2); filter: drop-shadow(0 0 5px rgba(232, 121, 249, 1)); }
.pulse { animation: bzPulse 2.4s ease-in-out infinite; }
.pulse-slow { animation: bzPulse 3.4s ease-in-out infinite; }
.pins-cyan line { stroke: rgba(63, 186, 245, 0.6); stroke-width: 1.5; }
.pins-mag line { stroke: rgba(232, 121, 249, 0.6); stroke-width: 1.5; }
.plate { fill: rgba(13, 19, 34, 0.92); stroke-width: 1; }
.plate--hub { stroke: rgba(63, 186, 245, 0.4); }
.plate--cyan { stroke: rgba(63, 186, 245, 0.35); }
.plate--mag { stroke: rgba(232, 121, 249, 0.35); }
.plate--dim { stroke: rgba(126, 139, 161, 0.3); }
.lbl {
fill: #e8f2fb;
font-family: var(--font-mono);
font-size: 11px;
}
.lbl--hub { font-size: 12px; font-weight: 700; }
.lbl--dim { fill: #9fb0c8; }
.cap { font-family: var(--font-mono); }
.cap--hub {
fill: var(--accent);
font-size: 9.5px;
filter: drop-shadow(0 0 4px rgba(63, 186, 245, 0.7));
}
.cap--dim { fill: #5c6a82; font-size: 9px; }
/* radar ring off the hub */
.ring {
transform-box: fill-box;
transform-origin: center;
animation: bzRing 3.6s ease-out infinite;
}
@keyframes bzDash { to { stroke-dashoffset: -260; } }
@keyframes bzDashRev { to { stroke-dashoffset: 260; } }
@keyframes bzPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@keyframes bzRing {
0% { transform: scale(0.5); opacity: 0.9; }
100% { transform: scale(1.7); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.pkt-fwd, .pkt-fwd-slow, .pkt-rev, .pkt-rev-slow, .pulse, .pulse-slow, .ring {
animation: none;
}
.ring { opacity: 0.5; }
}
</style>
+222 -51
View File
@@ -1,70 +1,176 @@
---
import { site } from "../data/site";
import { cvAvailable } from "../lib/assets";
import { fleetFacts } from "../data/fleet";
import Constellation from "./Constellation.astro";
---
<section class="hero">
<!-- hero-scoped atmosphere over the site-wide board art: directional scrims,
a static radial glow, and a 56px grid masked to an ellipse -->
<div class="hero__scrim hero__scrim--x" aria-hidden="true"></div>
<div class="hero__scrim hero__scrim--y" aria-hidden="true"></div>
<div class="hero__glow" aria-hidden="true"></div>
<div class="hero__grid" aria-hidden="true"></div>
<div class="container hero__inner">
<p class="eyebrow hero__eyebrow">{site.role}</p>
<div class="hero__copy">
<p class="eyebrow hero__eyebrow">{site.role}</p>
<h1 class="hero__title">
{site.name}
</h1>
<h1 class="hero__title">
{site.name}
</h1>
<p class="hero__positioning">{site.positioning}</p>
<p class="hero__positioning">{site.positioning}</p>
<p class="hero__tags mono" aria-label="Core technologies">
{site.tagline.map((t, i) => (
<Fragment>
{i > 0 && <span class="hero__sep" aria-hidden="true">▸</span>}
<span>{t}</span>
</Fragment>
))}
</p>
<p class="hero__tags mono" aria-label="Core technologies">
{site.tagline.map((t, i) => (
<Fragment>
{i > 0 && <span class="hero__sep" aria-hidden="true">▸</span>}
<span>{t}</span>
</Fragment>
))}
</p>
<div class="hero__cta">
<a class="btn btn--primary" href="/#projects">View Projects</a>
{cvAvailable && (
<a class="btn" href="/cv.pdf" download>
Download CV
</a>
)}
<a class="btn" href="/#contact">Contact</a>
<div class="hero__cta">
<a class="btn btn--primary" href="/#projects">View Projects</a>
{cvAvailable && (
<a class="btn" href="/cv.pdf" download>
Download CV
</a>
)}
<a class="btn" href="/#contact">Contact</a>
</div>
<!-- the typing fleet-fact bar: real numbers, cycled by site.js (external, CSP-safe);
with JS off / reduced motion it shows the first fact statically -->
<div
class="hero__fact mono"
id="fleet-fact"
data-facts={JSON.stringify(fleetFacts)}
>
<span class="hero__fact-prompt">~/{site.handle} ▸</span>
<span class="hero__fact-line"
><span id="fleet-fact-text">{fleetFacts[0]}</span><span
class="hero__caret"
aria-hidden="true">▮</span
></span
>
</div>
</div>
<div class="hero__viz" aria-hidden="false">
<Constellation />
</div>
</div>
<div class="hero__scroll mono" aria-hidden="true">
<span class="hero__diamond"></span>
<span>SCROLL — DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span>
</div>
</section>
<style>
.hero {
position: relative;
min-height: min(88vh, 760px);
min-height: min(92vh, 820px);
display: flex;
align-items: center;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border-accent);
overflow: hidden;
}
/* atmosphere layers (scoped to the hero, over the global board art) */
.hero__scrim,
.hero__glow,
.hero__grid {
position: absolute;
inset: 0;
pointer-events: none;
}
.hero__scrim--x {
background: linear-gradient(
90deg,
rgba(9, 12, 20, 0.96) 0%,
rgba(9, 12, 20, 0.88) 42%,
rgba(9, 12, 20, 0.62) 70%,
rgba(9, 12, 20, 0.55) 100%
);
}
.hero__scrim--y {
background: linear-gradient(
180deg,
rgba(9, 12, 20, 0.55) 0%,
transparent 30%,
transparent 70%,
rgba(9, 12, 20, 0.9) 100%
);
}
[data-theme="light"] .hero__scrim--x {
background: linear-gradient(
90deg,
rgba(246, 248, 251, 0.96) 0%,
rgba(246, 248, 251, 0.86) 42%,
rgba(246, 248, 251, 0.6) 70%,
rgba(246, 248, 251, 0.5) 100%
);
}
[data-theme="light"] .hero__scrim--y {
background: linear-gradient(
180deg,
rgba(246, 248, 251, 0.55) 0%,
transparent 30%,
transparent 70%,
rgba(246, 248, 251, 0.92) 100%
);
}
.hero__glow {
background:
radial-gradient(900px 600px at 78% 40%, rgba(232, 121, 249, 0.1), transparent 65%),
radial-gradient(1000px 700px at 15% 25%, rgba(63, 186, 245, 0.12), transparent 60%);
}
.hero__grid {
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: radial-gradient(ellipse 80% 70% at 60% 40%, black 30%, transparent 75%);
}
.hero__inner {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 640px) minmax(0, 1fr);
align-items: center;
gap: var(--space-6);
padding-block: var(--space-9);
width: 100%;
}
.hero__copy {
display: flex;
flex-direction: column;
}
.hero__viz {
justify-self: end;
width: min(48vw, 720px);
margin-right: -4%;
}
.hero__eyebrow {
margin-bottom: var(--space-4);
letter-spacing: 0.18em;
text-shadow: 0 0 14px rgba(63, 186, 245, 0.5);
}
.hero__title {
font-family: var(--font-display);
font-size: var(--step-5);
line-height: 1.12;
/* Chakra Petch is geometric/wide — looser tracking than Inter's -0.03em. */
line-height: 1.05;
letter-spacing: -0.005em;
/* Extend the paint box below the baseline so background-clip:text doesn't
crop glyph descenders (the "g" in Wright). */
padding-bottom: 0.14em;
/* Vivid electric nameplate — a full cyan→violet→magenta fill (theme-aware via
the accent tokens: deep on light, bright on dark) so the name is unmissable
on EITHER backdrop. Two layers: a light sheen band on top that glides across
once on load (heroSheen) then rests off-screen, over the solid gradient.
A tight dark edge-shadow keeps the colour crisp on the light board; the
wide accent glow gives the electric halo. */
/* two-line nameplate (mock parity) — wrap at the space, keep the column clear
of the constellation's left-most node labels */
max-width: min-content;
background:
linear-gradient(
105deg,
@@ -79,19 +185,18 @@ import { cvAvailable } from "../lib/assets";
-webkit-background-clip: text;
background-clip: text;
color: transparent;
/* Override the light-mode body text-halo (which washed the clipped fill). */
text-shadow: none;
filter: drop-shadow(0 1px 1px rgba(2, 6, 23, 0.22))
drop-shadow(0 6px 22px var(--accent-glow));
}
.hero__positioning {
margin-top: var(--space-5);
max-width: 42rem;
font-size: var(--step-2);
line-height: 1.35;
color: var(--text);
font-weight: 500;
letter-spacing: -0.01em;
max-width: 32rem;
font-size: var(--step-1);
line-height: 1.55;
color: var(--text-dim);
font-weight: 400;
text-wrap: pretty;
}
.hero__tags {
margin: var(--space-5) 0 0;
@@ -101,7 +206,7 @@ import { cvAvailable } from "../lib/assets";
align-items: center;
gap: 0.4rem 0.6rem;
font-size: var(--step--1);
color: var(--text-dim);
color: var(--text-label);
line-height: 1.7;
}
.hero__sep {
@@ -114,23 +219,89 @@ import { cvAvailable } from "../lib/assets";
gap: var(--space-3);
}
/* The staggered fade-up entrance is driven by the Motion layer
(src/scripts/anim.ts). What stays here is the one-shot electric sheen that
sweeps across the nameplate once on load — a pure background-position paint
effect (no transform/opacity), motion-aware. */
/* the typing bar — glass panel */
.hero__fact {
margin-top: var(--space-6);
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: var(--radius-btn);
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
max-width: 600px;
min-height: 22px;
font-size: 13px;
}
.hero__fact-prompt {
color: var(--accent);
flex-shrink: 0;
}
.hero__fact-line {
color: var(--text-body-strong);
/* real facts don't get clipped — wrap like a terminal would */
overflow-wrap: anywhere;
line-height: 1.5;
}
.hero__caret {
color: var(--accent);
animation: bzCaret 1.1s step-end infinite;
}
.hero__scroll {
position: absolute;
left: max(calc((100vw - var(--measure)) / 2), var(--space-5));
bottom: 28px;
display: flex;
align-items: center;
gap: 10px;
font-size: 11px;
color: var(--text-faint);
z-index: 1;
}
.hero__diamond {
width: 8px;
height: 8px;
border: 1px solid var(--accent);
transform: rotate(45deg);
display: block;
}
@keyframes bzCaret {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: no-preference) {
.hero__title {
animation: heroSheen 1.6s var(--ease) 0.7s both;
}
}
/* The light sheen band (1st layer) sweeps left across the glyphs once, then
parks off-screen so only the vivid gradient (2nd layer, static) remains. */
@media (prefers-reduced-motion: reduce) {
.hero__caret { animation: none; }
}
@keyframes heroSheen {
from {
background-position: 135% 0, 0 0;
from { background-position: 135% 0, 0 0; }
to { background-position: -35% 0, 0 0; }
}
/* responsive: constellation folds under, then hides on small screens */
@media (max-width: 1100px) {
.hero__inner {
grid-template-columns: 1fr;
}
to {
background-position: -35% 0, 0 0;
.hero__viz {
justify-self: center;
width: min(88vw, 560px);
margin-right: 0;
order: 2;
}
}
@media (max-width: 560px) {
.hero__viz { display: none; }
.hero__fact { font-size: 12px; }
.hero__scroll { display: none; }
}
</style>
+20
View File
@@ -0,0 +1,20 @@
// The fleet, as data — drives the hero constellation + typing facts.
// REAL numbers, captured 2026-07-03; refresh from the fleet at rebuild
// (kubectl get applications -n argocd | wc -l · kubectl get cnp -A | wc -l per cluster).
// Facts the hero typing bar cycles through. Real, verbatim — no invented numbers.
export const fleetFacts = [
"6 single-node Talos clusters · 0 SSH daemons · every machine is a document",
"139 ArgoCD Applications reconciled from one hub",
"170+ CiliumNetworkPolicies fleet-wide — every namespace starts at default-deny",
"zero open inbound ports — the origin dials out",
"*.bztmon.org — a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
"~140 pods running right now, reconciled by git",
] as const;
// Node roster for the constellation (positions live in the SVG component).
export const fleetNodes = {
hub: { name: "cave", caption: "139 apps · reconciling" },
satellites: ["alfred", "robin", "kate", "wgirl", "elfastc"],
standbyCaption: "standby · 33 apps known", // kate
} as const;
+30
View File
@@ -18,6 +18,36 @@
font-display: swap;
src: url("/fonts/chakra-petch-700.woff2") format("woff2");
}
/* Facelift: Chakra Petch becomes the site-wide face (300700), body included. */
@font-face {
font-family: "Chakra Petch";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("/fonts/chakra-petch-300.woff2") format("woff2");
}
@font-face {
font-family: "Chakra Petch";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/chakra-petch-400.woff2") format("woff2");
}
@font-face {
font-family: "Chakra Petch";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/chakra-petch-500.woff2") format("woff2");
}
/* JetBrains Mono — one variable file covers 400700 (nav, chips, eyebrows, receipt). */
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/jetbrains-mono-var.woff2") format("woff2");
}
/* ---- Base ---------------------------------------------------------------- */
+43 -9
View File
@@ -29,13 +29,13 @@
/* Grid / topology motif */
--grid-line: rgba(148, 163, 184, 0.06);
/* Fonts — system stacks (zero external requests) + one self-hosted display face. */
--font-sans: "Inter var", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
Roboto, Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, "JetBrains Mono", "Cascadia Code", "SF Mono",
/* Fonts — ALL self-hosted (zero external requests). Facelift pairing:
Chakra Petch (300700) is the site-wide face, display AND body;
JetBrains Mono carries the terminal register (nav, chips, eyebrows, receipt). */
--font-sans: "Chakra Petch", ui-sans-serif, system-ui, -apple-system,
"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", "SF Mono",
Menlo, Consolas, "Liberation Mono", monospace;
/* Display face — Chakra Petch (self-hosted woff2, OFL). Used ONLY on the hero
name + section titles; body stays in --font-sans for readability. */
--font-display: "Chakra Petch", var(--font-sans);
/* Fluid type scale */
@@ -58,13 +58,33 @@
--space-8: 4.5rem;
--space-9: 7rem;
/* Layout */
/* Layout — facelift geometry: tighter radii (34 chips, 6 buttons, 812 cards) */
--measure: 68rem;
--radius: 14px;
--radius-sm: 9px;
--radius: 12px;
--radius-sm: 8px;
--radius-btn: 6px;
--radius-chip: 4px;
--ring: 0 0 0 1px var(--border);
--shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
--ease: cubic-bezier(0.22, 1, 0.36, 1);
/* Facelift: text tiers (mock-exact) */
--text-strong: #eef5fd; /* headings */
--text-body-strong: #dbe6f4;
/* --text (#e7edf4) / --text-dim / --text-faint keep existing consumers working */
--text-secondary: #8fa2bd;
--text-label: #7e8ba1;
/* Facelift: glass panels + cyan-tinted borders + glows */
--panel: rgba(13, 19, 34, 0.72);
--panel-solid: #0d1322;
--panel-blur: blur(10px);
--border-accent: rgba(63, 186, 245, 0.14);
--border-accent-hover: rgba(63, 186, 245, 0.5);
--border-magenta: rgba(232, 121, 249, 0.18);
--glow-num: 0 0 12px rgba(63, 186, 245, 0.6);
--glow-card: 0 0 32px rgba(63, 186, 245, 0.14);
--glow-card-magenta: 0 0 32px rgba(232, 121, 249, 0.12);
}
[data-theme="light"] {
@@ -96,4 +116,18 @@
--grid-line: rgba(15, 23, 42, 0.05);
--shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
/* Facelift tiers, blueprint-adjusted for light */
--text-strong: #0b1422;
--text-body-strong: #1d2a3d;
--text-secondary: #3d4d63;
--text-label: #4a5a72;
--panel: rgba(255, 255, 255, 0.78);
--panel-solid: #ffffff;
--border-accent: rgba(12, 143, 206, 0.2);
--border-accent-hover: rgba(12, 143, 206, 0.55);
--border-magenta: rgba(192, 38, 211, 0.22);
--glow-num: 0 0 12px rgba(12, 143, 206, 0.35);
--glow-card: 0 0 32px rgba(12, 143, 206, 0.1);
--glow-card-magenta: 0 0 32px rgba(192, 38, 211, 0.08);
}