initial commit

This commit is contained in:
2026-06-01 01:43:58 -05:00
commit bb5bf9e028
17 changed files with 7158 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
---
---
<nav class="theme-toggle" aria-label="Theme">
<button
type="button"
data-theme-set="dark"
aria-pressed="true"
aria-label="Use dark theme"
>NIGHT</button>
<span class="theme-toggle__sep" aria-hidden="true">·</span>
<button
type="button"
data-theme-set="light"
aria-pressed="false"
aria-label="Use light theme"
>DAY</button>
</nav>
<script>
const root = document.documentElement;
const buttons = document.querySelectorAll<HTMLButtonElement>('[data-theme-set]');
function applyTheme(theme: 'dark' | 'light', persist = true) {
root.dataset.theme = theme;
buttons.forEach((b) => {
const isActive = b.dataset.themeSet === theme;
b.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
if (persist) {
try { localStorage.setItem('theme', theme); } catch {}
}
}
const initial = (root.dataset.theme === 'light' ? 'light' : 'dark') as 'dark' | 'light';
applyTheme(initial, false);
buttons.forEach((b) => {
b.addEventListener('click', () => {
const next = b.dataset.themeSet as 'dark' | 'light';
applyTheme(next, true);
});
});
</script>
+27
View File
@@ -0,0 +1,27 @@
export type Area = {
title: string;
body: string;
};
export const practice: Area[] = [
{
title: 'Networking & Systems Administration',
body: 'Design and maintain business network infrastructure. Administer mail servers, web servers, and databases. Implement security and monitoring.',
},
{
title: 'Desktop Support & Technical Services',
body: 'End-user support across Windows, Mac, and Linux. Hardware and software troubleshooting, system imaging and deployment, IT asset management.',
},
{
title: 'Programming & Automation Engineering',
body: 'Automation through scripting and open-source development. Custom tools for operational workflows, CI/CD pipelines, and system monitoring.',
},
{
title: 'Web Development & Frontend Engineering',
body: 'Web applications with Svelte and Tailwind. Responsive, accessible interfaces for internal tools and client work.',
},
{
title: 'Creative Production & Media Services',
body: 'Video production, live audio-visual systems, and digital content for broadcast and clients.',
},
];
+15
View File
@@ -0,0 +1,15 @@
export const tools: string[] = [
'Linux',
'macOS',
'Windows',
'Python',
'TypeScript',
'Svelte',
'TailwindCSS',
'Docker',
'Git',
'GitHub',
'nginx',
'Azure',
'GoogleWorkspace',
];
+41
View File
@@ -0,0 +1,41 @@
export type Role = {
title: string;
company: string;
years: string;
current?: boolean;
bullets: string[];
};
export const work: Role[] = [
{
title: "CIO / Co-founder",
company: "SimpleLogic Solutions LLC",
years: "present",
current: true,
bullets: [
"Founded and operate a managed IT services company and infrastructure platform, providing systems administration, networking, cloud, and cybersecurity services to businesses across Tennessee.",
"Design, deploy, and maintain Linux servers, virtualization platforms, business networks, device management systems, backups, and cloud infrastructure.",
"Lead product development, software engineering, business operations, and technical strategy for Launchd and internal automation platforms.",
],
},
{
title: "Owner & Operator",
company: "NixLabs Networks",
years: "20212025",
bullets: [
"Designed, deployed, and maintained secure network infrastructure, hosting platforms, and Linux-based systems for clients.",
"Automated infrastructure management and operational workflows using Bash, Python, and open-source technologies.",
"Provided technical support, troubleshooting, and systems administration for production server environments.",
],
},
{
title: "Field Technician",
company: "Metro Nashville Public Schools",
years: "2022-2023",
bullets: [
"Installed, configured, and maintained computer hardware, software, and networking equipment across district facilities.",
"Troubleshot and resolved technical issues for staff and students, minimizing service interruptions and downtime.",
"Assisted with device imaging, software deployment, inventory management, and large-scale technology rollouts.",
],
},
];
+53
View File
@@ -0,0 +1,53 @@
---
import '@fontsource-variable/geist';
import '@fontsource-variable/geist-mono';
import '../styles/global.css';
import ThemeToggle from '../components/ThemeToggle.astro';
interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="generator" content={Astro.generator} />
<meta name="color-scheme" content="dark light" />
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rummage.cc" />
<meta name="twitter:card" content="summary" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href="https://rummage.cc/" />
<script is:inline>
(function () {
try {
const stored = localStorage.getItem('theme');
const prefersLight =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: light)').matches;
const theme = stored === 'light' || stored === 'dark'
? stored
: prefersLight ? 'light' : 'dark';
document.documentElement.dataset.theme = theme;
} catch (_) {
document.documentElement.dataset.theme = 'dark';
}
})();
</script>
</head>
<body>
<ThemeToggle />
<slot />
</body>
</html>
+117
View File
@@ -0,0 +1,117 @@
---
import Layout from "../layouts/Layout.astro";
import { work } from "../data/work";
import { practice } from "../data/practice";
import { tools } from "../data/tools";
const description =
"Systems engineer and founder building open, scalable infrastructure, cloud platforms, and networks that prioritize simplicity over complexity.";
---
<script>
import "../scripts/toolset-drag";
</script>
<Layout title="Owen Rummage" description={description}>
<div class="page">
<header class="hero">
<h1 class="hero__name">Owen Rummage</h1>
<p class="hero__statement">
Systems engineer and founder building open, scalable
infrastructure, cloud platforms, and networks that prioritize
simplicity over complexity.
</p>
<p class="hero__contact">
<a class="link" href="mailto:hello@rummage.cc"
>hello@rummage.cc</a
>
<a
class="link"
href="https://github.com/owenrummage"
rel="me noopener"
target="_blank">github.com/owenrummage</a
>
<a
class="link"
href="https://www.linkedin.com/in/orummage"
rel="me noopener"
target="_blank">linkedin.com/in/orummage</a
>
</p>
</header>
<hr class="rule" />
<section class="work" aria-label="Work history">
{
work.map((role) => (
<article
class:list={["role", role.current && "role--current"]}
>
<div class="role__meta">
{role.current ? (
<span class="role__live">
<span>present</span>
</span>
) : (
<span class="role__years">{role.years}</span>
)}
</div>
<div class="role__body">
<h2 class="role__title">{role.title}</h2>
<p class="role__company">{role.company}</p>
<ul class="role__bullets">
{role.bullets.map((line) => (
<li>{line}</li>
))}
</ul>
</div>
</article>
))
}
</section>
<hr class="rule" />
<section class="practice" aria-label="Areas of practice">
{
practice.map((area, i) => (
<article class="area">
<div class="area__meta">
{i === 0 && (
<span class="area__label">practice</span>
)}
</div>
<div class="area__body">
<h2 class="area__title">{area.title}</h2>
<p class="area__desc">{area.body}</p>
</div>
</article>
))
}
</section>
<section class="toolset" aria-label="Tools">
<div class="toolset__meta">
<span class="toolset__label">tools</span>
</div>
<ul class="toolset__list">
{tools.map((t) => <li>{t}</li>)}
</ul>
</section>
<aside class="motto" aria-label="Closing note">
<div class="motto__meta" aria-hidden="true">motto</div>
<p class="motto__text">
Stay humble enough to learn, and confident enough to build.
</p>
</aside>
<hr class="rule" />
<footer class="foot">
<span class="foot__build">build 0.1 / Nashville</span>
<span class="foot__home">rummage.cc</span>
</footer>
</div>
</Layout>
+321
View File
@@ -0,0 +1,321 @@
// Toolset drag-to-rearrange
// Custom pointer-events implementation with FLIP reorder animation,
// velocity-based tilt on grab, spring-y drop, localStorage persistence.
const STORAGE_KEY = 'rummage:toolset-order';
const DRAG_THRESHOLD = 5; // px before drag begins (so clicks don't trigger it)
const FLIP_DURATION = 280;
const DROP_DURATION = 360;
const TILT_GAIN = 1.8;
const TILT_CLAMP = 14;
const TILT_DECAY = 0.32;
const SCALE_ACTIVE = 1.06;
type ChipMap = Map<HTMLLIElement, DOMRect>;
function tokenOf(el: Element): string {
return (el.textContent || '').trim();
}
function readReducedMotion(): boolean {
return (
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
}
function snapshotPositions(list: HTMLUListElement): ChipMap {
const map: ChipMap = new Map();
for (const child of list.children) {
map.set(child as HTMLLIElement, child.getBoundingClientRect());
}
return map;
}
function flipSiblings(
list: HTMLUListElement,
prev: ChipMap,
exclude: HTMLLIElement,
duration: number,
): void {
for (const c of list.children) {
const child = c as HTMLLIElement;
if (child === exclude) continue;
const before = prev.get(child);
if (!before) continue;
const after = child.getBoundingClientRect();
const dx = before.left - after.left;
const dy = before.top - after.top;
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;
child.classList.add('is-flipping');
child.style.transition = 'none';
child.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
// Force a synchronous reflow so the no-transition transform commits
// BEFORE we install the transition. Reading offsetWidth flushes
// pending layout; this is more reliable than requestAnimationFrame,
// which can stall in non-rendering contexts.
void child.offsetWidth;
child.style.transition = `transform ${duration}ms cubic-bezier(0.22, 1, 0.36, 1)`;
child.style.transform = '';
window.setTimeout(() => {
child.style.transition = '';
child.style.transform = '';
child.classList.remove('is-flipping');
}, duration + 40);
}
}
function restoreOrder(list: HTMLUListElement, items: HTMLLIElement[]): void {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return;
const order = JSON.parse(stored);
if (!Array.isArray(order)) return;
const byToken = new Map<string, HTMLLIElement>(
items.map((i) => [tokenOf(i), i]),
);
const placed = new Set<HTMLLIElement>();
for (const token of order) {
if (typeof token !== 'string') continue;
const el = byToken.get(token);
if (el) {
list.appendChild(el);
placed.add(el);
}
}
for (const i of items) {
if (!placed.has(i)) list.appendChild(i);
}
} catch {
/* ignore */
}
}
function persistOrder(list: HTMLUListElement): void {
try {
const order = Array.from(list.children).map((c) => tokenOf(c));
localStorage.setItem(STORAGE_KEY, JSON.stringify(order));
} catch {
/* ignore */
}
}
function attachChip(chip: HTMLLIElement, list: HTMLUListElement): void {
let pointerId: number | null = null;
let startX = 0;
let startY = 0;
let offsetX = 0;
let offsetY = 0;
let prevX = 0;
let tilt = 0;
let dragging = false;
const reduce = readReducedMotion();
function applyDragTransform(x: number, y: number): void {
// Measure the chip's natural (untransformed) rect to compute the
// delta needed to put it under the pointer.
chip.style.transition = 'none';
chip.style.transform = '';
const natural = chip.getBoundingClientRect();
const desiredLeft = x - offsetX;
const desiredTop = y - offsetY;
const dx = desiredLeft - natural.left;
const dy = desiredTop - natural.top;
const target = Math.max(
-TILT_CLAMP,
Math.min(TILT_CLAMP, (x - prevX) * TILT_GAIN),
);
tilt = tilt + (target - tilt) * TILT_DECAY;
prevX = x;
const scale = reduce ? 1 : SCALE_ACTIVE;
const rot = reduce ? 0 : tilt;
chip.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(${scale}) rotate(${rot}deg)`;
}
function maybeSwap(x: number, y: number): void {
let target: HTMLLIElement | null = null;
let insertBefore = false;
for (const c of list.children) {
const sib = c as HTMLLIElement;
if (sib === chip) continue;
// Skip chips currently mid-FLIP — their visual rect is in
// transit and would cause oscillating re-swaps.
if (sib.classList.contains('is-flipping')) continue;
const r = sib.getBoundingClientRect();
if (y < r.top || y > r.bottom) continue;
if (x >= r.left && x <= r.right) {
target = sib;
insertBefore = x < r.left + r.width / 2;
break;
}
}
if (!target) return;
const all = Array.from(list.children);
const sibIdx = all.indexOf(target);
const myIdx = all.indexOf(chip);
let newIdx = insertBefore ? sibIdx : sibIdx + 1;
if (newIdx > myIdx) newIdx -= 1;
if (newIdx === myIdx) return;
const prev = snapshotPositions(list);
if (insertBefore) {
list.insertBefore(chip, target);
} else {
list.insertBefore(chip, target.nextSibling);
}
flipSiblings(list, prev, chip, reduce ? 60 : FLIP_DURATION);
}
function beginDrag(): void {
dragging = true;
tilt = 0;
chip.classList.add('is-dragging');
list.classList.add('is-dragging-list');
document.body.classList.add('is-toolset-dragging');
}
function dropAnimate(): void {
// FLIP the chip from its current visible position to its natural rest
// position, animating out scale, rotate, color, and shadow together.
const visible = chip.getBoundingClientRect();
const heldTilt = tilt;
chip.style.transition = 'none';
chip.style.transform = '';
const natural = chip.getBoundingClientRect();
const dx = visible.left - natural.left;
const dy = visible.top - natural.top;
const scale = reduce ? 1 : SCALE_ACTIVE;
const rot = reduce ? 0 : heldTilt;
chip.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(${scale}) rotate(${rot}deg)`;
const dur = reduce ? 100 : DROP_DURATION;
const easing = 'cubic-bezier(0.22, 1, 0.36, 1)';
// Synchronous force-reflow commits the no-transition mid-drag
// transform before we install the new transition. More reliable
// than RAF in non-rendering contexts.
void chip.offsetWidth;
chip.style.transition =
`transform ${dur}ms ${easing}, ` +
`background-color ${dur}ms ${easing}, ` +
`color ${dur}ms ${easing}, ` +
`box-shadow ${dur}ms ${easing}`;
chip.style.transform = '';
// Drop the dragging classes at the same instant the transform
// starts animating, so color/shadow fade together with position.
chip.classList.remove('is-dragging');
list.classList.remove('is-dragging-list');
document.body.classList.remove('is-toolset-dragging');
window.setTimeout(() => {
chip.style.transition = '';
chip.style.transform = '';
// Defensive: ensure classes are cleared even if the synchronous
// path above was interrupted by an earlier exception.
chip.classList.remove('is-dragging');
list.classList.remove('is-dragging-list');
document.body.classList.remove('is-toolset-dragging');
}, dur + 40);
}
function teardownEvents(): void {
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
}
function onPointerDown(e: PointerEvent): void {
if (e.pointerType === 'mouse' && e.button !== 0) return;
if (pointerId !== null) return;
pointerId = e.pointerId;
const r = chip.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
offsetX = e.clientX - r.left;
offsetY = e.clientY - r.top;
prevX = e.clientX;
tilt = 0;
dragging = false;
try {
chip.setPointerCapture(e.pointerId);
} catch {
/* some browsers throw if already captured */
}
// Listen at the window level so events still fire even if pointer
// capture is lost (e.g. mid-reorder, mid-frame DOM moves) or the
// cursor leaves the chip's visual bounds.
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerUp);
}
function onPointerMove(e: PointerEvent): void {
if (e.pointerId !== pointerId) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (!dragging) {
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
beginDrag();
}
e.preventDefault();
// CRITICAL ORDER: swap first (which may change the chip's natural
// rect), THEN apply transform against the new natural so the chip
// stays locked to the pointer with no visual jump.
maybeSwap(e.clientX, e.clientY);
applyDragTransform(e.clientX, e.clientY);
}
function onPointerUp(e: PointerEvent): void {
if (e.pointerId !== pointerId) return;
try {
chip.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
teardownEvents();
const wasDragging = dragging;
pointerId = null;
dragging = false;
if (!wasDragging) return;
dropAnimate();
persistOrder(list);
}
chip.addEventListener('pointerdown', onPointerDown);
}
function init(): void {
const list = document.querySelector<HTMLUListElement>('.toolset__list');
if (!list) return;
const items = Array.from(list.children) as HTMLLIElement[];
if (items.length === 0) return;
restoreOrder(list, items);
// After restoreOrder the children are re-arranged but the list reference
// stays. Re-collect references for the attach loop.
Array.from(list.children).forEach((c) =>
attachChip(c as HTMLLIElement, list),
);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
+734
View File
@@ -0,0 +1,734 @@
/* ============================================================
* Owen Rummage — design tokens & global styles
* ============================================================ */
:root {
/* Dark theme (default). Warm-tinted near-black surface kept for human
warmth; magenta-purple accent reads as deliberate task-light against it
(warm room, cool focused light). */
--color-surface: oklch(0.178 0.006 70);
--color-surface-rise: oklch(0.225 0.007 72);
--color-ink: oklch(0.935 0.008 90);
--color-ink-soft: oklch(0.86 0.008 88);
--color-muted: oklch(0.69 0.008 82);
--color-faint: oklch(0.45 0.008 78);
--color-accent: oklch(0.72 0.21 325);
--color-accent-strong: oklch(0.78 0.22 325);
--color-rule: color-mix(in oklch, var(--color-ink) 14%, transparent);
--color-rule-strong: color-mix(in oklch, var(--color-ink) 28%, transparent);
--color-focus-ring: color-mix(
in oklch,
var(--color-accent) 70%,
transparent
);
--color-accent-wash: color-mix(
in oklch,
var(--color-accent) 14%,
transparent
);
/* Type */
--font-sans:
"Geist Variable", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
sans-serif;
--font-mono:
"Geist Mono Variable", ui-monospace, SFMono-Regular, "JetBrains Mono",
"IBM Plex Mono", "Cascadia Code", Menlo, Consolas, monospace;
/* Type scale */
--fs-display: clamp(2.5rem, 1.6rem + 3.6vw, 3.875rem);
--fs-statement: clamp(1.0625rem, 0.96rem + 0.45vw, 1.1875rem);
--fs-role-title: 1.25rem;
--fs-role-company: 1rem;
--fs-body: 1rem;
--fs-meta: 0.8125rem;
--fs-contact: 0.9375rem;
--fs-tiny: 0.75rem;
/* Rhythm */
--leading-display: 1.04;
--leading-statement: 1.45;
--leading-body: 1.6;
--leading-tight: 1.2;
--tracking-display: -0.032em;
--tracking-tight: -0.012em;
--tracking-mono: -0.01em;
--tracking-label: 0.04em;
/* Layout */
--container-max: 760px;
--container-pad-x: clamp(1.5rem, 4vw, 2.5rem);
--container-pad-top: clamp(3.5rem, 8vw, 6rem);
--container-pad-bottom: clamp(5rem, 12vw, 8rem);
--col-meta: 124px;
--gap-meta: 2rem;
/* Motion */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--dur-fast: 160ms;
--dur-base: 240ms;
--dur-slow: 420ms;
/* z-index scale */
--z-base: 1;
--z-toggle: 10;
--z-focus: 20;
color-scheme: dark;
}
[data-theme="light"] {
--color-surface: oklch(0.97 0 0);
--color-surface-rise: oklch(0.93 0 0);
--color-ink: oklch(0.2 0 0);
--color-ink-soft: oklch(0.32 0 0);
--color-muted: oklch(0.45 0 0);
--color-faint: oklch(0.62 0 0);
--color-accent: oklch(0.45 0.23 322);
--color-accent-strong: oklch(0.4 0.24 318);
--color-rule: color-mix(in oklch, var(--color-ink) 14%, transparent);
--color-rule-strong: color-mix(in oklch, var(--color-ink) 26%, transparent);
--color-focus-ring: color-mix(
in oklch,
var(--color-accent) 70%,
transparent
);
--color-accent-wash: color-mix(
in oklch,
var(--color-accent) 12%,
transparent
);
color-scheme: light;
}
/* ============================================================
* Reset (tight, no bloat)
* ============================================================ */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html {
-webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility;
font-feature-settings: "kern", "liga", "calt", "ss01", "ss02";
font-variant-ligatures: common-ligatures contextual;
background: var(--color-surface);
}
body {
background: var(--color-surface);
color: var(--color-ink);
font-family: var(--font-sans);
font-size: var(--fs-body);
line-height: var(--leading-body);
font-weight: 400;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
min-height: 100dvh;
transition:
background var(--dur-base) var(--ease-out-quint),
color var(--dur-base) var(--ease-out-quint);
}
img,
svg,
video,
canvas {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
button {
background: none;
border: none;
padding: 0;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
border-radius: 1px;
}
/* ============================================================
* Page shell
* ============================================================ */
.page {
max-width: var(--container-max);
margin: 0 auto;
padding: var(--container-pad-top) var(--container-pad-x)
var(--container-pad-bottom);
}
/* ============================================================
* Theme toggle (fixed corner)
* ============================================================ */
.theme-toggle {
position: fixed;
top: clamp(1rem, 2.4vw, 1.75rem);
right: clamp(1.25rem, 3vw, 2rem);
z-index: var(--z-toggle);
display: inline-flex;
align-items: center;
gap: 0.5ch;
font-family: var(--font-mono);
font-size: var(--fs-tiny);
letter-spacing: var(--tracking-mono);
color: var(--color-muted);
}
.theme-toggle button {
font: inherit;
color: inherit;
padding: 0.25rem 0.125rem;
transition: color var(--dur-fast) var(--ease-out-quint);
position: relative;
}
.theme-toggle button:hover {
color: var(--color-ink);
}
.theme-toggle button[aria-pressed="true"] {
color: var(--color-accent);
}
.theme-toggle__sep {
color: var(--color-faint);
user-select: none;
}
/* ============================================================
* Hero (identity)
* ============================================================ */
.hero {
display: flex;
flex-direction: column;
gap: clamp(1.25rem, 3vw, 1.75rem);
}
.hero__name {
font-family: var(--font-sans);
font-size: var(--fs-display);
font-weight: 500;
line-height: var(--leading-display);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
text-wrap: balance;
font-feature-settings: "kern", "ss01";
font-variation-settings: "wght" 540;
}
.hero__statement {
font-size: var(--fs-statement);
line-height: var(--leading-statement);
letter-spacing: var(--tracking-tight);
color: var(--color-ink-soft);
max-width: 58ch;
text-wrap: pretty;
}
.hero__contact {
font-family: var(--font-mono);
font-size: var(--fs-contact);
letter-spacing: var(--tracking-mono);
color: var(--color-muted);
margin-top: 0.25rem;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.35rem 1.25rem;
}
/* ============================================================
* Link grammar
* ============================================================ */
.link {
color: var(--color-accent);
text-decoration: none;
position: relative;
padding-bottom: 0.15em;
background-image: linear-gradient(
to right,
var(--color-accent),
var(--color-accent)
);
background-position: 0 100%;
background-repeat: no-repeat;
background-size: 100% 1px;
transition:
color var(--dur-fast) var(--ease-out-quint),
background-size var(--dur-base) var(--ease-out-quint);
}
.link:hover {
color: var(--color-accent-strong);
background-size: 100% 2px;
}
.link:focus-visible {
outline-offset: 4px;
}
.link--ink {
color: var(--color-ink);
background-image: linear-gradient(
to right,
var(--color-rule-strong),
var(--color-rule-strong)
);
}
.link--ink:hover {
color: var(--color-accent);
background-image: linear-gradient(
to right,
var(--color-accent),
var(--color-accent)
);
}
/* ============================================================
* Hairline divider
* ============================================================ */
.rule {
border: 0;
height: 1px;
background: var(--color-rule);
margin-block: clamp(3rem, 6vw, 4.5rem);
width: 100%;
}
/* ============================================================
* Work index (marginalia grid)
* ============================================================ */
.work {
display: flex;
flex-direction: column;
}
.role {
display: grid;
grid-template-columns: var(--col-meta) 1fr;
gap: var(--gap-meta);
padding-block: clamp(1.75rem, 3.5vw, 2.5rem);
border-top: 1px solid transparent;
transition: border-color var(--dur-base) var(--ease-out-quint);
}
.role + .role {
border-top-color: var(--color-rule);
}
.role__meta {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
column-gap: 0.625rem;
row-gap: 0.35rem;
font-family: var(--font-mono);
font-size: var(--fs-meta);
letter-spacing: var(--tracking-mono);
color: var(--color-muted);
padding-top: 0.35rem; /* optical align with role title baseline */
white-space: nowrap;
}
.role--current .role__meta {
column-gap: 0.3rem;
}
.role__years {
font-variant-numeric: tabular-nums;
}
.role__live {
display: inline-flex;
align-items: center;
gap: 0.45rem;
color: var(--color-accent);
font-family: var(--font-mono);
font-size: var(--fs-tiny);
letter-spacing: var(--tracking-label);
text-transform: uppercase;
font-variation-settings: "wght" 500;
}
.role__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-accent);
display: inline-block;
box-shadow: 0 0 0 0 var(--color-accent);
animation: pulse 2s var(--ease-out-quint) infinite;
}
.role__body {
display: flex;
flex-direction: column;
gap: 0.875rem;
max-width: 64ch;
}
.role__title {
font-family: var(--font-sans);
font-size: var(--fs-role-title);
font-weight: 500;
letter-spacing: var(--tracking-tight);
line-height: var(--leading-tight);
color: var(--color-ink);
text-wrap: balance;
font-variation-settings: "wght" 520;
}
.role__company {
font-family: var(--font-sans);
font-size: var(--fs-role-company);
color: var(--color-muted);
letter-spacing: var(--tracking-tight);
line-height: 1.35;
}
.role__bullets {
list-style: none;
padding: 0;
margin: 0.5rem 0 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
color: var(--color-ink-soft);
}
.role__bullets li {
font-size: var(--fs-body);
line-height: var(--leading-body);
text-wrap: pretty;
}
.role--current .role__title {
color: var(--color-ink);
}
/* ============================================================
* Practice (areas of competence — marginalia grid)
* ============================================================ */
.practice {
display: flex;
flex-direction: column;
}
.area {
display: grid;
grid-template-columns: var(--col-meta) 1fr;
gap: var(--gap-meta);
padding-block: clamp(1.5rem, 3vw, 2.25rem);
border-top: 1px solid transparent;
transition: border-color var(--dur-base) var(--ease-out-quint);
}
.area + .area {
border-top-color: var(--color-rule);
}
.area__meta {
font-family: var(--font-mono);
font-size: var(--fs-meta);
letter-spacing: var(--tracking-mono);
color: var(--color-muted);
padding-top: 0.35rem;
}
.area__label {
display: inline-block;
}
.area__body {
display: flex;
flex-direction: column;
gap: 0.625rem;
max-width: 64ch;
}
.area__title {
font-family: var(--font-sans);
font-size: var(--fs-role-title);
font-weight: 500;
letter-spacing: var(--tracking-tight);
line-height: var(--leading-tight);
color: var(--color-ink);
text-wrap: balance;
font-variation-settings: "wght" 520;
}
.area__desc {
font-size: var(--fs-body);
line-height: var(--leading-body);
color: var(--color-ink-soft);
text-wrap: pretty;
}
/* ============================================================
* Tools (single dense row in marginalia grid)
* ============================================================ */
.toolset {
display: grid;
grid-template-columns: var(--col-meta) 1fr;
gap: var(--gap-meta);
padding-block: clamp(1.75rem, 3.5vw, 2.5rem);
border-top: 1px solid var(--color-rule);
}
.toolset__meta {
font-family: var(--font-mono);
font-size: var(--fs-meta);
letter-spacing: var(--tracking-mono);
color: var(--color-muted);
padding-top: 0.35rem;
}
.toolset__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
font-family: var(--font-mono);
font-size: var(--fs-meta);
color: var(--color-ink-soft);
letter-spacing: var(--tracking-mono);
align-items: baseline;
}
.toolset__list li {
position: relative;
display: inline-flex;
align-items: baseline;
padding: 0.25em 0.5em;
margin: -0.25em 0;
border-radius: 3px;
color: var(--color-ink-soft);
cursor: grab;
touch-action: none;
user-select: none;
-webkit-user-select: none;
font-variant-numeric: tabular-nums;
transition:
background-color var(--dur-base) var(--ease-out-quint),
color var(--dur-fast) var(--ease-out-quint),
transform var(--dur-base) var(--ease-out-quint),
box-shadow var(--dur-base) var(--ease-out-quint);
will-change: transform;
}
.toolset__list li:active {
cursor: grabbing;
}
.toolset__list li.is-dragging {
cursor: grabbing;
z-index: 5;
background-color: var(--color-accent-wash);
color: var(--color-accent);
box-shadow:
0 14px 36px color-mix(in oklch, var(--color-accent) 22%, transparent),
0 2px 8px color-mix(in oklch, var(--color-accent) 30%, transparent);
/* JS owns transform / transition during drag */
transition: none !important;
}
.toolset__list li.is-dragging::before {
color: var(--color-accent);
}
/* Suppress hover styles on other chips while a drag is in flight, so
* they don't flash as the dragged chip sweeps over them. */
.toolset__list.is-dragging-list li:not(.is-dragging):hover {
background-color: transparent;
color: var(--color-ink-soft);
transform: none;
}
.toolset__list.is-dragging-list li:not(.is-dragging):hover::before {
color: var(--color-faint);
transform: none;
}
body.is-toolset-dragging,
body.is-toolset-dragging * {
cursor: grabbing !important;
}
.toolset__list li::before {
content: "#";
color: var(--color-faint);
margin-right: 0.06em;
display: inline-block;
transform-origin: 50% 65%;
transition:
color var(--dur-fast) var(--ease-out-quint),
transform var(--dur-base) var(--ease-out-quint);
}
.toolset__list li:hover {
color: var(--color-accent);
background-color: var(--color-accent-wash);
transform: translateY(-1px);
}
.toolset__list li:hover::before {
color: var(--color-accent);
transform: rotate(-14deg) translate(-1px, -1px);
}
/* ============================================================
* Motto (closing reflection — marginalia grid)
* ============================================================ */
.motto {
display: grid;
grid-template-columns: var(--col-meta) 1fr;
gap: var(--gap-meta);
margin-top: clamp(2.75rem, 5.5vw, 4rem);
}
.motto__meta {
font-family: var(--font-mono);
font-size: var(--fs-meta);
color: var(--color-muted);
letter-spacing: var(--tracking-mono);
padding-top: 0.4rem;
user-select: none;
}
.motto__text {
font-family: var(--font-sans);
font-style: italic;
font-size: var(--fs-statement);
color: var(--color-ink-soft);
line-height: var(--leading-statement);
letter-spacing: var(--tracking-tight);
max-width: 56ch;
text-wrap: balance;
font-variation-settings:
"wght" 420,
"slnt" -10;
}
/* ============================================================
* Footer
* ============================================================ */
.foot {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
font-family: var(--font-mono);
font-size: var(--fs-meta);
color: var(--color-muted);
letter-spacing: var(--tracking-mono);
}
.foot__build {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.foot__build::before {
content: "";
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--color-rule-strong);
}
.foot__home {
font-variant-numeric: tabular-nums;
}
.foot__handle {
color: var(--color-muted);
}
/* ============================================================
* Motion: live dot pulse + theme transitions
* ============================================================ */
@keyframes pulse {
0% {
box-shadow: 0 0 0 0
color-mix(in oklch, var(--color-accent) 50%, transparent);
}
70% {
box-shadow: 0 0 0 7px
color-mix(in oklch, var(--color-accent) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0
color-mix(in oklch, var(--color-accent) 0%, transparent);
}
}
/* ============================================================
* Responsive: stack the marginalia grid on narrow screens
* ============================================================ */
@media (max-width: 640px) {
.role,
.area,
.toolset,
.motto {
grid-template-columns: 1fr;
gap: 0.875rem;
}
.motto__meta {
display: none;
}
.role__meta {
flex-direction: row;
align-items: center;
gap: 0.875rem;
padding-top: 0;
flex-wrap: wrap;
}
.role--current .role__meta {
gap: 0.3rem;
}
.area__meta:empty {
display: none;
}
.area__meta,
.toolset__meta {
padding-top: 0;
}
.hero__statement {
max-width: 100%;
}
}
@media (min-width: 1024px) {
:root {
--container-max: 800px;
--col-meta: 132px;
}
}
/* ============================================================
* Reduced motion
* ============================================================ */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
.role__dot {
box-shadow: 0 0 0 3px
color-mix(in oklch, var(--color-accent) 25%, transparent);
}
.toolset__list li:hover,
.toolset__list li:hover::before {
transform: none;
}
}
/* ============================================================
* Print
* ============================================================ */
@media print {
:root {
--color-surface: #ffffff;
--color-ink: #111111;
--color-muted: #444444;
--color-faint: #777777;
--color-accent: #6b2d7a;
--color-rule: #cccccc;
}
.theme-toggle {
display: none;
}
.role__dot {
animation: none;
}
body {
background: #fff;
color: #111;
}
}
/* ============================================================
* Selection
* ============================================================ */
::selection {
background: var(--color-accent-wash);
color: var(--color-ink);
}