facelift: the scroll story - bento capabilities, dossier cards, series badges, GitOps receipt

02 Capabilities becomes a 4-col bento (Edge AI 2x2 flagship w/ magenta corner radial, Platform
wide, magenta-flavoured security tile, Observability row-completer) driven by span/flavor fields
in skills.ts. 03 Selected Work becomes dossier cards: house-style SVG thumbnails (per-project
motif via DossierThumb), DOSSIER/00N corner tags, 'the argument' line from the outcome field,
chip tags, mono meta footer. 04 Writing cards gain the security-series badge (data/series.ts)
and sit three across. Section numbers glow cyan; a circuit divider (gradient hairlines + two
diamonds) closes the hero. Footer opens with the GitOps receipt - git sha + build date baked at
build time via lib/build-info.ts, zero runtime calls.
This commit is contained in:
2026-07-03 19:43:45 +10:00
parent 65ba1ec7dc
commit 8d955aa802
13 changed files with 505 additions and 156 deletions
+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>
+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;
+11 -9
View File
@@ -154,7 +154,7 @@ import Constellation from "./Constellation.astro";
.hero__viz {
justify-self: end;
width: min(48vw, 720px);
margin-right: -4%;
margin-right: -8%;
}
.hero__eyebrow {
@@ -224,16 +224,19 @@ import Constellation from "./Constellation.astro";
margin-top: var(--space-6);
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
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);
max-width: 600px;
min-height: 22px;
font-size: 13px;
/* 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);
@@ -241,9 +244,8 @@ import Constellation from "./Constellation.astro";
}
.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;
white-space: nowrap;
line-height: 1.4;
}
.hero__caret {
color: var(--accent);
+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);
}
[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>
+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;
+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>