Lumen Studio
Own the component library and token pipeline. Cut surface-level duplication across twelve product teams.
Forge Labs
Built the trading dashboard front end. Introduced scroll-driven motion with a strict reduced-motion contract.
Quill & Co
Editorial platform work. Migrated a legacy jQuery codebase to a typed component model.
Contract
Shipped marketing sites and small tools for early-stage founders.
A vertical experience rail. The hairline draws top to bottom, nodes pop as the line reaches them, and each entry's text settles in just after. Left-aligned, no alternating sides, so it reads as a single column of time.
The about page, or a short career section under the hero. Keep to four to six entries, anything longer belongs on a dedicated CV page. Pair date ranges with a hyphen, never an em-dash.
No alternating left/right sides, it breaks the column. No icons on nodes, the square is enough. Do not animate the rail width, only its height.
<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->
<style>
:root{
--black:#050505;--surface:#0C0C0C;--white:#FFFFFF;--grey:#A3A3A3;--grey-dim:#6B6B6B;--accent:#22D3EE;
--line:rgba(255,255,255,.16);--line-faint:rgba(255,255,255,.07);
--font-display:'Space Grotesk',system-ui,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,monospace;
--dur-fast:150ms;--dur-base:300ms;--ease-out:cubic-bezier(.16,1,.3,1);
}
*{margin:0;padding:0;box-sizing:border-box;border-radius:0!important}
body{background:var(--black);color:var(--white);font-family:var(--font-display);min-height:100vh;padding:64px 48px}
.tl{position:relative;list-style:none;max-width:720px;margin:0 auto;visibility:hidden;--rail:1}
.tl::before{content:"";position:absolute;left:3px;top:4px;bottom:4px;width:1px;background:var(--line);transform:scaleY(var(--rail));transform-origin:top center}
.tl-item{position:relative;padding:0 0 48px 40px}
.tl-item:last-child{padding-bottom:0}
.tl-node{position:absolute;left:0;top:6px;width:7px;height:7px;background:var(--white)}
.tl-date{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--grey);display:block;margin-bottom:8px}
.tl-role{font-size:clamp(1.25rem,2.2vw,1.625rem);font-weight:500;letter-spacing:-.01em;line-height:1.2}
.tl-org{font-family:var(--font-mono);font-size:13px;color:var(--grey);margin-top:4px}
.tl-desc{color:var(--grey);font-size:.9375rem;line-height:1.6;margin-top:12px;max-width:52ch}
</style>
<ol class="tl" id="tl">
<li class="tl-item">
<span class="tl-node"></span>
<span class="tl-date">2024 - now</span>
<h3 class="tl-role">Design Systems Lead</h3>
<p class="tl-org">Lumen Studio</p>
<p class="tl-desc">Own the component library and token pipeline. Cut surface-level duplication across twelve product teams.</p>
</li>
<li class="tl-item">
<span class="tl-node"></span>
<span class="tl-date">2021 - 2024</span>
<h3 class="tl-role">Senior Frontend Engineer</h3>
<p class="tl-org">Forge Labs</p>
<p class="tl-desc">Built the trading dashboard front end. Introduced scroll-driven motion with a strict reduced-motion contract.</p>
</li>
<li class="tl-item">
<span class="tl-node"></span>
<span class="tl-date">2019 - 2021</span>
<h3 class="tl-role">Frontend Engineer</h3>
<p class="tl-org">Quill & Co</p>
<p class="tl-desc">Editorial platform work. Migrated a legacy jQuery codebase to a typed component model.</p>
</li>
<li class="tl-item">
<span class="tl-node"></span>
<span class="tl-date">2018 - 2019</span>
<h3 class="tl-role">Independent</h3>
<p class="tl-org">Contract</p>
<p class="tl-desc">Shipped marketing sites and small tools for early-stage founders.</p>
</li>
</ol>
<script>
const tl = document.getElementById("tl");
const items = gsap.utils.toArray(tl.querySelectorAll(".tl-item"));
const nodes = items.map(i => i.querySelector(".tl-node"));
const texts = items.flatMap(i => [i.querySelector(".tl-date"), i.querySelector(".tl-role"), i.querySelector(".tl-org"), i.querySelector(".tl-desc")]);
function intro(){
return gsap.timeline({delay:0.3, defaults:{ease:"power3.out"}})
.set(tl, {autoAlpha:1})
.fromTo(tl, {"--rail":0}, {"--rail":1, duration:0.9, ease:"power2.inOut"}, 0)
.from(nodes, {scale:0, duration:0.4, stagger:0.2}, 0.2)
.from(texts, {autoAlpha:0, y:14, duration:0.5, stagger:0.04}, 0.35);
}
let timeline = null;
const mm = gsap.matchMedia();
mm.add({
full: "(prefers-reduced-motion: no-preference)",
reduce: "(prefers-reduced-motion: reduce)"
}, (ctx) => {
if (ctx.conditions.full) timeline = intro();
else gsap.set(tl, {autoAlpha:1});
});
</script>