facelift: the Bat-Computer redesign - constellation hero, bento, dossiers, receipt

Promotes the blog art identity to the whole site. Fleet constellation hero (six real nodes,
dual-trace packets, typing fleet facts), Chakra Petch + JetBrains Mono self-hosted, bento
capabilities, dossier work cards, security-series badges, featured blog index, GitOps receipt
footer, native view transitions, light blueprint theme. CSP strict throughout; blog content
byte-identical.
This commit is contained in:
2026-07-03 20:08:12 +10:00
25 changed files with 1409 additions and 211 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();
+47
View File
@@ -0,0 +1,47 @@
---
// Circuit divider — gradient hairlines meeting two rotated-square "diamonds"
// (cyan, magenta). Pure CSS, from the design handoff.
---
<div class="divider container" aria-hidden="true">
<div class="divider__line divider__line--in"></div>
<div class="divider__diamond divider__diamond--cyan"></div>
<div class="divider__line divider__line--mid"></div>
<div class="divider__diamond divider__diamond--magenta"></div>
<div class="divider__line divider__line--out"></div>
</div>
<style>
.divider {
display: flex;
align-items: center;
}
.divider__line {
height: 1px;
}
.divider__line--in {
flex: 1;
background: linear-gradient(90deg, transparent, rgba(63, 186, 245, 0.5));
}
.divider__line--mid {
width: 120px;
background: rgba(63, 186, 245, 0.35);
}
.divider__line--out {
flex: 1;
background: linear-gradient(90deg, rgba(232, 121, 249, 0.5), transparent);
}
.divider__diamond {
width: 6px;
height: 6px;
transform: rotate(45deg);
margin: 0 10px;
flex-shrink: 0;
}
.divider__diamond--cyan {
border: 1px solid var(--accent);
}
.divider__diamond--magenta {
border: 1px solid var(--accent-2);
}
</style>
+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>
+83
View File
@@ -0,0 +1,83 @@
---
// House-style mini-diagram for a dossier card thumbnail. Keyed by the project's
// `diagram` frontmatter; each motif is a tiny node/line sketch in the board palette.
// Pure SVG, pulsing cores only (reduced-motion turns them off).
interface Props {
kind?: string;
}
const { kind = "generic" } = Astro.props;
---
<svg viewBox="0 0 400 180" aria-hidden="true" class="thumb">
{kind === "edge-ai" ? (
<>
{/* GPU chip feeding a rack of stores */}
<line x1="70" y1="90" x2="330" y2="50" class="ln ln--c" />
<line x1="70" y1="90" x2="330" y2="90" class="ln ln--c" />
<line x1="70" y1="90" x2="330" y2="130" class="ln ln--m" />
<rect x="46" y="66" width="48" height="48" rx="6" class="nd nd--c" />
<text x="70" y="95" text-anchor="middle" class="tx">GPU</text>
<rect x="314" y="36" width="32" height="28" rx="4" class="nd nd--dim" />
<rect x="314" y="76" width="32" height="28" rx="4" class="nd nd--dim" />
<rect x="314" y="116" width="32" height="28" rx="4" class="nd nd--m" />
<circle cx="200" cy="70" r="2.5" class="dot dot--c pulse" />
<circle cx="200" cy="110" r="2.5" class="dot dot--m pulse-slow" />
</>
) : kind === "homelab" ? (
<>
{/* hub and spokes — the fleet */}
<line x1="200" y1="90" x2="90" y2="45" class="ln ln--c" />
<line x1="200" y1="90" x2="90" y2="135" class="ln ln--c" />
<line x1="200" y1="90" x2="310" y2="45" class="ln ln--m" />
<line x1="200" y1="90" x2="310" y2="135" class="ln ln--m" />
<circle cx="200" cy="90" r="24" class="hub" />
<circle cx="200" cy="90" r="3" class="dot dot--c pulse" />
<circle cx="90" cy="45" r="12" class="nd nd--c" />
<circle cx="90" cy="135" r="12" class="nd nd--c" />
<circle cx="310" cy="45" r="12" class="nd nd--m" />
<circle cx="310" cy="135" r="12" class="nd nd--m" />
</>
) : (
<>
{/* pipeline — commits flowing through gates to a fleet */}
<line x1="40" y1="90" x2="360" y2="90" class="ln ln--c" />
<rect x="88" y="70" width="40" height="40" rx="5" class="nd nd--c" />
<rect x="180" y="70" width="40" height="40" rx="5" class="nd nd--dim" />
<rect x="272" y="70" width="40" height="40" rx="5" class="nd nd--m" />
<circle cx="60" cy="90" r="2.5" class="dot dot--c pulse" />
<circle cx="156" cy="90" r="2.5" class="dot dot--c pulse-slow" />
<circle cx="248" cy="90" r="2.5" class="dot dot--m pulse" />
<circle cx="340" cy="90" r="2.5" class="dot dot--m pulse-slow" />
</>
)}
</svg>
<style>
.thumb {
width: 100%;
height: 100%;
display: block;
}
.ln {
stroke-width: 1;
}
.ln--c { stroke: rgba(63, 186, 245, 0.3); }
.ln--m { stroke: rgba(232, 121, 249, 0.3); }
.nd { fill: var(--panel-solid); stroke-width: 1.5; }
.nd--c { stroke: var(--accent); }
.nd--m { stroke: rgba(232, 121, 249, 0.5); stroke-width: 1; }
.nd--dim { stroke: rgba(126, 139, 161, 0.5); stroke-width: 1; }
.hub { fill: rgba(63, 186, 245, 0.06); stroke: var(--accent); stroke-width: 1.5; }
.tx { fill: var(--accent); font-family: var(--font-mono); font-size: 10px; }
.dot--c { fill: var(--accent); }
.dot--m { fill: var(--accent-2); }
.pulse { animation: thumbPulse 2.4s ease-in-out infinite; }
.pulse-slow { animation: thumbPulse 3.4s ease-in-out infinite; }
@keyframes thumbPulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.pulse, .pulse-slow { animation: none; }
}
</style>
+42
View File
@@ -1,12 +1,23 @@
---
import { site } from "../data/site";
import { socials } from "../data/socials";
import { buildInfo } from "../lib/build-info";
const year = 2026; // build-stamped; bump via the build, not a runtime Date()
const linkedinHref = socials.find((s) => s.label === "LinkedIn")?.href;
const emailHref = socials.find((s) => s.label === "Email")?.href;
---
<footer class="footer">
<!-- the GitOps receipt — every value baked at build time, zero runtime calls -->
<div class="container">
<p class="footer__receipt mono" data-reveal>
<span class="footer__receipt-label">RECEIPT</span>
<span class="footer__rdot">·</span> HEAD <span class="footer__rval">{buildInfo.sha}</span>
<span class="footer__rdot">·</span> built <span class="footer__rval">{buildInfo.builtAt}</span>
<span class="footer__rdot">·</span> reconciled by <span class="footer__rval">ArgoCD</span>
<span class="footer__rdot">·</span> <span class="footer__rok">✓ Synced · Healthy</span>
</p>
</div>
<div class="container footer__inner">
<div>
<p class="mono footer__name">{site.name}</p>
@@ -62,6 +73,37 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
margin-top: var(--space-8);
color: var(--text-dim);
}
.footer__receipt {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
font-size: 11px;
letter-spacing: 0.04em;
color: var(--text-faint);
border: 1px solid var(--border-accent);
border-radius: var(--radius-btn);
background: var(--panel);
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
padding: 10px 14px;
margin-bottom: var(--space-6);
width: fit-content;
max-width: 100%;
}
.footer__receipt-label {
color: var(--accent);
letter-spacing: 0.14em;
}
.footer__rdot {
opacity: 0.5;
}
.footer__rval {
color: var(--text-dim);
}
.footer__rok {
color: var(--accent);
}
.footer__inner {
display: flex;
flex-wrap: wrap;
+224 -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%
);
}
:global([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%
);
}
:global([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: -8%;
}
.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,91 @@ 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: 10px;
padding: 13px 16px;
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);
/* one-liner: the box sizes to its fact (smaller type) and may run a little
past the copy column — there's clear board under it until the viz */
width: max-content;
max-width: min(52rem, calc(100vw - 2 * var(--space-5)));
min-height: 20px;
font-size: 11.5px;
}
.hero__fact-prompt {
color: var(--accent);
flex-shrink: 0;
}
.hero__fact-line {
color: var(--text-body-strong);
white-space: nowrap;
line-height: 1.4;
}
.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>
+17
View File
@@ -2,6 +2,7 @@
import type { CollectionEntry } from "astro:content";
import { formatDate } from "../lib/blog";
import { readingTime } from "../lib/reading";
import { series } from "../data/series";
interface Props {
posts: CollectionEntry<"blog">[];
@@ -21,6 +22,11 @@ const { posts } = Astro.props;
)}
<div class="post__card-body">
<div class="post__meta mono">
{series[post.id] && (
<span class="post__series">
{series[post.id].name} · {String(series[post.id].number).padStart(2, "0")}
</span>
)}
<time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
<span class="post__sep">·</span>
<span>{readingTime(post.body)}</span>
@@ -45,6 +51,7 @@ const { posts } = Astro.props;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
gap: var(--space-4);
}
.post__link {
@@ -84,8 +91,18 @@ const { posts } = Astro.props;
font-size: 0.72rem;
color: var(--text-faint);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.post__series {
color: var(--accent-2);
border: 1px solid var(--border-magenta);
border-radius: 3px;
padding: 2px 7px;
letter-spacing: 0.1em;
font-size: 0.66rem;
}
.post__sep {
opacity: 0.6;
}
+98 -55
View File
@@ -1,89 +1,132 @@
---
// Dossier card — thumbnail diagram, DOSSIER/00N corner tag, title, "the argument",
// tag chips, mono meta footer. Facelift shape from the design handoff.
import type { CollectionEntry } from "astro:content";
import DossierThumb from "./DossierThumb.astro";
interface Props {
entry: CollectionEntry<"projects">;
/** 1-based position → the DOSSIER / 00N corner tag. */
index?: number;
}
const { entry } = Astro.props;
const { entry, index = 1 } = Astro.props;
const p = entry.data;
const href = `/projects/${entry.id}/`;
const dossierNo = String(index).padStart(3, "0");
---
<a class="project card" href={href} data-reveal>
<div class="project__top">
<h3 class="project__title">{p.title}</h3>
<span class="project__arrow" aria-hidden="true">→</span>
<a class="dossier" href={href} data-reveal>
<div class="dossier__thumb">
<DossierThumb kind={p.diagram} />
<span class="dossier__tag mono">DOSSIER / {dossierNo}</span>
</div>
<div class="dossier__body">
<h3 class="dossier__title">
{p.title} <span class="dossier__arrow" aria-hidden="true">→</span>
</h3>
<p class="dossier__argument">The argument: {p.outcome}</p>
<ul class="dossier__chips mono" role="list">
{p.stack.map((s) => <li class="dchip">{s}</li>)}
</ul>
<p class="dossier__meta mono">{p.role} · {p.period}</p>
</div>
<p class="project__outcome">{p.outcome}</p>
<p class="project__summary">{p.summary}</p>
<ul class="project__stack">
{p.stack.map((s) => <li class="tag">{s}</li>)}
</ul>
<p class="project__meta mono">
<span>{p.role}</span><span class="project__sep">·</span><span>{p.period}</span>
</p>
</a>
<style>
.project {
padding: var(--space-5);
.dossier {
display: flex;
flex-direction: column;
gap: var(--space-3);
color: inherit;
height: 100%;
}
a.project:hover {
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: 10px;
overflow: hidden;
text-decoration: none;
color: inherit;
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
transition:
border-color 180ms var(--ease),
box-shadow 180ms var(--ease),
transform 180ms var(--ease);
}
.dossier:hover {
text-decoration: none;
border-color: rgba(63, 186, 245, 0.55);
box-shadow: 0 0 32px rgba(63, 186, 245, 0.15);
transform: translateY(-3px);
}
.project__top {
@media (prefers-reduced-motion: reduce) {
.dossier:hover { transform: none; }
}
.dossier__thumb {
height: 180px;
background: #0b101c;
position: relative;
border-bottom: 1px solid rgba(63, 186, 245, 0.12);
}
:global([data-theme="light"]) .dossier__thumb {
background: var(--surface-2);
border-bottom-color: var(--border-accent);
}
.dossier__tag {
position: absolute;
top: 12px;
left: 14px;
font-size: 10px;
letter-spacing: 0.14em;
color: var(--text-faint);
}
.dossier__body {
padding: 24px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
flex-direction: column;
gap: 12px;
flex: 1;
}
.project__title {
font-size: var(--step-1);
.dossier__title {
margin: 0;
font-family: var(--font-display);
font-size: 20px;
font-weight: 600;
color: var(--text-strong);
}
.project__arrow {
.dossier__arrow {
color: var(--accent);
display: inline-block;
transition: transform 0.2s var(--ease);
}
a.project:hover .project__arrow {
.dossier:hover .dossier__arrow {
transform: translateX(4px);
}
.project__outcome {
color: var(--text);
font-weight: 500;
border-left: 2px solid var(--accent);
padding-left: var(--space-3);
}
.project__summary {
color: var(--text-dim);
font-size: var(--step--1);
.dossier__argument {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
text-wrap: pretty;
}
.project__stack {
.dossier__chips {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: auto 0 0;
padding: 0;
margin: var(--space-2) 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.project__meta {
margin-top: auto;
padding-top: var(--space-3);
font-size: 0.72rem;
.dchip {
font-size: 11px;
color: var(--text-secondary);
background: rgba(63, 186, 245, 0.08);
border-radius: 3px;
padding: 4px 8px;
}
.dossier__meta {
margin: 0;
font-size: 11px;
color: var(--text-faint);
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.project__sep {
opacity: 0.6;
border-top: 1px solid rgba(63, 186, 245, 0.1);
padding-top: 12px;
}
</style>
+3 -1
View File
@@ -40,7 +40,9 @@ const { id, eyebrow, title, index } = Astro.props;
box-shadow: 0 0 8px var(--accent-glow);
}
.section__index {
color: var(--text-faint);
color: var(--accent);
text-shadow: var(--glow-num);
font-size: 1.05em;
margin-right: 0.6rem;
}
.section__title {
+126 -66
View File
@@ -1,94 +1,154 @@
---
// One bento tile in the capabilities grid — size/flavour driven by the data
// (span: hero 2×2 flagship · wide 2×1 · full row · default 1×1).
import type { SkillGroup } from "../data/skills";
interface Props {
group: SkillGroup;
}
const { group } = Astro.props;
const spanClass =
group.span === "hero"
? "tile--hero"
: group.span === "wide"
? "tile--wide"
: group.span === "full"
? "tile--full"
: "";
---
<article class="skill card" data-reveal>
<h3 class="skill__title">{group.title}</h3>
<p class="skill__blurb">{group.blurb}</p>
<ul class="skill__items">
<article
class:list={["tile", spanClass, group.flavor === "magenta" && "tile--magenta"]}
data-reveal
>
{group.span === "hero" && <div class="tile__corner" aria-hidden="true" />}
{group.flag && <p class="tile__flag mono">{group.flag}</p>}
<h3 class="tile__title">{group.title}</h3>
<p class="tile__blurb">{group.blurb}</p>
<ul class="tile__chips mono" role="list">
{group.items.map((item) => (
<li class="chip"><span class="chip__node" aria-hidden="true"></span>{item}</li>
<li class:list={["chip", group.span === "hero" && "chip--lit"]}>{item}</li>
))}
</ul>
</article>
<style>
.skill {
padding: var(--space-5);
.tile {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: 12px;
padding: 28px;
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: 10px;
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
transition:
border-color 180ms var(--ease),
box-shadow 180ms var(--ease);
}
.skill__title {
font-size: var(--step-1);
.tile:hover {
border-color: var(--border-accent-hover);
box-shadow: var(--glow-card);
}
.skill__title::before {
content: "# ";
color: var(--accent);
font-family: var(--font-mono);
.tile--magenta {
border-color: var(--border-magenta);
}
.skill__blurb {
color: var(--text-dim);
font-size: var(--step--1);
line-height: 1.5;
}
.skill__items {
list-style: none;
padding: 0;
margin: var(--space-2) 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.tile--magenta:hover {
border-color: rgba(232, 121, 249, 0.5);
box-shadow: var(--glow-card-magenta);
}
/* Circuit-node chips — a glowing cyan→magenta node + mono label, wired to the
board palette. Gradient border via padding-box/border-box layering so the
radius survives. Lights to a full gradient outline + glow on hover. */
.chip {
--chip-grad: linear-gradient(100deg, var(--accent), var(--accent-2));
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.01em;
.tile--hero {
grid-column: span 2;
grid-row: span 2;
padding: 32px;
gap: 16px;
border-color: rgba(63, 186, 245, 0.2);
}
.tile--wide {
grid-column: span 2;
}
.tile--full {
/* "full" = the row-completer: 3 columns, sharing its row with the 1×1 before it */
grid-column: span 3;
}
.tile__corner {
position: absolute;
inset: 0;
background: radial-gradient(
500px 320px at 85% 100%,
rgba(232, 121, 249, 0.1),
transparent 70%
);
pointer-events: none;
}
.tile__flag {
position: relative;
margin: 0;
font-size: 11px;
letter-spacing: 0.14em;
color: var(--accent-2);
}
.tile__title {
position: relative;
margin: 0;
font-family: var(--font-display);
font-size: 19px;
font-weight: 600;
color: var(--text-strong);
}
.tile--hero .tile__title {
font-size: 26px;
}
.tile__blurb {
position: relative;
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
text-wrap: pretty;
}
.tile--hero .tile__blurb {
font-size: 15px;
color: var(--text-dim);
padding: 0.3rem 0.62rem 0.3rem 0.55rem;
border-radius: 7px;
border: 1px solid var(--accent-line);
background: color-mix(in srgb, var(--surface-2) 88%, var(--accent) 12%);
transition: color 0.2s var(--ease), transform 0.2s var(--ease),
border-color 0.2s var(--ease), box-shadow 0.2s var(--ease),
background 0.2s var(--ease);
max-width: 420px;
}
/* The node: a small cyan→magenta square that glows, like a lit via/pad. */
.chip__node {
flex: none;
width: 6px;
height: 6px;
border-radius: 2px;
background: var(--chip-grad);
box-shadow: 0 0 7px var(--accent-glow);
.tile__chips {
position: relative;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: auto 0 0;
padding: 0;
font-size: 11.5px;
color: var(--text-dim);
}
.chip:hover {
color: var(--text);
transform: translateY(-1px);
border-color: transparent;
/* dark fill (padding-box) inside a live gradient border (border-box) */
background: linear-gradient(var(--surface-2), var(--surface-2)) padding-box,
var(--chip-grad) border-box;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.32), 0 0 16px var(--accent-glow);
.chip {
border: 1px solid rgba(126, 139, 161, 0.25);
border-radius: var(--radius-chip);
padding: 4px 9px;
}
@media (prefers-reduced-motion: reduce) {
.chip {
transition: none;
}
.chip:hover {
transform: none;
.chip--lit {
font-size: 12px;
color: var(--text-body-strong);
border-color: rgba(63, 186, 245, 0.3);
padding: 5px 10px;
}
/* bento collapses gracefully on narrow screens (grid goes 2-col then 1-col
in the parent; spans clamp automatically via grid auto-placement) */
@media (max-width: 720px) {
.tile--hero,
.tile--wide,
.tile--full {
grid-column: 1 / -1;
grid-row: auto;
}
}
</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;
+7
View File
@@ -0,0 +1,7 @@
// Post series — numbered badges on blog cards. Keyed by post slug.
// The security series threads the zero-trust arc across posts as they publish.
export const series: Record<string, { name: string; number: number }> = {
"secret-zero": { name: "SECURITY SERIES", number: 1 },
"workload-least-privilege": { name: "SECURITY SERIES", number: 3 },
// broker-pattern → 02 when it publishes (currently a draft in the workshop)
};
+33 -22
View File
@@ -6,24 +6,33 @@ export type SkillGroup = {
// Short framing line for the group.
blurb: string;
items: string[];
/** Bento tile size — "hero" = 2×2 flagship · "wide" = 2×1 · "full" = whole row · default 1×1. */
span?: "hero" | "wide" | "full";
/** Accent flavour — "magenta" marks security-flavoured tiles. */
flavor?: "magenta";
/** Eyebrow label rendered on the hero tile. */
flag?: string;
};
export const skills: SkillGroup[] = [
{
title: "Cloud & Identity",
blurb: "Hybrid estates across the major clouds, with identity done properly.",
title: "Edge AI & GPU",
span: "hero",
flag: "FLAGSHIP",
blurb: "Serving vision and language models on real hardware at the edge.",
items: [
"Azure",
"Entra ID",
"Intune",
"AWS",
"GCP",
"Microsoft 365",
"Azure Blob / S3",
"NVIDIA GPU Operator",
"GPU passthrough (ESXi, as code)",
"GPU readiness probing & watchdogs",
"Pod lifecycle management",
"YOLO / computer-vision inference",
"Local LLM serving (llama.cpp)",
"Edge Kubernetes",
],
},
{
title: "Platform & Virtualization",
span: "wide",
blurb: "Kubernetes platforms designed to be reasoned about and recovered.",
items: [
"Kubernetes",
@@ -51,6 +60,7 @@ export const skills: SkillGroup[] = [
},
{
title: "Networking & Security",
flavor: "magenta",
blurb: "Default-deny, segmentation, and a small attack surface.",
items: [
"SD-WAN",
@@ -62,8 +72,22 @@ export const skills: SkillGroup[] = [
"RBAC · 2FA / SSO",
],
},
{
title: "Cloud & Identity",
blurb: "Hybrid estates across the major clouds, with identity done properly.",
items: [
"Azure",
"Entra ID",
"Intune",
"AWS",
"GCP",
"Microsoft 365",
"Azure Blob / S3",
],
},
{
title: "Observability & Ops",
span: "full",
blurb: "Knowing the system is healthy — and the GPUs with it.",
items: [
"Prometheus",
@@ -75,17 +99,4 @@ export const skills: SkillGroup[] = [
"ITIL processes",
],
},
{
title: "Edge AI & GPU",
blurb: "Serving vision and language models on real hardware at the edge.",
items: [
"NVIDIA GPU Operator",
"GPU passthrough (ESXi, as code)",
"GPU readiness probing & watchdogs",
"Pod lifecycle management",
"YOLO / computer-vision inference",
"Local LLM serving (llama.cpp)",
"Edge Kubernetes",
],
},
];
+18
View File
@@ -0,0 +1,18 @@
// Build-time GitOps receipt data — captured ONCE at build (SSG), zero runtime calls.
// The footer renders these as the "receipt" line; values are baked into the static HTML.
import { execSync } from "node:child_process";
function git(cmd: string): string {
try {
return execSync(cmd, { encoding: "utf8" }).trim();
} catch {
return "unknown";
}
}
export const buildInfo = {
/** Short commit sha of the source HEAD this build was cut from. */
sha: git("git rev-parse --short HEAD"),
/** ISO date (UTC, minute precision) the build ran. */
builtAt: new Date().toISOString().slice(0, 16).replace("T", " ") + "Z",
} as const;
+164 -3
View File
@@ -2,14 +2,22 @@
import Layout from "../../layouts/Layout.astro";
import Section from "../../components/Section.astro";
import PostList from "../../components/PostList.astro";
import { getPosts, allTags } from "../../lib/blog";
import { getPosts, allTags, formatDate } from "../../lib/blog";
import { readingTime } from "../../lib/reading";
import { series } from "../../data/series";
import { site } from "../../data/site";
const posts = await getPosts();
const tags = allTags(posts);
// Featured = the security-series opener when present, else the newest post.
const featured = posts.find((p) => p.id === "secret-zero") ?? posts[0];
const rest = posts.filter((p) => p.id !== featured.id);
const featuredSeries = series[featured.id];
---
<Layout title="Blog" path="/blog" description="Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.">
<Section id="blog" eyebrow="Writing" index="*" title="Notes from the platform.">
<Section id="blog" eyebrow={`~/${site.handle}/blog`} index="*" title="Notes from the build.">
<p class="lead blog__intro" data-reveal>
Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters.
</p>
@@ -23,7 +31,44 @@ const tags = allTags(posts);
</nav>
)}
<PostList posts={posts} />
{featured && (
<a class="featured" href={`/blog/${featured.id}/`} data-reveal>
<div class="featured__body">
<div class="featured__meta mono">
{featuredSeries && (
<span class="featured__series">
{featuredSeries.name} · {String(featuredSeries.number).padStart(2, "0")}
</span>
)}
<time datetime={featured.data.date.toISOString()}>{formatDate(featured.data.date)}</time>
<span class="featured__sep">·</span>
<span>{readingTime(featured.body)}</span>
</div>
<h3 class="featured__title">{featured.data.title}</h3>
<p class="featured__summary">{featured.data.summary}</p>
<ul class="featured__tags" role="list">
{featured.data.tags.map((t) => (
<li class="tag">{t}</li>
))}
</ul>
<span class="featured__read mono">Read the post →</span>
</div>
{featured.data.hero && (
<div class="featured__art" aria-hidden="true">
<img
src={`${featured.data.hero}?v=2`}
alt=""
width="1600"
height="1073"
loading="eager"
decoding="async"
/>
</div>
)}
</a>
)}
<PostList posts={rest} />
</Section>
</Layout>
@@ -49,4 +94,120 @@ const tags = allTags(posts);
color: var(--text);
text-decoration: none;
}
/* featured card — text left, hero art right, scrimmed into the panel */
.featured {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: 12px;
overflow: hidden;
color: inherit;
text-decoration: none;
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
margin-bottom: var(--space-6);
transition:
border-color 180ms var(--ease),
box-shadow 180ms var(--ease);
}
.featured:hover {
text-decoration: none;
border-color: var(--border-accent-hover);
box-shadow: var(--glow-card);
}
.featured__body {
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-3);
position: relative;
z-index: 1;
}
.featured__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.72rem;
color: var(--text-faint);
}
.featured__series {
color: var(--accent-2);
border: 1px solid var(--border-magenta);
border-radius: 3px;
padding: 2px 7px;
letter-spacing: 0.1em;
font-size: 0.66rem;
}
.featured__sep {
opacity: 0.6;
}
.featured__title {
margin: 0;
font-family: var(--font-display);
font-size: var(--step-2);
font-weight: 600;
color: var(--text-strong);
letter-spacing: -0.01em;
}
.featured__summary {
margin: 0;
color: var(--text-dim);
font-size: var(--step-0);
line-height: 1.65;
text-wrap: pretty;
}
.featured__tags {
list-style: none;
padding: 0;
margin: var(--space-2) 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.featured__read {
margin-top: auto;
padding-top: var(--space-3);
font-size: 0.8rem;
color: var(--accent);
}
.featured__art {
position: relative;
min-height: 280px;
}
.featured__art img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* scrim the art into the panel from the text side */
.featured__art::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(90deg, var(--panel-solid) 0%, transparent 45%);
}
:global([data-theme="light"]) .featured__art::before {
background: linear-gradient(90deg, var(--panel-solid) 0%, transparent 45%);
opacity: 0.85;
}
@media (max-width: 860px) {
.featured {
grid-template-columns: 1fr;
}
.featured__art {
order: -1;
min-height: 200px;
}
.featured__art::before {
background: linear-gradient(180deg, transparent 55%, var(--panel-solid) 100%);
}
}
</style>
+19 -2
View File
@@ -1,6 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import Hero from "../components/Hero.astro";
import CircuitDivider from "../components/CircuitDivider.astro";
import Section from "../components/Section.astro";
import About from "../components/About.astro";
import SkillGroup from "../components/SkillGroup.astro";
@@ -22,6 +23,8 @@ const postTags = allTags(allPosts);
<Layout path="/">
<Hero />
<CircuitDivider />
<Section id="about" eyebrow="About" index="01" title="Reliability you can reason about.">
<About />
</Section>
@@ -34,7 +37,7 @@ const postTags = allTags(allPosts);
<Section id="projects" eyebrow="Selected work" index="03" title="Platforms built to keep working unattended.">
<div class="grid grid--projects">
{featuredProjects.map((entry) => <ProjectCard entry={entry} />)}
{featuredProjects.map((entry, i) => <ProjectCard entry={entry} index={i + 1} />)}
</div>
<p class="projects__more" data-reveal>
<a class="btn" href="/projects/">All projects →</a>
@@ -70,11 +73,25 @@ const postTags = allTags(allPosts);
display: grid;
gap: var(--space-5);
}
/* capabilities bento: 4 columns, hero tile spans 2×2, min row height per the spec */
.grid--skills {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: minmax(190px, auto);
gap: 16px;
}
@media (max-width: 1000px) {
.grid--skills {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 720px) {
.grid--skills {
grid-template-columns: 1fr;
}
}
.grid--projects {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
gap: 20px;
}
.projects__more {
margin-top: var(--space-6);
+1 -1
View File
@@ -17,7 +17,7 @@ const projects = (await getCollection("projects")).sort(
the outcome. More always cooking.
</p>
<div class="grid">
{projects.map((entry) => <ProjectCard entry={entry} />)}
{projects.map((entry, i) => <ProjectCard entry={entry} index={i + 1} />)}
</div>
</Section>
</Layout>
+43
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 ---------------------------------------------------------------- */
@@ -464,3 +494,16 @@ html.js [data-reveal] .chip {
display: none;
}
}
/* ---- Cross-document view transitions (facelift polish) --------------------
Native CSS only: Chromium/Safari get a soft crossfade between pages; Firefox
ignores. Zero JS, zero CSP surface, and reduced-motion turns it off. */
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 180ms;
}
}
+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);
}