Colophon · v1.2

The visual language behind the work.

This portfolio runs on a small, opinionated system: three surface tones, one typeface, a predictable rhythm of cream and lime panels, and a kit of components that repeat across every case study. Below are the foundations, the components, the motion rules, and the way the whole thing behaves in dark mode.

  • 01 Color
  • 02 Typography
  • 03 Spacing, layout & breakpoints
  • 04 Tokens
  • 05 Rhythm
  • 06 Components
  • 07 Forms
  • 08 Device frames
  • 09 Case-study blocks
  • 10 Data & motion components
  • 11 Sticky header behavior
  • 12 Motion rules
  • 13 Dark mode
  • 14 Imagery
  • 15 Font delivery
  • 16 Work index row
  • 17 Case study order
  • 18 Accessibility & stacking
  • 19 Gallery grid

01Color

Surfaces.

Four surface tones do the heavy lifting: cream for default canvas, cream deep for footers and recessed panels, lime as the accent surface that signals a moment of focus, and lime soft for hover states.

#ECE7D9

Cream

Default canvas

#DDD5C1

Cream deep

Footer / panels

#DCED66

Lime

Accent surface

#E5F094

Lime soft

Hover / quieter lime

Ink — text scale.

A three-step text ramp. Primary ink does ~95% of the work; ink-2 supports body copy; ink-3 carries captions, eyebrows and dates. The whole ramp inverts in dark mode without any component-level changes.

Ink

Light #0F140F · Dark #ECE7D9

Primary text. Flips on theme change.

Ink 2

Light #2A2F29 · Dark #CFC9B9

Body and supporting text.

Ink 3

Light #555A52 · Dark #A59F8E

Captions, eyebrows, tertiary copy.

02Type

Inter Tight, one family.

A single typeface for the whole site, with display sizes tightened to -0.02em and a near-flush 0.98 line-height for posters. Stylistic sets ss01 and cv11 are enabled globally for a more humanist single-story a and rounded g.

Display · 60/0.98 · -0.02em

Design that feels human.

Heading · 36/1.02

A section title sets the topic.

Subhead · 22/1.2

A subhead carries the next layer of context.

Body · 17/1.55 · 68ch measure

Body copy sits at roughly 17px with a generous 1.55 line-height, capped at a 68ch measure so paragraphs stay readable without truncating early on wide screens.

Caption · 14/1.5

Captions, dates, and supporting metadata.

Eyebrow · 11.5/0.18em uppercase

Section eyebrow

Weights used

Regular 400 · Medium 500

No bold. Hierarchy comes from size, color, and rhythm — not weight.

Tracking

Display: -0.02em · Eyebrow: +0.18em

Display tightens, eyebrow opens up. Both create rhythm against neutral body tracking.

OpenType features

ss01 · cv11

Enabled on body. Activates the single-story a and rounded g variants.

03Spacing & layout

The grid behind the rhythm.

Layout is built on a 12-column grid inside a 1320px max-width container, with 24px gutters on mobile and 40px on desktop. Section padding lives in a small predictable set, corner radii are tuned per scale, and only a couple of breakpoints do any real work.

Max container width

1320px

px-6 mobile · px-10 desktop

Prose measure

68ch

Body paragraphs cap here

Section padding · standard

80 / 112

py-20 mobile · py-28 desktop

Section padding · hero

96 / 160

py-24 mobile · py-40 desktop

Card radius

16px

rounded-2xl, the default

Pill radius

999px

Full pill on buttons and chips

Device chrome radii

8 · 16 · 40

Desktop · tablet · mobile

Hairline rule

1px

--color-line, 14% ink

03 — Continued

Breakpoints — three layouts, not five.

Tailwind's default scale ships six breakpoints. This site really only uses two: md at 768px (the desktop pivot) and xl at 1280px (where device frames upsize for wide screens). sm and lg exist but are barely used — most components have one mobile layout and one desktop layout.

base< 640

Mobile · single column, px-6 gutters, py-20 sections.

sm640+

Rarely used. Small inline tweaks only.

md768+

Primary desktop pivot. Multi-column grids, px-10 gutters, py-28 sections.

lg1024+

Rarely used. Most md: layouts already work here.

xl1280+

Device frames upsize: mobile 230 → 375, tablet 320 → 480.

2xl1536+

Not used. Container caps at 1320px.

Layout rules that always hold.

Container caps at 1320px

Every section uses mx-auto max-w-[1320px]. Wider viewports keep the same content width with growing margins.

Gutters: 24 → 40px

px-6 on mobile, px-10 from md upward. Gutters do not grow further at lg or xl.

12-column grid above md

Most desktop layouts are md:grid-cols-12 with a 4 / 8 split between a head column and a content column.

Section padding: 80 → 112 → 160

py-20 mobile, py-28 desktop for standard sections; py-40 reserved for full hero moments.

No min-height heroes

Heroes are sized by content + padding, never by 100vh. The first viewport on every page is content, not chrome.

One pivot per component

Components ship one mobile layout and one md+ layout. Adding a second pivot usually means the component is doing two jobs.

Container width across viewports

375 · mobile

327px

768 · md pivot

688px

1024 · lg

944px

1280 · xl

1200px

1440 · typical desktop

1320px

1920 · wide

1320px

Filled segment is the content container; the margin on either side grows on wide screens once the cap is hit.

04 — Tokens

Every named value in one place.

Tokens map directly to CSS custom properties. Light values show on the left; dark values on the right. Components only ever read the token name — that's how a single class change flips the entire theme.

Token
Light
Dark
Role
--color-cream
#ECE7D9
#16140F
Default page surface
--color-cream-deep
#DDD5C1
#1F1D17
Footer, recessed panels
--color-lime
#DCED66
#1F3014
Accent surface (forest in dark)
--color-lime-soft
#E5F094
#8A9B50
Hover / muted lime
--color-ink
#0F140F
#ECE7D9
Primary text
--color-ink-2
#2A2F29
#CFC9B9
Body / supporting
--color-ink-3
#555A52
#A59F8E
Captions, eyebrows
--color-line
rgba(15,20,15,.14)
rgba(236,231,217,.16)
Hairline rules and borders
--color-line-strong
rgba(15,20,15,.32)
rgba(236,231,217,.36)
Stronger dividers, ring on devices
--color-scrim
rgba(15,20,15,.9)
rgba(0,0,0,.9)
Always-dark overlay scrim
--color-input
#F7F1E0
#221F18
Input field surface
--color-action-bg
#0F140F
#BDCD6A
Primary pill background
--color-action-fg
#DCED66
#0F140F
Primary pill text
--color-action2-bg
#0F140F
#ECE7D9
Secondary circular action bg
--color-device-chrome
#0F140F
#ECE7D9
Phone / tablet / browser housing

05Rhythm

Cream, lime, repeat.

Pages alternate cream and lime panels so a long scroll never flattens. Lime is reserved for moments of focus: hero, case-study intros, and final outcome moments.

Cream · Default canvas

Setup, context, supporting content.

Lime · Focus surface

Hero, intro, and outcome moments.

Cream · Continues

Detail sections, process, artifacts.

Cream deep · Footer / panels

One step recessed from the page surface.

06 — Components

Buttons, pills & chips.

The whole site is built from eight chip-shaped affordances. Each one has a fixed background treatment, a documented contrast ratio, and a single job. Hierarchy is enforced by shape and weight, not by adding more variants.

Level
Component
Used when…
Primary
.pill
The single most important action on a page or section.
Secondary
.pill.pill-ghost
A second affordance next to the primary pill.
Tertiary
.action-circle (lg / sm)
Quiet wayfinding arrows: project rows, lightbox arrows, scroll cues.
Toggle
Theme toggle button
On/off controls that aren’t a true action.
Tag
.skill-chip
Read-only chips on case-study intros.
Floating
.orbit-pill
Decorative drifting pills on the home portrait.
Status
Coming-soon chip
A disabled or upcoming state, never clickable.
Inline
Body link
Inline emphasis within prose. Underline on hover only.

Primary — .pill

States

See selected work →Hover stateSending…

Rest, hover (lift 1px), and disabled (60% opacity). No color change on hover, no drop shadow — the lift is the entire feedback.

Background treatment

Light · primary
Dark · primary

Light: ink #0F140F bg + lime #DCED66 text (~13:1, AAA). Dark: muted lime #BDCD6A bg + ink #0F140F text (~11:1, AAA). Padding 0.85rem × 1.5rem. Radius 999px. Font weight 500.

Secondary — .pill.pill-ghost

On cream surface

A bit about meView all work

Transparent fill, 1px line at --color-line-strong (32% ink), text inherits page ink. Always paired with a primary .pill nearby.

On lime surface

Browse workDownload résumé ↗

The same component on the lime accent surface. Line uses lime-tinted line-strong in dark mode so the ghost reads identically against either accent.

Tertiary — .action-circle

Size · 48px (lg)

Default size for the Work index row arrows. Translates +4px on row hover so the arrow physically leans into the click target.

Size · 44px (md)

Used inside cards and inline next to primary CTAs. Same colors, same rules — just a smaller hit target where the page doesn't need a large arrow.

Background — always fixed

Cream chip + ink icon in both themes (--color-action2-*). The only chip that does NOT flip with theme — it has to be findable against any page surface.

Toggle — Theme button

A custom button shape

Light mode → tap to flip

36×36 circle with a 1px border at --color-line-strong, no fill. Hover adds a 6% ink wash. Icon swaps between a moon (light mode) and a sun (dark mode). Lives in the header rail only.

Dark mode equivalent

Dark mode → tap to flip

Same shape and border, but the icon is now a sun. The button itself does NOT flip background — it stays a hairline-bordered circle, because the action it represents (changing theme) is symmetric.

Tag — .skill-chip

On lime · light mode

  • Design systems
  • Editorial UX
  • Motion

9% ink fill (a soft tint of the surface text color) + ink text. The chip belongs to the lime panel underneath; it's never used on cream.

On forest · dark mode

Design systemsEditorial UXMotion

Re-tinted in CSS: muted-lime fill (18% transparent #BDCD6A) + cream text. Same readable hierarchy on a dark lime accent.

Floating — .orbit-pill

Frosted-glass treatment

Design systemsEditorial UXMotionPrototyping

The only chip with a frosted background. 50% cream fill + backdrop-filter: blur(14px) saturate(140%), an inset 1px white highlight, a 1px ink-8% border, and a 28px soft drop shadow. Renders the whatever's-behind effect over the portrait image.

Size + motion variation

Sm pillMd pillLg pillXl pill

Same component, four font-size steps. Padding scales with the font-size (em-based) so proportions hold.

Font size, padding and float path are randomized per pill so the cloud reads as alive rather than synchronized. Sizes go through the inline font-size (not transform-scale), so text stays sharp at every size.

Status — Coming-soon chip

Disabled / upcoming

Soon

Used on the Work index where a row is intentionally not yet linked. Hairline border at --color-line-strong, no fill, ink-3 text, uppercase 0.18em tracking. Smaller than any other chip on the site so it never competes with active actions.

Specs at a glance

  • Radius — 999px (full pill)
  • Border — 1px --color-line-strong
  • Fill — none
  • Color — --color-ink-3
  • Cursor — default (not clickable)

Padding 4×12 · text 10.5px · tracking 0.18em · uppercase · weight 500. Border 1px line-strong. Never clickable. Always paired with a project row that's otherwise complete in layout.

Supporting — Eyebrow, rule, inline link

Eyebrow

Section eyebrow

11.5px, uppercase, 0.18em tracking, --color-ink-3. The site's quiet wayfinder above every section heading.

Rule

1px hairline at --color-line (14% ink light / 16% cream dark). Separates major content beats without competing for attention.

Inline link

Working from Bentonville.

Inherits text color. Underline ONLY on hover (and on rest in footer links). An always-underline default would compete with the eyebrow rhythm.

06 — Continued

Iconography — hand-cut, not imported.

There is no icon package. The site ships zero lucide, heroicons, react-icons or any other icon library. Every icon is a hand-written inline SVG, and most arrows are Unicode glyphs typeset as text. This keeps the bundle small and the visual language unified.

Inline SVG specs

ViewBox

0 0 24 24

Stroke

1.4 – 1.6

Caps & joins

round

Color

currentColor

Moon (theme)

Sun (theme)

Chevron (submenu)

Only three icons exist in code (moon, sun, chevron). Everything else uses a Unicode arrow glyph rendered as text — which means arrows inherit the surrounding font weight and antialiasing automatically.

Unicode arrow glyphs

Forward

Most CTAs

Back

Prev case study

External

Outbound links

Replay

Animation reset

Up

Reveal hint

Down

Scroll cue

All six glyphs are inserted directly as text inside button labels (e.g. <span className="pill">See selected work →</span>). No SVG, no font icon, no library — they ride along with the typeface.

Why no library?

  • Three icons isn't enough to justify the bundle weight or API surface of a package.
  • Inline SVGs inherit color via currentColor, which means they flip automatically with the ink ramp on theme change.
  • Arrows-as-text means a button label and its arrow share font metrics — there's never a vertical-alignment fight between glyph and text.

07Forms

Inputs with weight.

One field shape across the site: rounded rectangle, warm fill, inset highlight, eyebrow label above. Focus moves the border to ink and adds a 2px translucent ring. The label is always above — never inside — so the field is readable before and after entry.

Demonstration only — not wired to the form endpoint.

Default

you@example.com

Focus

you@example.com

Filled

michael@example.com

Success state — after submit

Thanks — I’ll be in touch.

Replies usually go out within a few days. If it’s urgent, my email lives in the résumé.

08Device frames

Four chrome variants.

Every product screen on the site sits in a device frame. The chrome shifts to cream in dark mode so the housing reads as a real device, not a punched-out shape. When multiple images pass through one frame, the screen cross-fades on a stagger so a row of frames never animates in unison.

9:41•••

Mobile · 9:16

Tablet · 3:4

Desktop · 16:10

TV · 16:9

Cross-fade

1400ms fade between images, default 4400ms interval. Each frame on a page can take an offset so rows don't flip in sync.

Chrome detail

Desktop carries three traffic-light dots and a URL pill. Mobile and tablet have only the housing — screens speak for themselves.

Drop shadow

Single deep shadow, offset down 25–30px, no second light-source shadow. Devices feel grounded but not heavy.

09Case-study blocks

The repeatable case-study kit.

Every case study is built from five shapes: an intro Meta dl, a Stat row, paired Story blocks, scene-tone panels for hero imagery, and a PrevNext footer that closes the page out.

Meta — intro grid

Client
IBM · The Weather Company
Role
Principal UX Designer
Timeline
2017 – 2019
Reach
100M+ daily users

Stat — outcome row

88+

page templates shipped

3

device classes unified

0

ad placements broken

Story — title + body

Editorial templates that scaled

The redesign rebuilt a patchwork of templates into one editorial system that handled photo, video and article use cases without forking.

Responsive across surfaces

Desktop and tablet were treated as peer surfaces. Layouts adapted without losing the article's voice or breaking ad placements.

Previously

← Under Armour Unified Profile

Next case study

Amazon Fire TV & Fire Tablet →

10 — Data & motion components

Specialty pieces, used sparingly.

A handful of components show up in only one or two places. They earn their keep by carrying meaning that a paragraph can't.

Live · OrbitPills

4 of 12 slots shown · auto-swaps every 2.2s

Portrait
ResearchCraftWireframesSystems

Live · ImageRotator

1400ms cross-fade · 1.8s interval (sped up for demo)

Frame 1
Frame 2
Frame 3
Frame 4

Live · Lightbox

Click to open · Esc or scrim to close

Live · ThemeToggle

Local-only — flips the demo card, not the page

Theme

Light

All specialty components

OrbitPills

Home hero

Skill terms that bubble in around the portrait, drift on one of four float paths (6.4 – 9.2s, mixed horizontal+vertical arcs), and pop out when swapped. Twelve orbit slots positioned around the face frame; each pill picks a slot at random.

SplitFlap

Home counters

Mechanical split-flap digit display for the years-of-experience and shipped-projects counters. Each character animates independently with a staggered flip on mount.

ImageRotator

Case studies

Cross-fades a stack of images on an interval. Used outside device frames for hero stills and gallery moments.

Lightbox

Gallery

Click-to-zoom photo viewer with keyboard nav, scrim background using --color-scrim (always-dark), and arrow buttons in .action-circle styling.

ThemeToggle

Header

Toggles light/dark. The choice is stored in localStorage; an inline pre-paint script in <head> reads the value before React hydrates so there's no flash of incorrect theme on first load.

Header

Every page

Sticky header with a transparent backdrop until scroll, then a backdrop-blur cream surface with a hairline rule. Mobile collapses into a full-viewport overlay anchored with position:fixed.

Footer

Every page

Cream-deep panel with identity column, sitemap, and outbound links. Inverts to a deeper warm dark in dark mode without changing layout.

11Sticky header

Shy on scroll, present on intent.

The header is sticky but doesn't sit on the page the whole time. It hides as you scroll deeper and reappears the moment you scroll back. The behavior is three rules layered together: a scrolled state for the surface, a hidden state for downward motion, and a body-lock state when the mobile menu is open.

Live · Sticky header

Scroll inside the panel · y=8 flips surface · y>120 hides on down-scroll

Watson
WorkAboutContact

Hero · header transparent over lime

Scroll line 1 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 2 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 3 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 4 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 5 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 6 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 7 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 8 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 9 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 10 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 11 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 12 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 13 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

Scroll line 14 — keep scrolling to pass y = 120 and watch the header hide on the way down, return on the way up.

at restvisible

At rest · y < 8px

Transparent over lime hero

The header inherits the hero's lime tone. No backdrop, no ring. Logo and nav links read as part of the page.

Scrolled · y > 8px

Cream backdrop + hairline rule

A small threshold flips the header into its 'scrolled' surface so it stays legible over arbitrary content. Hairline rule sits below it.

Scrolling down · y > 120px

Hidden (translateY -100%)

Once past 120px, downward scroll movements > 6px hide the header. Open submenus close at the same instant so they don't float orphaned.

Scrolling up

Revealed instantly

Any upward delta > 6px brings the header back. The threshold prevents flicker from tiny touch jitter.

Mobile menu open

Body scroll locked

Opening the menu sets body { overflow: hidden } and listens for Escape. The menu renders through a portal so it isn't trapped inside any transformed ancestor.

Scroll → header state map

y = 0

At top

Transparent

y = 8

Surface flips

Cream + rule

y = 120

Hide threshold

Below this: shy

Δy +6

Scrolling down

Hide header

Δy −6

Scrolling up

Reveal header

Implemented with a single scroll listener using passive: true, tracking scrollY against a lastY ref. No requestAnimationFrame throttle is needed — the listener only flips React state on threshold crossings, which is cheap.

Submenu hover delay

The Work submenu closes on a small delay so the cursor can travel from the trigger to the menu without it snapping shut underneath.

Portal-rendered overlay

Mobile menu renders into document.body via createPortal so page transforms (page-transition) can't trap it.

Escape closes menu

An Esc key listener is attached only while the menu is open, removed when it closes.

12Motion

Quiet, not still.

Motion has three jobs on this site: signal navigation, reveal new content gracefully, and add a small amount of life to the home portrait. Every animation respects prefers-reduced-motion and disables itself when that media query matches.

Live · SplitFlap

delay 80ms · perCharDelay 26ms · flapsPerChar 3–6 · flapInterval 55ms

Each character lands in a randomized order (Fisher–Yates shuffle on the index list), so the string fills in across the line rather than left-to-right.

SplitFlap — character flip

3–6 flaps per char · 55ms interval · 26ms per-char stagger

Each character starts as a non-breaking space (reserving width), then flips through random letters 3–6 times before landing on its final glyph. A Fisher–Yates shuffle of the index list means characters complete in random order across the line, not left-to-right. Text is tokenised into word units so a line never breaks mid-word during the animation.

Live · Page transition

380ms · cubic-bezier(0.2, 0.7, 0.2, 1) · 8px translate

Mounted route

/work/paypal/

Page transition

380ms · cubic-bezier(0.2, 0.7, 0.2, 1) · 8px translate

Every route mount fades in with an 8px upward settle. The wrapper resets to transform:none at end so it doesn't trap fixed elements (mobile menu, lightbox).

Live · Reveal on scroll

IntersectionObserver · threshold 0.15 · -10% bottom rootMargin · once per element

Scroll ↓ to trigger reveals

Section block 1
Section block 2
Section block 3

Reveal on scroll

700ms · 16px translate · once per element

Section blocks fade in slightly as they enter the viewport. IntersectionObserver at 0.15 threshold with -10% bottom rootMargin. Fires once and disconnects.

Live · Orbit pill — enter / exit

Enter 520ms scale 0.2→1.08→1 · Exit 340ms scale 1→1.18→0

Prototyping

Orbit pills — enter

520ms · cubic-bezier(0.2, 1.4, 0.4, 1) · scale 0.2 → 1.08 → 1

Bubble-pop scale curve. Each pill picks one of four float paths to drift on after entering.

Orbit pills — exit

340ms · cubic-bezier(0.5, 0, 0.8, 0.3) · scale 1 → 0

A reverse pop: briefly inflates to 1.18 then collapses to zero. Old pill leaves before the new one bubbles in.

Device frame carousel

1400ms cross-fade · 4400ms interval · staggered by offset

Multiple frames on a single page take different offsets so rows of devices never advance in sync.

Live · Hover lift

200ms · transform: translateY(-1px) — hover the pill

Hover me Hover me The lift is the entire feedback — no color shift, no shadow.

Hover lift

200ms · transform: translateY(-1px)

Primary pills lift one pixel on hover. No color change, no shadow — the lift is the entire feedback.

13 — Dark mode

Not an inversion — a re-tuning.

Dark mode isn't a flipped palette. Several tokens swap roles, the lime accent becomes a deep forest green, device chrome takes the opposite path of the page surface, and a pre-paint script prevents any flash of the wrong theme on first load. Below is exactly what happens.

Live · Dark mode flip

Every token re-tunes at once — surface, accent, chrome, text.

Watson
WorkAboutContact

Accent panel

Lime in light · forest in dark

skill chip

Body

Page surface, ink scale, and rule lines all flip in one frame. The chrome on the device next door inverts the opposite way.

Device chrome inverts

Lime → forest green

Light · #DCED66
Dark · #1F3014

Lime accent surfaces would glow uncomfortably on a dark page. In dark mode .bg-lime swaps to #1F3014, and ink tokens inside .bg-lime are rebound to bright lime variants so text inside the panel keeps the same visual pop the light theme has.

Device chrome inverts

Light page · dark chrome
Dark page · cream chrome

The page surface goes dark, but device chrome goes light (cream). Phones and tablets still read as physical objects against the page — not as black rectangles disappearing into the canvas.

Scene tones blend, don't dim

Light tone above · dark blend below

Each project on the Work index has a per-project pastel scene tone. In dark mode that tone is mixed at 30% into a near-neutral #0D0D0C base, so cool tones stay cool and warm tones pull warmer naturally instead of all becoming the same dimmed brown.

No flash on first load

(function(){
  var s = localStorage.getItem('theme');
  if (!s) {
    s = matchMedia('(prefers-color-scheme: dark)')
          .matches ? 'dark' : 'light';
  }
  if (s === 'dark') {
    document.documentElement.classList.add('dark');
  }
})();

An inline script in <head> reads the stored theme (or the OS preference if unset) and applies the .dark class to <html> before React hydrates. The page paints in the correct theme on first frame.

Skill chips re-tune

LightMode
DarkMode

In light mode skill chips use 9% ink fill on lime. In dark mode the lime surface is forest green, so chips re-tint to a muted-lime fill with cream text for the same readable contrast — handled in CSS, not per-component.

Always-fixed tokens

  • --color-scrim — always dark, for overlays
  • --color-on-scrim — always cream, for UI on the scrim
  • --color-action2-fg — always ink, secondary action icon

A small set of tokens never flip. The scrim behind the lightbox is always near-black; the secondary action-circle stays cream-on-ink in both themes. These exist for UI that has to behave the same regardless of page surface.

Element
Light
Dark
Page surface
Cream #ECE7D9
Near-black #16140F
Primary text
Ink #0F140F
Cream #ECE7D9
Lime panels
Lime #DCED66
Forest #1F3014
Lime panel text
Ink #0F140F
Bright lime #DCED66
Device chrome
Ink #0F140F
Cream #ECE7D9
Primary pill
Ink bg / lime text
Muted lime bg / ink text
Scrim (lightbox)
rgba(15,20,15,0.9)
rgba(0,0,0,0.9)
Scene tones
Per-project pastel
Pastel × 30% + #0D base

14Imagery

Frames before photos.

Photos and screens always sit inside a frame: a circle for the portrait, device chrome for product screens, a tinted scene panel for case-study heroes. The frame is the consistency; the contents can change project to project.

Portrait circle
Device chrome
Scene tone

15Font delivery

How Inter Tight reaches the page.

The whole site renders in one typeface. It's loaded through the platform's font optimization, served self-hosted (no third-party request at runtime), and tuned with two OpenType features so headings read with the intended stylistic alternates.

Source

Inter Tight — Google Fonts

The original face is from Google Fonts. The site fetches it at build time and serves the woff2 files from the same origin, so visitors never make a runtime request to a third-party CDN.

Weights in use

Regular400
Medium500

Only two weights are loaded. Display text (h1–h4) uses 500 with tightened tracking and 0.98 line-height. Body copy uses 400.

Loading strategy

  • font-display: swap — fall back text paints first, swaps when the webfont arrives.
  • Subsets restricted to latin.
  • Exposed as a CSS variable --font-sans, which falls back to ui-sans-serifsystem-ui → sans-serif.

OpenType features

Body sets two features globally:

font-feature-settings:
  'ss01' on,   /* alternate single-story 'a' */
  'cv11' on;   /* alternate 'g' tail */

ss01 / cv11 on

agility

Defaults

agility

The alternates give Inter Tight a slightly more humanist feel — single-story a, looped g — which pairs better with the editorial scale used in headings.

Selection color

::selection is overridden to ink background with lime text. The selection block ends up as a tiny moment of the site's signature contrast.

Try selecting this snippet.

16Work index row

One row per project.

The Work listing is a single repeating row component, separated by hairline rules. Each row holds an index numeral, the client and role, a one-line tagline, the year, an action circle, and a scene-tone panel housing the device frame. Hover lifts the device a single pixel and slides the action arrow one pixel right.

Row anatomy · 12-col grid

01

PayPal

Principal UX Designer

Building a developer-facing platform for global payments.

2019 – 2021
col-1 · index
col-4 · client + role
col-4 · tagline
col-2 · year
col-1 · action

Typography

Index

.eyebrow · 0.78rem · 0.18em tracking · uppercase 500

Client

clamp(2rem, 3.6vw, 3.2rem) · weight 500 · line-height 0.98

Role

0.95rem · text-ink-3

Tagline

1.05–1.1rem · text-ink-2

Year

0.95rem · text-ink-3

Action

.action-circle 48×48 · arrow translates +1px on row hover

Hover behavior

  • Action arrow — translateX(+1px), 200ms.
  • Scene panel device — translateY(−1px), 700ms ease-out. The whole row's group-hover drives the device lift.
  • No color shift on the row itself. The movement is the feedback.

Coming-soon row

Not yet linkable — row renders as a static <div>, action circle is replaced with the "Soon" status chip.

Soon

Scene tones per project

Each project owns a pastel scene tone used as the background of its device panel. In dark mode the tone is blended at 30% into a near-neutral base, so cool tones stay cool and warm tones pull warmer instead of flattening.

PayPal

#b8c6dc

Light · Dark blend

Under Armour

#c7b6b1

Light · Dark blend

RigUp

#d9b890

Light · Dark blend

Ernst & Young

#d8c47a

Light · Dark blend

IBM · The Weather Company

#a9bcc8

Light · Dark blend

IBM · Notifications

#c5cfd9

Light · Dark blend

Amazon

#a8bdb8

Light · Dark blend

Microsoft

#aec0d6

Light · Dark blend

17Case study order

A canonical section order.

Every case study follows the same nine-section spine. The eyebrow label changes per section, the background alternates between cream, cream-deep and lime, and the block types are reused from the case-study kit shown earlier. The PayPal study is the reference example.

01

eyebrow · Case study

Hero

Lime
  • Eyebrow + title
  • Tagline
  • Meta dl (Role / Team / Timeline / Platform / Reach)
02

eyebrow ·

Hero image

Lime
  • Full-bleed device frame or scene photo
03

eyebrow · Overview

Overview

Cream
  • h2 setup line
  • Two-column lede body copy
04

eyebrow · Strategy

Strategy

Lime
  • h2 framing line
  • Numbered strategy beats with eyebrow labels (3–4)
05

eyebrow · Research

Research

Cream
  • h2 finding
  • Supporting body + optional inline image
06

eyebrow · Design

Design

Cream-deep
  • h2 system claim
  • Story pairs (multiple)
  • Optional full-bleed image
07

eyebrow · Outcome

Outcome

Lime
  • h2 result line
  • Stat row (3–4 stats)
08

eyebrow · Reflection

Reflection

Cream
  • h2 closing thought
  • Single Story block or paragraph
09

eyebrow · Previously / Next

PrevNext footer

Cream-deep
  • Two-column "Previously" / "Next case study" pair

Why this spine

Strategy, Outcome and Hero are the three lime panels — the site's accent color is reserved for the parts of a story that earn emphasis. Overview, Research, and Reflection sit quietly on cream and let the writing carry. Design lands on cream-deep so the Story pairs and full-bleed images read as a discrete block.

18Accessibility & polish

Focus, stacking, reduced motion.

The site keeps its interactions visible: focus states are real, the overlay stack is consistent, and every animation respects the operating-system preference for reduced motion. A short field guide to all three.

Focus rings

Form fields

2px ring · color: ink @ 15% · border darkens to ink

Lightbox tiles

2px ring · color: ink (solid) · focus-visible only

Pills (primary + ghost)

Inherits browser-default focus ring · keyboard-reachable

Inline text links

Underline always visible · focus uses browser default

Continue to the Gallery.

Stacking order · top to bottom

  1. z-[100]

    Lightbox + scrim

    Highest layer. Renders through a portal; locks body scroll while open.

  2. z-[60]

    Mobile menu overlay

    Fixed full-viewport lime panel. Portal-rendered so page transforms can't trap it.

  3. z-40

    Sticky header

    Sits above page content. Hides past y > 120 on scroll-down, returns on scroll-up.

  4. auto

    Page content

    Normal document flow. Reveal and page-transition wrappers don't introduce stacking contexts at rest.

prefers-reduced-motion · what changes

When the OS preference is set to reduce, the following animations disable or simplify. The information stays. The motion stops.

  • Page transition

    Off · routes mount instantly

  • Reveal on scroll

    Off · content paints visible

  • SplitFlap text

    Off · final string renders immediately

  • Orbit pills · enter / exit

    Off · pills appear and disappear without scaling

  • Orbit pills · float drift

    Off · pills sit at their slot positions

  • Image rotator cross-fade

    Kept · decorative, not informational

  • Hover lift & arrow nudge

    Kept · single-pixel, no vestibular risk

  • Theme transitions

    Kept · 300–500ms color fade only

19Gallery grid

Masonry, not a tile grid.

The Gallery uses CSS columns instead of a CSS grid so portrait and landscape photos can sit side by side without cropping or letterboxing. Each tile reserves space via aspect-ratio so the layout never reflows as images load.

Column behavior

Mobile · < 640px

1 column

sm · ≥ 640px

2 columns

lg · ≥ 1024px

3 columns

Implemented as column-count with gap-5 (20px) on mobile and gap-6 (24px) on md+. Figures use break-inside-avoid so an image never splits across two columns.

Tile behavior

  • Aspect reserve — each figure sets aspectRatio: w / h inline so the masonry column height is correct on first paint, before the image arrives.
  • Hover — image scales to 1.01 over 300ms; a small "View" pill fades in at the bottom-right corner.
  • Focus — 2px ink ring on the tile, focus-visible only (no ring on mouse click).
  • Activation — whole tile is a button; opens the full-screen Lightbox at the same aspect ratio.

Live · masonry preview

Portrait
Landscape
Square
Tall
Wide
Portrait

Resize the browser to watch the column count flip at the 640px and 1024px breakpoints.

Section structure

The page is divided into themed sections (Snaps, Student work, Just for fun). Each section opens with a 12-column heading row — eyebrow + title on the left, short paragraph on the right — followed by its own masonry block. Sections never share a grid, so different categories can lay out independently.

Colophon

Small system. Repeated decisions. The work stays in the foreground.