Skip to content

Latest commit

 

History

History
517 lines (427 loc) · 33.2 KB

File metadata and controls

517 lines (427 loc) · 33.2 KB
name Wen Tjun — Full-stack builder
description A mineral canvas, confident editorial typography, and a tactile engineering sculpture.
colors
pale-mineral deep-slate muted-sage-gray burnt-rust dark-brick engraving-rust warm-ivory light-sage-surface deeper-sage-surface dark-forest-slate mist-rule sage-rule strong-sage-rule control-outline marker-outline muted-leader slider-sage rust-wash rust-halo skip-white
#e5eae9
#243337
#53615f
#a74425
#973b20
#ad532e
#fff4e6
#dce3df
#cdd8d2
#243b39
#abb7b6
#a6b3b0
#869895
#8da09a
#71867f
#728580
#9dafa8
#a7442510
#a7442522
#ffffff
typography
display display-desktop-min display-tablet-min display-tablet-max display-mobile-min display-mobile-max display-narrow brand brand-mobile introduction introduction-tablet introduction-mobile introduction-narrow perspective-title perspective-title-tablet perspective-title-mobile body body-tablet body-mobile body-narrow control control-mobile label contact marker tab-index tab-index-mobile map-place-desktop-max map-place-tablet-min map-place-tablet-max map-place-phone-max map-place-short map-brand-compact map-visit map-visit-compact map-attribution map-picker-count map-status map-year map-date map-date-compact map-picker-title map-picker-detail
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
58px
620
1.04
-0.035em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
42px
620
1.04
-0.035em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
38px
620
1.04
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
52px
620
1.04
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
40px
620
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
56px
620
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
38px
620
0.98
-0.04em
fontFamily fontSize fontWeight letterSpacing
var(--font-sans), Arial, sans-serif
30px
600
-1.3px
fontFamily fontSize fontWeight letterSpacing
var(--font-sans), Arial, sans-serif
25px
600
-1.3px
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
16px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
28px
500
1.1
-0.7px
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
25px
500
1.1
-0.7px
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
28px
500
1.1
-0.7px
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
16px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.5
fontFamily fontSize
var(--font-sans), Arial, sans-serif
13px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
12px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
14px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
15px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
12px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
11px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
11px
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
130px
450
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
44px
450
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
76px
450
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
64px
450
0.98
-0.04em
fontFamily fontSize fontWeight lineHeight letterSpacing
var(--font-sans), Arial, sans-serif
50px
450
0.98
-0.04em
fontFamily fontSize fontWeight
var(--font-sans), Arial, sans-serif
26px
600
fontFamily fontSize fontWeight
var(--font-sans), Arial, sans-serif
20px
500
fontFamily fontSize fontWeight
var(--font-sans), Arial, sans-serif
18px
500
fontFamily fontSize fontWeight
var(--font-sans), Arial, sans-serif
12px
400
fontFamily fontSize fontWeight lineHeight
var(--font-sans), Arial, sans-serif
13px
400
1.3
fontFamily fontSize lineHeight
var(--font-sans), Arial, sans-serif
15px
1.4
fontFamily fontSize fontWeight
var(--font-sans), Arial, sans-serif
16px
500
fontFamily fontSize
var(--font-sans), Arial, sans-serif
14px
fontFamily fontSize
var(--font-sans), Arial, sans-serif
13px
fontFamily fontSize fontWeight lineHeight
var(--font-sans), Arial, sans-serif
16px
500
1.3
fontFamily fontSize
var(--font-sans), Arial, sans-serif
14px
rounded
control map-visit-control
3px
4px
spacing
control-gap control-gap-compact page-desktop page-tablet page-mobile page-narrow
16px
8px
44px
30px
24px
20px
components
assembly assembly-hover reset reset-hover rotation perspective-tabs perspective-tab-selected profile-links map-atlas map-visit-picker
backgroundColor textColor typography rounded padding
{colors.light-sage-surface}
{colors.dark-forest-slate}
{typography.control}
{rounded.control}
0 10px
backgroundColor
{colors.deeper-sage-surface}
backgroundColor textColor typography rounded padding
transparent
{colors.muted-sage-gray}
{typography.control}
{rounded.control}
0 5px
backgroundColor
{colors.deeper-sage-surface}
backgroundColor height
transparent
44px
backgroundColor textColor padding
transparent
{colors.muted-sage-gray}
8px 4px
backgroundColor textColor
{colors.rust-wash}
{colors.dark-brick}
textColor
{colors.deep-slate}
backgroundColor textColor height
{colors.deep-slate}
{colors.pale-mineral}
100svh
backgroundColor textColor padding width
{colors.deep-slate}
{colors.pale-mineral}
8px 12px 12px
min(320px, calc(100vw - 32px))

Design System: Wen Tjun — Full-stack builder

Project: wentjun.github.io Surfaces: Personal homepage / and travel record /whereabouts · https://wentjun.com/ Refreshed: 5 October 2026 Basis: Sculpture-led refinement. Cool mineral palette retained; smaller introduction, attached layer names, connected assembled labels, and quiet footer.

Overview

Creative North Star: "Tactile engineering centerpiece"

The interactive four-layer sculpture leads the composition. A restrained introduction and the selected perspective occupy the left column. Persistent names connect the physical layers to Interface, Systems, Applied AI, and Delivery on wide screens. Assembly gathers those names into a connected row beneath the object.

The cool mineral background, slate text, glass, ceramic, graphite, metal, and copper remain the established palette. No cream canvas or additional accent colors are introduced. The headline stays subordinate to the sculpture. Contact, whereabouts, and profile links remain simple text links.

Colors

The palette combines cool mineral neutrals with warm burnt rust. Exact interface values live in the frontmatter and are implemented as semantic --color-* properties on :root in src/styles/global.css. The homepage styles, marker leaders, and assembly icon consume those properties; sculpture material gradients retain their authored values.

The companion .impeccable/design.json contains component specimens, motion, and breakpoint metadata. Its generated OKLCH tonal ramps are panel previews, not additional approved interface colors; alpha variants use their underlying hue for those previews.

Runtime token mapping

Design color CSS property
pale-mineral --color-page
deep-slate --color-text
muted-sage-gray --color-text-muted
burnt-rust --color-accent
dark-brick --color-selected-text
warm-ivory --color-on-accent
light-sage-surface --color-surface
deeper-sage-surface --color-surface-hover
dark-forest-slate --color-control-text
mist-rule --color-rule-header
sage-rule --color-rule-tabs
strong-sage-rule --color-rule-footer
control-outline --color-control-outline
marker-outline --color-marker-outline
muted-leader --color-leader
slider-sage --color-slider-track
rust-wash --color-selected-wash
rust-halo --color-focus-halo
skip-white --color-skip-surface

Primary

Burnt rust carries “builder.”, the signature period, selection, focus, and the slider thumb. Dark brick supports selected tab text; engraving rust stays distinct on the material faces. Rust wash and rust halo are translucent interaction variants.

Neutral

Color Role
Pale mineral Continuous page background; neutral marker fill; slider-thumb border.
Deep slate Primary text, first headline line, wordmark, and favicon background.
Muted sage-gray Supporting copy, location, inactive labels, and reset text.
Warm ivory Text on active rust markers and selected text.
Light sage surface Assembly button, desktop active tab, and tab hover background.
Deeper sage surface Assembly and reset hover background.
Dark forest-slate Assembly text and icon.
Mist rule Thin header divider.
Sage rule Top and bottom tab-strip borders.
Strong sage rule Footer divider.
Control outline Fine inset assembly-button border.
Marker outline Inactive numbered marker borders.
Muted leader Unselected lines connecting numbers to plates.
Slider sage Rotation track.

The keyboard skip link uses Skip white. Warm ivory supplies text on active rust markers and selected text.

Sculpture materials

Gradients describe physical materials, rather than decorative page backgrounds. Preserve their order from top to bottom.

Layer Material and face colors Edge colors
01 Interface Cool translucent glass: icy highlight #e4f5f5, mist blue #a5c6d1, pale aqua #c7e2e7, blue-gray #789ba9. The first three stops have 97%, 88%, and 94% opacity. #658a94 → #9cbec5 → #6e939d
02 Systems Warm ceramic: creamy highlight #fffcf0 → stone ivory #e7e0d0 → warm gray #c6bdab. #a99f8d → #d3cbbc → #b5aa97
03 Applied AI Dense graphite: gray-green #647171 → charcoal #343f40 → deep graphite #172327. #283537 → #435353 → #263638
04 Delivery Brushed metal: mineral gray #9ba7a7 → silver sage #d1dad5 → softened steel #abb6b2. #748481 → #b9c5bd → #879792

Copper-colored rods use a dark brown shaft (#644735), warm highlight (#d5a780), and light cap (#ebc2a0). Brass-toned collars (#b6a083) have dark rims (#796145). Inactive drawings use muted green-gray (#697b78), with lighter sage (#a8b7ad) on graphite for visibility.

Typography

Use the bundled variable sans-serif throughout the homepage and Whereabouts, including small numbers and angular readouts. The font file is src/app/fonts/sans.woff2; the layout declares weights 100–900. Its family name is not declared in the source, so this asset is the reference. Fallbacks are Arial and a generic sans-serif. Sculpture marker numbers, perspective indices, and the rotation readout use tabular numerals to keep digits aligned.

The headline uses clamp(42px, 4vw, 58px) at weight 620, line height 1.04, and −0.035em tracking; “builder.” starts a second line in rust. Tablet uses clamp(38px, 4.8vw, 52px). Mobile uses clamp(40px, 10.5vw, 56px) at line height 0.98, reducing to 38px on narrow screens.

Introduction and perspective body text are 16px on desktop and 15px elsewhere, with line height 1.5. The perspective heading is 28px, reducing to 25px on tablet. Tabs use 13px labels with 11px numerical indices, reducing to 12px labels on tablet and narrow mobile. Persistent sculpture labels are 14px, or 12px in the assembled row.

Layout

Desktop

The 96px header has 44px margins, name and location on the left, and contact on the right. The main layout uses a 29:71 grid with a 32px gap, 32px top padding, and 48px bottom padding. Introduction and perspective explorer occupy two rows on the left; the sculpture spans both rows on the right. The introduction has 48px top padding. Its supporting paragraph begins 24px below the headline.

The explorer begins 32px below the introduction. The caption uses a fine top rule, 24px top padding, and 280px minimum height. Desktop selection happens exclusively through the named sculpture controls; the separate tab strip is hidden. The sculpture frame is 680px tall with a ruled control area beneath it. There are no visible instructions or view-state captions above or below the object. At 1550px and above, content centers within 1500px and the frame grows to 720px.

The footer uses the same mineral background, a fine top rule, 44px side margins, 20px vertical padding, and 88px minimum height. Whereabouts sits left; Writing, GitHub, and LinkedIn sit right.

Intermediate widths

At 901–1100px, margins become 30px, the grid becomes 35:65 with a 24px gap, and the sculpture frame becomes 560px. At 701–900px, switch to a single column with a 440px sculpture frame capped at 640px wide; this avoids squeezing the copy beside the object. Introduction top padding is 32px and caption minimum height is 330px. Persistent layer names remain visible. Separated marker centers stay at least 110px from the frame’s left edge to reserve label space. Assembled names remain visible beneath their numbered row.

Mobile

At 900px and below, use one column with 24px margins, reducing to 20px at 380px and below. Order: header, introduction, perspective tabs, caption, controls, sculpture, footer. The DOM follows this reading order, with the desktop grid retaining its existing composition. The compact header is at least 88px high, with the Singapore location beneath the name and contact alongside. Introduction top padding is 28px and its paragraph has 24px top margin.

At 700px and below, the sculpture frame is 318px (268px on narrow screens), extending 18px beyond the content column at each side. Markers, attached labels, and leader lines hide. Four named tabs provide selection. Controls use two compact rows: Assemble/Separate on the left and Reset all on the right, then Rotate beside a full remaining-width slider. Rows are separated by 4px, with 8px between the toolbar and sculpture frame. Controls keep 44px hit targets, and mobile Up/Down keys remain available for scrolling. The caption reserves the intrinsic height of the longest perspective using overlapping grid cells; inactive panels remain hidden from view and interaction. This adapts to font size and width while preventing jumps between selections. Leave 24px before the controls. Tab numerals are 11px. Mobile toolbar keyboard order is Assemble/Separate, Reset all, then Rotate; desktop retains Assemble/Separate, Rotate, then Reset all. The footer puts Whereabouts above a row of three profile links.

Elevation & Depth

Header, introduction, caption, and sculpture share the mineral background. The footer retains the mineral background. Fine horizontal dividers organize the page. UI elevation is limited to the marker halo and button outline; physical shadows belong to the sculpture. The favicon is a pale mineral W on a squared deep-slate field.

The ground shadow fades from slate (#536266) at 21% opacity to transparent. Shadows cast between separated plates use dark green-gray (#344a49) at roughly 5–8% opacity with a soft blur. The plate-shadow SVG filter uses a Gaussian blur with standard deviation 10 in view-box units.

Shadow vocabulary

  • Assembly outline: inset 0 0 0 1px #8da09a.
  • Marker hover/focus halo: 0 0 0 3px #a7442522.
  • Selected tabs use a translucent rust wash, rust text, and a rust bottom edge.

These are outlines and state indicators. The page does not use elevated cards.

Shapes

Controls are small rectangles with gently softened corners (3px); tabs and layer labels stay unboxed. Marker faces are circular (32px) inside larger square hit targets (44px). The rotation thumb is circular. Fine one-pixel rules divide the header, tabs, and footer.

The sculpture uses rounded plate silhouettes, visible thickness, shaped apertures, and circular rod holes. Preserve its geometry in layer-geometry.ts; the UI does not imitate these materials with card surfaces.

Components

Buttons and links

The Assemble/Separate button is a compact sage rectangle with barely rounded corners (3px), a thin inset outline, and a small line-drawn stack icon. It has a minimum height of 44px. Hover or keyboard focus previews the next pose on the icon; activation changes the sculpture. Reset all is a quiet text button with a transparent resting background and sage hover fill when available. It is separated from rotation by an extra 12px on desktop. At the default selection, angle, and pose it is dimmed and marked aria-disabled; the handler ignores activation. It remains keyboard-focusable so resetting does not drop focus. Any changed layer, angle, or pose enables it. Assemble/Separate and Rotate share a named native fieldset with Reset all.

Say hello and the footer links are simple text paired with a northeast arrow. Hover adds an underline offset by 4px. The name remains a wordmark, without a link treatment.

Perspective tabs and caption

At widths above 900px, the four named sculpture buttons are the sole selector. They retain a roving tab stop, pressed state, and Left/Right, Up/Down, Home/End navigation. The selected explanation is a labelled region associated with its own heading. Clicking either a layer name or the plate updates it without moving focus.

At 900px and below, the markers hide and four compact horizontal tabs provide selection. The explanation becomes a tabpanel associated with its mobile tab. Selection uses rust text, a rust bottom edge, and a translucent rust wash. Unselected tabs use sage with slate text on hover; selected tabs retain their rust text and wash. Focus uses an inset slate outline. Left/Right and Home/End navigate mobile tabs. Selection survives resizing between the two layouts.

The caption retains the selected perspective in both poses. Assembly preserves selection. The changing Assemble/Separate action and the sculpture show the current pose; a visually hidden live status also announces it. Reserved height keeps the page stable across the four captions. A polite live region announces selected content updates.

Sculpture and markers

Four broad plates with softly rounded corners form an oblique stack. Glass, ceramic, and graphite plates have differently shaped apertures; the metal base is solid apart from the rod holes and slightly larger. Two copper-colored rods pass through matching holes and collars. Fine drawings sit on the material surfaces, following their perspective: person-to-agent-to-application, an application boundary, direct and model-assisted routes through a shared check, and an unfinished application becoming complete.

Desktop numbered markers are 32px circles within 44px hit targets. At widths above 900px, persistent names sit to their left and follow the layer geometry. When assembled, names move below the markers, which spread into a row joined by a fine line. Marker spacing is 112px in frames at least 650px wide, otherwise 88px. The object reserves room for this row and its labels; the connecting line fades in during the final third of assembly. Reduced motion applies the completed state immediately. The selected marker fills with rust and uses warm ivory text. Fine leader lines connect markers to separated plates; markers move into a row below the assembled object. Persistent names are part of the native buttons, with a padded clickable area beside each numbered marker. Hover and keyboard focus emphasize the corresponding plate; there are no duplicate hover tooltips.

Selected engravings always use engraving rust. In the separated desktop pose, a short rust edge segment marks selection; in the assembled pose, a broader sidewall band appears. On mobile, the selected sidewall band remains visible in both poses, while numbered markers, leader lines, and the short segment are hidden. Named tabs remain the mobile selection controls.

Inputs and interaction states

The rotation input is a thin sage track (3px) with a round rust thumb, a pale mineral border, and a small center tick. Its interactive area is 44px tall. The WebKit thumb is 18px including its border; Firefox uses a 14px content box plus a 2px border on each side. It rotates from −30° to +30° in one-degree steps. A sans-serif angle readout with tabular numerals appears above 1100px; smaller layouts hide the readout. There are no text-entry forms on the current page.

Default and Reset all select 03 Applied AI, with the plates separated at 0°. Assembly and rotation preserve the selected perspective. Pose changes use cubic ease-out (1 - (1 - progress)^3) over 450ms; reduced-motion preferences apply the destination immediately. The sculpture frame remains fixed during interaction so surrounding content does not move.

Keyboard focus uses a visible rust outline (2px), usually offset by 4px; compact controls place it inward. Mobile tabs and sculpture controls support arrow-key navigation and Home/End. Sculpture controls also support Up/Down. Preserve the keyboard skip link, named controls, and announced caption changes. Without JavaScript, all four perspectives appear as static text.

Controls remain disabled until the sculpture viewport has been measured. Without JavaScript, interactive controls are hidden and the static perspective copy remains available.

Do's and Don'ts

Do

  • Do keep “builder.” in burnt rust, with a dedicated second line on tablet and mobile.
  • Do retain glass, ceramic, graphite, and metal in their established layer order.
  • Do preserve 44px control hit areas, keyboard selection, and visible focus.
  • Do keep sculpture framing and caption space stable during interaction.
  • Do apply reduced-motion destinations immediately and retain static copy without JavaScript.

Don’t

  • Don't duplicate the desktop sculpture controls with a second tab strip or repeat their persistent names in tooltips.
  • Don't replace the bundled fonts or use illustration materials as generic UI backgrounds.
  • Don't treat historical screenshots as authority over the current source and responsive rules.

Source references

Visual evidence: desktop, tablet, mobile, and narrow-screen previews reviewed on 4 October 2026 during the sculpture-led refinement. The September archive is historical. The homepage specification is above; the Whereabouts specification follows below.

Homepage destination icons use a consistent 16px SVG stroke: a right arrow for internal Whereabouts navigation, a northeast arrow for external profiles, and an envelope for email contact. Accessible link names remain plain text.

Whereabouts

The approved travel surface is a continuous slate dark-water atlas: geography, a large selected place, and chronological history share one canvas. The homepage sections and tokens above remain unchanged; these rules apply only to /whereabouts. Recorded from the implemented surface on 8 October 2026.

Shared colors

Local map aliases reverse the homepage's foreground and background: --ink → --color-page (pale mineral), --paper → --color-text (deep slate), --muted → --color-rule-header (mist rule), and --accent → --color-accent (burnt rust). --rule mixes 16% ink with transparent; --surface mixes 7% ink with paper. The surface declares a dark color scheme. The skip link uses mineral behind slate text; map focus outlines and the selected pin ring use mineral, not the homepage's rust halo.

The transparent-water SVG reveals the slate canvas. Its muted sage land uses the existing sage-rule color and marker-outline coastlines; the generator and bundled artwork agree. Selected countries use mineral fill at 28% opacity and mineral stroke at 50%. Current and selected location dots use rust with mineral borders; selection adds a mineral ring and enlarges the dot from 9px to 13px. History selection uses the subtle 7% mineral/slate surface, heavier city text, and a rust dot edged in mineral. Unselected history hover uses a quieter 4% mineral/slate fill so it does not mimic selection. Rust never supplies the sole contrast cue.

The Continuous Atlas Rule. Keep header, history, title, and water on one slate field. Readability comes from broad map-wide slate fades, with a transparent history panel and no local title rectangle or frosted sidebar. Desktop fades protect the header and left reading column, and blend the bottom slate field from 31% to transparent at 54%; at ≤800px the bottom fade runs from 61% to 77%. The picker remains an opaque slate native popover.

Typography and layout

Every Whereabouts text role inherits the existing bundled proportional sans via var(--font-sans), Arial, sans-serif. src/app/layout.tsx loads src/app/fonts/sans.woff2 as a local variable font with weights 100–900. Titles, statuses, dates, years, controls, popover content, and mobile text are real DOM text using that font, not lettering baked into artwork. The homepage uses the same sans-serif family, including its numerical labels and rotation readout; no monospace font is loaded.

Role Desktop Compact / short screens
Selected place clamp(58px, 8vw, 130px), weight 450, line height 0.98, tracking −0.04em At ≤800px: clamp(44px, 10vw, 76px); ≤540px: clamp(42px, 12vw, 64px); height ≤650px: 50px, or 40px at ≤540px width
Wordmark 30px, weight 600, tracking −0.04em 26px at ≤800px
Visit city 20px, weight 500 (600 selected), tracking −0.5px 18px at ≤800px
Country 16px below the selected place; 13px in history Selected-place country becomes 14px at ≤800px; history stays 13px
Temporal status 15px, line height 1.4 Unchanged, including short phones
Year / date 16px, weight 500 / 14px Dates become 13px at ≤800px
Map attribution 12px 12px
Picker title and city 16px; title weight 500, line height 1.3; selected option weight 600 Unchanged
Picker detail and Close 14px 14px
Picker visit count Hidden above 800px 13px, weight 400, line height 1.3 at ≤800px, regardless of height

The Proportional Atlas Rule. Use the bundled sans everywhere on Whereabouts, including statuses, year/date, popover, and mobile. Keep captions in natural case with normal spacing; no monospace, uppercase transformations, or tracked captions. Preserve the selected-place title's existing 130px maximum with its implemented −0.04em tracking.

The page fills 100svh; travel history scrolls independently. Visit rows use an inset keyboard-focus outline so the scroll container does not clip its sides. Above 800px, history and map detail occupy separate columns: 340px history with a 52px gap, reducing to 310px with a 32px gap at 801–1100px. At ≤800px they stack, with map detail above history. Header minimum heights are 76px desktop, 60px at ≤800px, 56px at ≤540px, and 48px on screens ≤650px high. Visit rows have a 76px minimum height, or 68px at ≤800px. The attribution link has a 44px minimum height. On short screens (height ≤650px), the footer remains hidden to preserve map and history space.

Navigation and visit picker

The existing scroll-driven navigation remains: scrolling history changes the selected visit; vertical wheel and single-finger gestures over the map feed that same history. Pinch-to-zoom and horizontal gestures retain their browser behavior. History rows and visible map markers also select visits; the map measures history and status/title bounds, disables and hides map controls intersecting either reading surface, and keeps those visits selectable from the history list. Status copy remains “Currently in”, “Was in”, or “Heading to” with the selected visit date when applicable.

“Back to home” pairs its text with a 16px left arrow using the homepage icon vocabulary: 1.3px current-color stroke, round caps and joins. The decorative icon is hidden from assistive technology; the link retains its text name and 44px target height.

Grouped map markers open a native popover titled “Visits near [city]”; repeat visits confined to one city use “Visits to [city]”. Each option retains its country and date. No additional “Across all dates” subtitle is needed. Placement accounts for the trigger, selected location dot, neighboring pins, header, and viewport edges. The picker heading and Close button remain fixed while only the options scroll. At widths ≤540px and heights ≤700px, the picker uses the viewport width minus 32px, sits 16px above the bottom when there is room below its selected pin and place detail, and caps its height at 55% of the viewport. At all mobile widths (≤800px), regardless of height, a visit count with a 4px gap below the picker heading makes the full set apparent. If fewer than 144px remain for the short-phone placement, the picker falls back to its pin-aware placement candidates. Long lists scroll within the available space using a thin themed scrollbar; the fixed header stays outside that scrolling region. Close and Escape restore focus; selection or viewport changes dismiss stale context. The expanded map trigger retains its intentional 4px corner radius. Its minimum size is 160×44px with 8px vertical and 12px horizontal padding; it grows with enlarged text, up to the viewport width minus 32px. Measured control dimensions determine the offset above the selected pin and exclusion bounds for the header, reading surfaces, and neighboring markers.

Preserved interaction choices

Keep the compact dot-and-chevron cluster symbol and the expanded “Nearby visits” label. The selected history row retains its mineral-edged rust dot and stronger city weight; the dot links list selection to the map pin.

The history's bottom fade and partially visible next row provide the scrolling cue. Do not add introductory scroll instructions. The implemented fade is 32px while more history remains, and disappears at the end; the top fade is 12px when earlier history is available. Returning to the scroll origin restores the current location. No separate “Back to now” control is included, keeping the mobile interface restrained.

Camera motion

The Latest Selection Rule. Each selection cancels the pending camera frame and starts from the current interpolated position. Nearby travel takes 420ms; travel beyond one viewport width takes 720ms and widens progressively, capped at a 25% reduction in scale. Position uses smoothstep easing (t² × (3 − 2t)); widening uses a sine-squared envelope and returns to the destination scale. Repeat visits to the same city share geometry and do not restart movement. Reduced motion, first measurement, and resize apply the destination immediately. Country highlighting fades over 450ms with ease, or changes instantly under reduced motion.

Evidence: src/components/whereabouts/whereabouts.module.css, whereabouts.tsx, travel-map.tsx, travel-camera.ts, and use-travel-story.ts; src/app/layout.tsx; src/styles/global.css; public/world-map.svg; scripts/generate-travel-map.py.