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
01 — Color
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.
Cream
Default canvas
Cream deep
Footer / panels
Lime
Accent surface
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.
02 — Type
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.
03 — Spacing & 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< 640Mobile · 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.
--color-cream--color-cream-deep--color-lime--color-lime-soft--color-ink--color-ink-2--color-ink-3--color-line--color-line-strong--color-scrim--color-input--color-action-bg--color-action-fg--color-action2-bg--color-device-chrome05 — Rhythm
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.
.pill.pill.pill-ghost.action-circle (lg / sm)Theme toggle button.skill-chip.orbit-pillComing-soon chipBody linkPrimary — .pill
States
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: 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
Transparent fill, 1px line at --color-line-strong (32% ink), text inherits page ink. Always paired with a primary .pill nearby.
On lime surface
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
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
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
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
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
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
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.
07 — Forms
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.
Default
Focus
Filled
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é.
08 — Device 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.
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.
09 — Case-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
Live · ImageRotator
1400ms cross-fade · 1.8s interval (sped up for demo)
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.
11 — Sticky 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
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 rest · y < 8px
Transparent over lime heroThe 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 ruleA 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 instantlyAny upward delta > 6px brings the header back. The threshold prevents flicker from tiny touch jitter.
Mobile menu open
Body scroll lockedOpening 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 = 0At top
Transparent
y = 8Surface flips
Cream + rule
y = 120Hide threshold
Below this: shy
Δy +6Scrolling down
Hide header
Δy −6Scrolling 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.
12 — Motion
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 staggerEach 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 translateEvery 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
Reveal on scroll
700ms · 16px translate · once per elementSection 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
Orbit pills — enter
520ms · cubic-bezier(0.2, 1.4, 0.4, 1) · scale 0.2 → 1.08 → 1Bubble-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 → 0A 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 offsetMultiple 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 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.
Accent panel
Lime in light · forest in dark
skill chipBody
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
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
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
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.
14 — Imagery
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.
15 — Font 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
400500Only 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 toui-sans-serif→system-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.
16 — Work 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
PayPal
Principal UX Designer
Building a developer-facing platform for global payments.
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-hoverdrives 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.
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
17 — Case 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.
eyebrow · Case study
Hero
- • Eyebrow + title
- • Tagline
- • Meta dl (Role / Team / Timeline / Platform / Reach)
eyebrow · —
Hero image
- • Full-bleed device frame or scene photo
eyebrow · Overview
Overview
- • h2 setup line
- • Two-column lede body copy
eyebrow · Strategy
Strategy
- • h2 framing line
- • Numbered strategy beats with eyebrow labels (3–4)
eyebrow · Research
Research
- • h2 finding
- • Supporting body + optional inline image
eyebrow · Design
Design
- • h2 system claim
- • Story pairs (multiple)
- • Optional full-bleed image
eyebrow · Outcome
Outcome
- • h2 result line
- • Stat row (3–4 stats)
eyebrow · Reflection
Reflection
- • h2 closing thought
- • Single Story block or paragraph
eyebrow · Previously / Next
PrevNext footer
- • 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.
18 — Accessibility & 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
Stacking order · top to bottom
z-[100]Lightbox + scrim
Highest layer. Renders through a portal; locks body scroll while open.
z-[60]Mobile menu overlay
Fixed full-viewport lime panel. Portal-rendered so page transforms can't trap it.
z-40Sticky header
Sits above page content. Hides past y > 120 on scroll-down, returns on scroll-up.
autoPage 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
19 — Gallery 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 / hinline 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
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.