Content update and Professional details

This commit is contained in:
Greg Kilmer
2026-07-04 18:58:16 -04:00
parent 35d561525f
commit cf9eeb865e
6 changed files with 396 additions and 161 deletions
+57 -45
View File
@@ -2,16 +2,14 @@
/**
* InterestHero.astro
* ---------------------------------------------------------------------------
* Shared header for every interest sub-page, implementing the spec's
* per-interest template (Header: title + tagline; Overview: narrative). Uses the
* warm "interest" surface tone via InterestLayout.
* Shared header for every interest sub-page. Compact hero on the animated
* Milky Way field: a back-to-hub breadcrumb, a small icon mark, the title, and
* the italic tagline.
*
* The decorative icon and the tagline (in mono, as a pull quote) give each page
* a consistent rhythm while the page supplies its own title + overview copy.
*
* An optional `image` prop renders a wide celestial-framed banner behind / above
* the heading so each interest page leads with a vibe-matched photo. A faint
* starfield + alchemical accent reinforces the esoteric mood.
* Tuned for the dark starfield: text uses bright cream tones (not the dim
* --color-text-subtle/--color-text which vanish on the deep sky). The title is
* h2-sized (the page is a sub-page, not Home) so it doesn't dominate. An
* optional banner image sits above the heading.
*/
import Icon from '@components/Icon.astro';
import Reveal from '@components/motion/Reveal.astro';
@@ -46,20 +44,57 @@ const {
Carries the per-page morph name (one .vt-sky per page snapshot). -->
<AnimatedBackground client:idle className="vt-sky" />
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-8">
<Reveal>
<a
href="/interests"
class="inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.18em] transition-colors"
style="color: var(--color-text-subtle);"
>
<Icon name="arrow-left" class="h-4 w-4" />
{eyebrow}
</a>
</Reveal>
{/* Two-column layout: text on the left, image on the right. Putting the image
BESIDE the text (instead of stacked above it) means the hero height is
driven by the text column and stays consistent across interest pages
regardless of image presence. On small screens it stacks (image below). */}
<div class="container-prose relative z-10 grid items-center gap-6 py-6 sm:grid-cols-[1.4fr_1fr]">
{/* --- Left: text column --- */}
<div class="flex flex-col items-start gap-4">
<Reveal>
<a
href="/interests"
class="inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.18em] transition-colors"
style="color: var(--color-cream); opacity: 0.8;"
>
<Icon name="arrow-left" class="h-4 w-4" />
{eyebrow}
</a>
</Reveal>
<Reveal delay={60}>
<span
class="inline-flex h-12 w-12 items-center justify-center rounded-xl"
style="background: color-mix(in srgb, var(--color-accent) 18%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={icon} class="h-6 w-6" />
</span>
</Reveal>
<Reveal delay={100}>
<h2
class="font-sans font-bold tracking-tight"
style="color: var(--color-cream); font-size: clamp(1.75rem, 2vw + 1rem, 2.5rem); line-height: 1.15;"
>
{title}
</h2>
</Reveal>
<Reveal delay={140}>
<p
class="max-w-xl border-l-2 pl-4 font-sans text-base italic sm:text-lg"
style="border-color: var(--color-accent); color: var(--color-cream); opacity: 0.92;"
>
{tagline}
</p>
</Reveal>
</div>
{/* --- Right: image column (only when an image is provided). A fixed
aspect keeps every interest hero the same height. --- */}
{image && (
<Reveal delay={40} class="w-full">
<Reveal delay={80} class="w-full">
<figure
class="relative w-full overflow-hidden rounded-2xl border"
style="border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);"
@@ -69,7 +104,7 @@ const {
alt={imageAlt}
loading="lazy"
decoding="async"
class="aspect-[21/9] w-full object-cover"
class="aspect-[4/3] w-full object-cover"
/>
<StarBurst
class="pointer-events-none absolute left-3 top-3 h-5 w-5 drop-shadow-[0_0_6px_color-mix(in_srgb,var(--color-accent)_60%,transparent)]"
@@ -78,28 +113,5 @@ const {
</figure>
</Reveal>
)}
<Reveal delay={60}>
<span
class="inline-flex h-16 w-16 items-center justify-center rounded-2xl"
style="background: color-mix(in srgb, var(--color-accent) 18%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={icon} class="h-8 w-8" />
</span>
</Reveal>
<Reveal delay={120}>
<h1 class="text-balance">{title}</h1>
</Reveal>
<Reveal delay={160}>
<p
class="max-w-2xl border-l-2 pl-5 font-sans text-lg italic sm:text-xl"
style="border-color: var(--color-accent); color: var(--color-text);"
>
{tagline}
</p>
</Reveal>
</div>
</section>
@@ -0,0 +1,77 @@
---
/**
* Certifications.astro
* ---------------------------------------------------------------------------
* Card grid for the Certifications section of the Professional page. Each entry
* is a cert with name, issuer, year, and optional status. Seeded with SAP/cloud
* placeholders (Greg to fill); swap the array below.
*
* Card aesthetic matches EducationHistory / ProjectCard.
*/
import Icon from "@components/Icon.astro";
interface CertEntry {
icon: string;
name: string;
issuer: string;
year: string;
status?: "Active" | "Expired" | "In progress";
}
// Placeholder entries — replace with Greg's real certifications.
const entries: CertEntry[] = [
/*{
icon: "microsoft-azure-devops",
name: "SAP BTP Certification",
issuer: "SAP",
year: "[Year]",
status: "Active",
},*/
{
icon: "cloud",
name: "Planet 9 Devlopment",
issuer: "Neptune",
year: "2019",
status: "Active",
},
/*{
icon: "robot",
name: "[AI / ML cert]",
issuer: "[Issuer]",
year: "[Year]",
},*/
];
---
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{
entries.map((c) => (
<article class="card flex flex-col gap-3">
<div class="flex items-start justify-between gap-3">
<span
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style="background: color-mix(in srgb, var(--color-secondary) 18%, transparent); color: var(--color-secondary);"
aria-hidden="true"
>
<Icon name={c.icon} class="h-5 w-5" />
</span>
{c.status && <span class="pill">{c.status}</span>}
</div>
<div class="flex flex-col gap-1">
<h3 class="font-sans text-base" style="color: var(--color-text);">
{c.name}
</h3>
<p class="text-sm font-medium" style="color: var(--color-accent);">
{c.issuer}
</p>
<span
class="font-mono text-xs uppercase tracking-[0.18em]"
style="color: var(--color-text-subtle);"
>
{c.year}
</span>
</div>
</article>
))
}
</div>
@@ -0,0 +1,76 @@
---
/**
* EducationHistory.astro
* ---------------------------------------------------------------------------
* Card grid for the Education section of the Professional page. Each entry is a
* degree/diploma with institution, period, and optional detail. Data is seeded
* with placeholder rows (Greg to fill); swap the array below with real entries.
*
* Mirrors the Timeline/ProjectCard card aesthetic: raised surface, hairline
* border, gold accent for the credential, mono period label.
*/
import Icon from "@components/Icon.astro";
interface EduEntry {
icon: string;
credential: string;
institution: string;
period: string;
detail?: string;
}
// Placeholder entries — replace with Greg's real education history.
const entries: EduEntry[] = [
{
icon: "school",
credential: "B.S., Computer Science",
institution: "Drexel University",
period: "2014 – 2018",
detail:
"Foundational coursework in software engineering, algorithms, and systems. Focuses in AI and human-computer interaction.",
},
];
---
<div class="grid gap-4 sm:grid-cols-2">
{
entries.map((e) => (
<article class="card flex flex-col gap-3">
<div class="flex items-start gap-3">
<span
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={e.icon} class="h-5 w-5" />
</span>
<div class="flex flex-col gap-1">
<h3 class="font-sans text-lg" style="color: var(--color-text);">
{e.credential}
</h3>
<p
class="font-sans font-semibold"
style="color: var(--color-accent);"
>
{e.institution}
</p>
<span
class="font-mono text-xs uppercase tracking-[0.18em]"
style="color: var(--color-text-subtle);"
>
{e.period}
</span>
</div>
</div>
{e.detail && (
<p
class="text-sm leading-relaxed"
style="color: var(--color-text-muted);"
>
{e.detail}
</p>
)}
</article>
))
}
</div>
+126 -68
View File
@@ -2,15 +2,23 @@
/**
* SectionNav.astro
* ---------------------------------------------------------------------------
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
* sections as a stacked icon+label list, highlights the section currently in
* view, and lets the user click any item to smooth-scroll to it. A vertical
* progress line beside the items fills as the user scrolls down the page.
* Fixed LEFT-SIDE vertical rail for the Professional page. Six sections as a
* stacked icon+label list; highlights the current section; click to smooth-
* scroll. A vertical progress line LEFT of the icons fills as you scroll.
*
* Stylistically it mirrors the page's `.card` language (same raised surface,
* border, radius) so it reads as a sibling of the content cards rather than a
* separate chrome element. Icons + labels use bright tokens so they pop on the
* dark surface. Hidden below `lg`.
* Three bugs previously fixed here:
* 1. Contrast: the rail used surface-relative tokens (--color-surface-raised /
* --color-cream) which collapsed to the same tone on light surfaces (the
* interest-surface cream made icons/text invisible). The rail now uses
* FIXED dark colors regardless of the page's surface tone.
* 2. Progress line clipping: the line sat at the icon column's horizontal
* center, running THROUGH the circles. It now sits in a dedicated left
* gutter, clear of the icons.
* 3. Last section (Contact) not activating: the IntersectionObserver band was
* too thin for the short final section to land in. The rootMargin is
* widened + click handlers set active immediately so feedback is instant.
*
* Hidden below `lg`.
*/
import Icon from "@components/Icon.astro";
const sections = [
@@ -28,70 +36,79 @@ const sections = [
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
data-pro-nav
>
{/* Rail container — matches the .card aesthetic: raised surface, hairline
border, 1rem radius. Bright icons + labels inside. */}
{/* Rail container. FIXED dark colors (not surface tokens) so it's always
legible regardless of the page's data-surface tone. Layout: a left gutter
for the progress line, then the icon+label list. */}
<div
class="card relative flex flex-col gap-1 p-3"
style="border-color: var(--color-border); background: var(--color-surface-raised); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.6);"
class="relative flex gap-2 rounded-2xl border p-3 shadow-2xl"
style="border-color: rgba(232, 181, 74, 0.22); background: linear-gradient(180deg, rgba(12, 14, 40, 0.97), rgba(8, 10, 32, 0.97)); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.7);"
>
{/* Vertical progress track behind the dots. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
aria-hidden="true"
>
</div>
{/* The fill bar — height set by JS as the user scrolls. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
data-pro-fill
aria-hidden="true"
>
{/* --- Left gutter: the progress track + fill. Sits LEFT of the icons. --- */}
<div class="relative my-1 w-2 shrink-0">
{/* Track (full height of the gutter). */}
<div
class="pointer-events-none absolute left-1/2 top-0 h-full w-px -translate-x-1/2"
style="background: rgba(255, 255, 255, 0.12);"
aria-hidden="true"
>
</div>
{/* Fill — height set by JS. Anchored to the top of the track. */}
<div
class="pointer-events-none absolute left-1/2 top-0 w-px -translate-x-1/2 origin-top"
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
data-pro-fill
aria-hidden="true"
>
</div>
</div>
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
style="color: var(--color-cream); opacity: 0.85;"
>
<span
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
style="background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-cream);"
data-section-dot={s.id}
{/* --- The icon+label list. --- */}
<div class="flex flex-col gap-1">
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
class="group relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
style="color: #e8e6f5;"
>
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
</span>
<span class="relative z-10 tracking-wide">{s.label}</span>
</a>
))}
<span
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
style="background: rgba(232, 181, 74, 0.14); color: #f4cd72;"
data-section-dot={s.id}
>
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
</span>
<span class="relative z-10 tracking-wide">{s.label}</span>
</a>
))}
</div>
</div>
</nav>
<style>
/* Active link: full opacity + accent text + subtle accent wash. */
/* Inactive items: readable but slightly dimmed. */
.pro-section-nav a[data-section-link] {
opacity: 0.82;
}
.pro-section-nav a[data-section-link]:hover {
opacity: 1;
background: rgba(232, 181, 74, 0.08);
}
/* Active link: bright accent text + accent wash. */
.pro-section-nav a[data-active="true"] {
opacity: 1 !important;
color: var(--color-accent) !important;
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
background: rgba(232, 181, 74, 0.14);
}
/* Active dot: filled accent, bright icon, glow ring. */
/* Active dot: filled accent, dark icon, glow ring. */
.pro-section-nav a[data-active="true"] [data-section-dot] {
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
color: var(--color-ink) !important;
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
}
/* Hover lift on inactive items. */
.pro-section-nav a:hover {
opacity: 1 !important;
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
background: var(--color-accent) !important;
color: #0b1026 !important;
box-shadow: 0 0 12px rgba(232, 181, 74, 0.65);
}
</style>
<script>
// Spy: highlight the link for whichever section is in view + fill the rail.
function setupSectionSpy() {
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
if (!nav) return;
@@ -107,40 +124,81 @@ const sections = [
const setActive = (id: string) => {
for (const l of links) {
const active = l.dataset.sectionLink === id;
l.setAttribute("data-active", active ? "true" : "false");
l.setAttribute("data-active", l.dataset.sectionLink === id ? "true" : "false");
}
};
// Highlight the most-visible section.
// Manual-scroll suppression. When the user clicks a nav link we set active
// immediately AND suppress IO updates until the smooth-scroll settles. This
// is what stops the flicker: without it, the IO callback and the scroll
// handler fight during the animated scroll, toggling active state rapidly.
let suppressIo = false;
let suppressTimer: number | undefined;
// Click → set active immediately + suppress IO until scrolling settles.
for (const l of links) {
l.addEventListener("click", () => {
const id = l.dataset.sectionLink!;
setActive(id);
suppressIo = true;
window.clearTimeout(suppressTimer);
// Re-enable IO once the smooth-scroll has had time to finish.
suppressTimer = window.setTimeout(() => {
suppressIo = false;
}, 900);
});
}
// The IntersectionObserver is the SINGLE source of truth for the active
// section during free scrolling. (Previously the scroll handler also set
// active, which caused the two systems to thrash.)
const io = new IntersectionObserver(
(entries) => {
if (suppressIo) return;
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) setActive(visible.target.id);
},
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
// Band spans roughly the middle 50% of the viewport vertically.
{ rootMargin: "-25% 0px -25% 0px", threshold: [0, 0.1, 0.25, 0.5] },
);
sections.forEach((s) => io.observe(s));
// Progress fill: fraction of the page scrolled.
const trackTop = 24; // px — matches top-6 on the track/fill
const trackBottom = 24; // px — matches bottom-6
// Progress fill ONLY. The scroll handler must never set active state — that
// was the other half of the flicker. Last-section-at-bottom is handled by
// a dedicated, debounced check below instead of on every scroll tick.
const updateFill = () => {
if (!fill) return;
const track = fill.parentElement; // the .w-2 gutter
if (!track) return;
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
// Fill height as a percentage of the track length (which spans the card
// minus top/bottom padding).
const cardEl = fill.parentElement;
if (!cardEl) return;
const trackPx = cardEl.clientHeight - trackTop - trackBottom;
fill.style.height = `${(frac * trackPx).toFixed(1)}px`;
fill.style.height = `${(frac * track.clientHeight).toFixed(1)}px`;
};
// Debounced at-bottom check: pin the last section active when truly at the
// bottom, but only after scrolling has paused (not on every tick).
let bottomTimer: number | undefined;
const checkBottom = () => {
window.clearTimeout(bottomTimer);
bottomTimer = window.setTimeout(() => {
if (suppressIo) return;
const doc = document.documentElement;
const atBottom =
window.innerHeight + window.scrollY >= doc.scrollHeight - 4;
if (atBottom && sections.length) {
setActive(sections[sections.length - 1].id);
}
}, 120);
};
updateFill();
window.addEventListener("scroll", updateFill, { passive: true });
window.addEventListener("scroll", () => {
updateFill();
checkBottom();
}, { passive: true });
window.addEventListener("resize", updateFill);
}
setupSectionSpy();
+2 -17
View File
@@ -32,25 +32,10 @@ const rendered = await Promise.all(
);
---
<ol class="relative flex flex-col gap-10">
{/* The vertical line. */}
<span
class="absolute left-[1.15rem] top-2 bottom-2 w-px"
style="background: var(--color-border);"
aria-hidden="true"
></span>
<ol class="flex flex-col gap-10">
{rendered.map(({ entry, Content }, i) => (
<li class="relative pl-12">
<li class="relative">
<Reveal delay={i * 80}>
{/* Marker dot on the timeline line (kept small; the icon now lives inline
with the company name inside the card so it never clips the card). */}
<span
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
aria-hidden="true"
>
</span>
<div class="card flex flex-col gap-3">
{/* Header row: icon + company name inline, period on the right. */}
+58 -31
View File
@@ -14,14 +14,16 @@ import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import Icon from "@components/Icon.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import SectionNav from "@components/professional/SectionNav.astro";
import Timeline from "@components/professional/Timeline.astro";
import SkillGrid from "@components/professional/SkillGrid.astro";
import ProjectCard from "@components/professional/ProjectCard.astro";
import EducationHistory from "@components/professional/EducationHistory.astro";
import Certifications from "@components/professional/Certifications.astro";
import AlchemicalDivider from "@components/mystic/AlchemicalDivider.astro";
import Astrolabe from "@components/mystic/Astrolabe.astro";
import CircuitField from "@components/mystic/CircuitField.tsx";
import CelestialFrame from "@components/mystic/CelestialFrame.astro";
import { getCollection } from "astro:content";
const experience = (await getCollection("experience")).sort(
@@ -37,24 +39,43 @@ const projects = (await getCollection("projects")).sort(
title="Professional"
description="Software developer and team lead: cloud and mobile development, SAP Business Technology Platform, AI/ML, full-stack work. Resume and portfolio for Greg Kilmer."
>
<!-- Page header -->
<section class="relative overflow-hidden pt-28 sm:pt-32">
{
/* Circuit-board field. Now self-contained (nebula gradient + canvas inside
one root div), mirroring AnimatedBackground's structure so the morph from
a star hero is element-to-element equivalent — no separate gradient
flash. Shares view-transition-name "sky" via vt-sky. */
}
<CircuitField client:idle className="vt-sky opacity-100" />
{/* Slowly rotating astrolabe ornament behind the header. */}
<Astrolabe
class="pointer-events-none absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
style="color: var(--color-accent); opacity:.13;"
/>
<div
class="container-prose relative z-10 flex flex-col gap-10 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
>
<Reveal>
{/* Page header + overlapping headshot.
The headshot is anchored to the HERO SECTION (not container-prose) so its
straddle point is the true hero/next-section boundary. It's positioned on
the right, aligned with the content column's right edge via the same
container-prose centering math (a centered max-width column). translate-y-1/2
puts its center on the boundary: half in the hero, half over the next
section, in front of both (z-50). */}
<section class="relative overflow-visible pt-28 pb-28 sm:pt-32 sm:pb-36">
{/* Background layer — clipped to the hero so the circuit canvas/astrolabe
don't bleed into the content below. */}
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<CircuitField client:idle className="vt-sky opacity-100" />
<Astrolabe
class="absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
style="color: var(--color-accent); opacity:.13;"
/>
</div>
{/* Headshot — anchored to the hero section's bottom edge (the true boundary
with the next section), aligned right with the content column. Wrapped in
a centered max-width box that mirrors container-prose so the headshot's
right edge lines up with the content's right edge. */}
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-50 mx-auto max-w-[72rem] px-5 lg:ml-56 lg:max-w-[calc(72rem+14rem)] sm:px-8">
<div class="pointer-events-auto ml-auto w-44 translate-y-1/2 sm:w-52 md:w-60">
<CelestialFrame rounded="2xl" emphasis="strong">
<img
src="/images/about-portrait.jpg"
alt="Greg Kilmer headshot"
class="h-full w-full object-cover"
style="aspect-ratio: 4/5;"
/>
</CelestialFrame>
</div>
</div>
<div class="container-prose relative z-10 lg:ml-56 lg:max-w-[calc(72rem+14rem)]">
<Reveal class="max-w-2xl">
<SectionHeader
tone="hero"
kicker="Professional"
@@ -80,7 +101,7 @@ const projects = (await getCollection("projects")).sort(
It keeps container-prose's max-width but lets it sit beside the rail. */
}
<div
class="container-prose flex flex-col gap-20 py-16 sm:gap-24 sm:py-20 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
class="container-prose flex flex-col gap-20 pt-28 pb-16 sm:gap-24 sm:pt-40 sm:pb-20 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
>
{/* ===== 1. Summary ===== */}
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
@@ -185,7 +206,7 @@ const projects = (await getCollection("projects")).sort(
</section>
{/* ===== 5. Education ===== */}
<section id="education" class="scroll-mt-28 flex flex-col gap-6">
<section id="education" class="scroll-mt-28 flex flex-col gap-8">
<Reveal>
<SectionHeader
kicker="05"
@@ -194,16 +215,22 @@ const projects = (await getCollection("projects")).sort(
subtitle="My foundation."
/>
</Reveal>
<div class="grid gap-4 sm:grid-cols-2">
<Placeholder
label="Education history"
hint="Add degrees, institutions, and dates."
/>
<Placeholder
label="Certifications"
hint="SAP, cloud, or other relevant certifications."
/>
</div>
<Reveal delay={60}>
<div class="flex flex-col gap-3">
<h3 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
Education history
</h3>
<EducationHistory />
</div>
</Reveal>
<Reveal delay={120}>
<div class="flex flex-col gap-3">
<h3 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
Certifications
</h3>
<Certifications />
</div>
</Reveal>
</section>
{/* ===== 6. Contact ===== */}