PT Yejun Tak
Plot · 1.3.0 · September 2026

Find it. Try it. Build with it.

The components behind this site, with working examples, anatomy and guidance for when to use each one. Search by a component name or the problem you need to solve.

Aeonik · Geist Mono 7 form & feedback entries 5 live template examples
Research / In use

The same system, from portfolio to protocol.

The public AI readiness page shares this site's base template and stylesheets. Its research-specific layouts compose these existing elements.

Foundations and actions

Aeonik and Geist Mono, semantic color and spacing tokens, light and dark themes, primary and secondary buttons, links, and tags.

Inspect button components

Page and content patterns

Shared header and footer, page hero, section headings, CTA pairs, archetype cards and persona-grid components. On the protocol page, persona cards describe starting roles, not validated research personas.

Inspect the persona grid
Quickstart

Ship a component in 30 seconds.

Four patterns for the most common cases. The CSS is loaded on every page, copy, paste, ship.

1 A primary action
<button class="button button-primary" type="button">
  Submit
</button>
Full button doc
2 A capability tag cluster
  • Sales workflow
  • B2B SaaS
  • Featured
<ul class="m-tag-cluster">
  <li class="tag">Sales workflow</li>
  <li class="tag">B2B SaaS</li>
  <li class="tag tag-accent">Featured</li>
</ul>
Full tag doc
3 A page hero
Work

Selected work.

<section class="o-hero o-hero--page">
  <span class="o-hero-page-eyebrow">Work</span>
  <h1 class="o-hero-page-title">Selected work.</h1>
</section>
Full hero doc
4 An info callout
ROI estimate

Investment recovers by case study 13.

<article class="m-callout">
  <svg class="m-callout-icon">…</svg>
  <div class="m-callout-body">…</div>
</article>
Full callout doc

Need something else? Press ⌘+K to search the whole system.

Why Plot exists

A system is an investment, not a hobby.

Every choice on this site, every button, every hero, every case-study layout, comes from one tokenized source. That's expensive up front and saves time forever after. Honest accounting:

Before

Each page invented itself.

  • 12 case studies × bespoke styles ≈ ~48 hours of one-off CSS
  • 4 different button styles drifted across the site
  • 3 incompatible card layouts on /portfolio alone
  • New visitor had to read source to understand structure
After (Plot v1.2)

One system. Twelve documented components.

  • One token system, three-tier (primitive → semantic → component)
  • 12 documented components + 5 templates + 22 behavior hooks
  • New case study page: ~10 min from data file → live URL
  • Hiring managers can audit every decision via /design-system
ROI estimate

Up-front cost: ~80 hours building the tokenized system + documentation. Ongoing: ~2 hr/month maintenance (audit, deprecation, doc updates). Saved per future case study: ~6 hr. Break-even at ~13 future case studies; portfolio plans 5+ per year. Investment recovers in year one.

Production use

How Plot shows up in real work.

Every other page on this site consumes Plot. Below: a sample of what each route uses. Numbers are instance counts at v1.2.

RouteTemplateComponents consumed
/ Identity hero .o-hero--home · 5× .o-card-archetype · .o-status-pill · .m-cta-pair · .o-cta
/portfolio Filtered index .o-hero--page · .m-filter-row + 6× .m-chip · 15× .o-card-work · ~85 .tag
/portfolio/multi-role-analytics Case-study reading .o-hero--case · 6× .o-process · 3× .m-metric · 4× .o-artifact-grid items · .m-link-card · .o-cta
/portfolio/voice-ai-tmobile Case-study reading .o-hero--case · 7× .o-process · .m-callout--accent (confidential note) · 3× .m-metric · 5× .tag
/about Utility page .o-hero--page · 9× .m-career-entry · .m-eyebrow-title × N · .o-cta
/contact Utility page .o-hero--page · .o-contact-grid · 4× .m-field · .button button-accent
/404 Utility page .o-hero--page · .m-empty-state · .m-cta-pair

Click any route to see Plot in production.

01, Color

Three-tier tokens. Semantic is the canon.

Components reference semantic tokens (Tier 2) like --color-action. Semantic tokens reference primitives (Tier 1) like --amber-500. Component-specific tokens (Tier 3) like --button-radius live alongside the component CSS. New code always reaches for the semantic name.

Tier 2 · canon

Semantic tokens, what to use

Intent-named. Every component, atom, and template should reach for these names. They survive primitive rebrands.

Surface roles

--color-bgPage background
--color-bg-tintedSection tint
--color-surfaceCards, panels
--color-surface-elevatedRecessed wells, hover

Text roles

--color-text-primaryHeadlines · ink
--color-text-bodyBody copy
--color-text-mutedEyebrows, meta
--color-text-placeholderInput placeholder

Border roles

--color-border-faintInner dividers
--color-borderDefault hairline
--color-border-strongHover, emphasis

Action / accent roles

--color-actionSurface fill (button-accent)
--color-action-hoverHover state
--color-action-textText on light · AA ✓
--color-action-softSoft wash, badges
--color-action-focusFocus ring · 40% alpha
--color-action-fgFG on accent surface
Token chain

How a button reads tokens, primitive → semantic → component

Every Tier 3 component token resolves through Tier 2 semantic to a Tier 1 primitive. Change the primitive, the chain updates everywhere automatically. Change the semantic, only the components mapped to that intent move.

  1. Tier 1 · Primitive --r-3 8px Raw value. Components must NOT reference this directly.
  2. Tier 2 · Semantic var(--r-3) , in this system, radii primitives ARE the semantic layer (radii are too few to need a separate intent layer). For colors, the Tier-2 layer is explicit (--color-action → var(--amber-500)). For radii / spacing, primitives serve double duty.
  3. Tier 3 · Component --button-radius var(--r-3) → 8px A real component token, defined in tokens.css. The button atom reads --button-radius, never --r-3 directly.
  4. Usage .button { border-radius: var(--button-radius); } , resolves to 8px at runtime The atom CSS reads the component token. Component-token names survive primitive renames; only one place changes when the radius scale moves.
Tier 2 · canon

Data-viz scale

Five monochrome steps for charts and process diagrams, plus the highlight that draws the eye.

--viz-1
--viz-2
--viz-3
--viz-4
--viz-5
--viz-highlightThe one that pops
Tier 1 · primitives Raw values. Implementation detail, components must not reach here.

Gray scale

--gray-0
--gray-50
--gray-100
--gray-200
--gray-300
--gray-400
--gray-500
--gray-700
--gray-800
--gray-900
--gray-950

Amber scale

--amber-300dark-mode accent
--amber-500light-mode surface
--amber-600hover surface
--amber-700text · AA ✓
--amber-800text hover
Legacy · transitional Pre-rename names. Kept for v1.0 compatibility. New code: use the semantic name. Removal: planned for v2.0.
Legacy aliasUse instead (canon)Status
--bg--color-bgDeprecated · keep until v2.0
--surface--color-surfaceDeprecated
--surface-2--color-surface-elevatedDeprecated
--ink--color-text-primaryDeprecated
--ink-soft--color-text-bodyDeprecated
--muted--color-text-mutedDeprecated
--hairline--color-borderDeprecated
--hairline-strong--color-border-strongDeprecated
--hairline-faint--color-border-faintDeprecated
--accent--color-actionDeprecated
--accent-hover--color-action-hoverDeprecated
--accent-text--color-action-textDeprecated
--accent-soft--color-action-softDeprecated
--focus-ring--color-action-focusDeprecated
02, Typography

Aeonik for type. Geist Mono for numbers.

Sentence case. No uppercase headlines. Eyebrows are the only place uppercase lives. Fluid clamp() sizes scale from mobile (375px) to wide desktop (1440px+).

.t-display-xl 48 → 96px / 500 / 1.02
Crafted in detail.
.t-display-l 36 → 64px / 500 / 1.04
A working design system, not a brochure.
.t-title-xl 28 → 40px / 500 / 1.1
Tokens, not values.
.t-title-l 22 → 28px / 500 / 1.18
Atoms compose into molecules.
.t-title-m 18px / 500 / 1.3
Card titles and list headers.
.t-body-l 17px / 400 / 1.55
Body large, used for the lede in case study heroes and primary reading copy on the home page. Tight tracking (-0.003em) keeps it sharp at 17px without sacrificing readability.
.t-body-m 15px / 400 / 1.55
Body medium, the workhorse. Card descriptions, secondary paragraphs, support copy across sections.
.t-body-s 13px / 400 / 1.3
Body small, captions, footnotes, meta information that supports without competing.
.t-eyebrow 12px / 500 / +0.08em
Section eyebrow, uppercase
.t-mono 14px / 500, Geist Mono
$ 1,234,567 · v1.0.42 · 03:42:18 UTC
.t-mono-s 12px / 400, Geist Mono
--accent: hsl(38 95% 50%);
03, Spacing

4px base, geometric scale.

Eleven steps. Every margin, padding, and gap in the system is one of these, never a one-off pixel.

TokenValueUse whenVisual
--s-14pxTightest inline gap (icon ↔ adjacent text inside a button)
--s-28pxInline element gaps (cluster items, badge ↔ label)
--s-312pxVertical rhythm in card body, list item gap
--s-416pxSection padding (mobile), gap between blocks
--s-524pxSection padding (desktop), gap between cards
--s-632pxBetween subsections, paragraph spacing in long-form
--s-748pxBetween sections (default .o-section padding)
--s-864pxBetween major page regions, hero block bottom padding
--s-996pxHero padding-block, generous section breaks
--s-10128pxTop-edge breathing for the home hero (.o-hero--home)
--s-11160pxReserve, not currently used. Kept for layout patterns that need exceptional separation (full-bleed gallery → next chapter).
04, Radii

Six steps, including pill.

--r-14px · tags
--r-26px · inputs
--r-38px · buttons
--r-412px · cards
--r-516px · panels
--r-full9999px · pills
05, Shadows

Three elevation levels.

--shadow-1Hairline lift
--shadow-2Card default
--shadow-3Hover lift, modals
06, Button

The unit test for the system.

If we can't document the button, we can't document anything. Below: full anatomy, four variants with usage rules, every state with its trigger, full accessibility receipts, and the complete token chain. The doc represents the same .button atom shipping on every page of this site.

Atom Stable

Button

A clickable surface for actions. Triggers a transition, submission, or change of state, never used for navigation between pages (that's a link).

.button · /static/system/atoms.css
Usage~28
Variants5
States6
A11yAA ✓
Touchedv1.2
Bus factor1
Live
Anatomy

Real button styles and link behavior. Try Tab, then Enter.

Start a conversation
Label and optional icon sit inside the shared padding and radius. The second specimen is disabled.
  • iconoptional leading slot, 16px svg, stroke-width 1.6
  • contentrequired, the label text
  • trailing iconoptional, e.g. arrow or chevron
  • padding-xvar(--button-padding-x) = 16px (sm: 12, lg: 22)
  • padding-yvar(--button-padding-y) = 8px (sm: 4, lg: 12)
  • min-height36px (default), 32px (sm), 48px (lg)
  • min-width72px in the default button
  • radiusvar(--button-radius) resolves to var(--r-3) = 8px
Variants
ClassUse whenVisual
.button-primary The single most important action on the surface (CTA, "Submit", "Continue"). Maximum one per visible region.
.button-accent Reserved for the home-page primary CTA where amber is meaningful brand emphasis. Use sparingly, overuse drains its weight.
.button-secondary Supporting action paired with primary or accent (e.g., "Cancel" beside "Submit", "Contact" beside "Review the work").
.button-ghost Tertiary, low-emphasis action in toolbars, table rows, or recovery flows. Visible on hover.
.button-icon Compact circular control for toolbars (close, settings, copy). Always requires an aria-label.
Sizes
ClassMin heightPaddingUse whenExample
.button-sm 32px 4 / 12 Inline actions in lists, secondary CTAs in compact UI
, (default) 36px 8 / 16 Standard form actions, page-level CTAs
.button-lg 48px 12 / 22 Hero CTAs only, never inside dense UI
States
StateVisual changeTrigger
default Resting visual per variant Initial render
:hover Background shifts one step (primary → ink-soft, accent → amber-600) Mouse over (only on hover-capable devices)
:active scale(0.97) over 100ms with --ease-out Mouse-down, Enter, Space
:focus-visible 3px focus ring at --color-action-focus (40% amber) Tab from keyboard. Mouse focus does not show the ring.
[data-loading] Inline spinner replaces leading icon; content remains visible data-loading="true" attribute set by the consumer
:disabled 50% opacity, pointer-events: none disabled attribute or aria-disabled="true"
Accessibility
  • Min target40 × 80 px (default size). Icon button is 36 × 36, bumped to 44 × 44 on touch via media query. Touch users always meet WCAG 2.5.5.
  • Color contrastPrimary: 18.5:1 (ink on bg, AAA). Accent: 5.5:1 (white on amber-500, AA Large). Secondary: 18.5:1 text + 3:1 border.
  • KeyboardTab to focus. Enter or Space activates. Esc never closes a button.
  • ARIA, icon buttonaria-label required. Without it, screen readers announce "button" with no name.
  • ARIA, loadingSet aria-busy="true" in addition to data-loading. Update aria-label to "Loading…" so the change is announced.
  • ARIA, disabledPrefer disabled attribute (removes from tab order). Use aria-disabled="true" only when the button must remain focusable to expose a tooltip explaining why.
  • Reduced motion:active scale animation is preserved (it's a 100ms tap response, not motion sickness territory). Hover background transitions remain.
  • Screen reader announcement"[Variant], [content], button", e.g., "Submit, button". For loading: "Loading, Submit, button".
Behavior

The button uses a hardware-accelerated transform: scale(0.97) on :active with a custom cubic-bezier(0.23, 1, 0.32, 1) ease-out at 100ms. The whole button responds, not just the text, that's the difference between feeling clicked and feeling pressed.

Hover transitions cap at 220ms. Color and border interpolate via --ease-quick. There is no shadow on hover, Ive's restraint applies; the variant's job is to look unmistakable at rest, not to perform interactivity on hover.

Code
<!-- Default -->
<button class="button button-primary" type="button">Submit</button>

<!-- Loading state -->
<button class="button button-primary" type="button"
        data-loading="true" aria-busy="true"
        aria-label="Loading…">
  <span class="button-content">Submit</span>
</button>

<!-- Icon-only -->
<button class="button button-icon" type="button"
        aria-label="Settings">
  <svg width="16" height="16" ...>...</svg>
</button>
Tokens used
  • --button-radius→var(--r-3) = 8px
  • --button-padding-x→16px
  • --button-padding-y→8px
  • --button-min-height→36px
  • --button-active-scale→0.97
  • --button-icon-gap→var(--s-2) = 8px
  • Primary fill→var(--color-text-primary)
  • Accent fill→var(--color-action) = var(--amber-500)
  • Focus ring→var(--color-action-focus) = amber-500/0.4
Do / Don't
✓ Do
  • Use exactly one Primary or Accent button per visible region, they compete for the eye.
  • Pair Primary with Secondary (e.g., "Submit" + "Cancel"). Pair Accent with Secondary (e.g., "Get started" + "Contact").
  • Use aria-label on every .button-icon, without it, screen readers announce "button" with no name.
  • Set aria-busy="true" in addition to data-loading so the state change is announced.
  • Reserve .button-lg for hero CTAs only. Default size for everything else.
✗ Don't
  • Don't use Accent for non-action UI (decorative dividers, status labels, headings). Amber is a verb, not an adjective.
  • Don't put two Primary buttons next to each other, the user can't tell which is "the" action. Use Primary + Secondary.
  • Don't use Ghost as a primary action, its low visual weight is the entire point. It's tertiary.
  • Don't disable a button without explaining why (use a tooltip with aria-describedby). Silent disabled = mystery.
  • Don't bypass .button-content for the loading state, the blur-mask transition needs that wrapper to fade between text and spinner cleanly.
Anti-patterns

Two visual lessons. Words tell; visuals teach.

✓ Right

Primary + Secondary pairing, user knows immediately which is THE action.

✗ Wrong

Two Primaries compete. User can't tell which action is "the" action, eye loses.

✓ Right

Icon button with aria-label="Settings", screen readers announce "Settings, button."

✗ Wrong

No aria-label, screen readers announce "button" with no name. Keyboard users are stranded.

Used by

Reverse-dependency map. Changing .button CSS will affect every consumer below, coordinate the audit before MAJOR changes.

  • .m-cta-pair←contains 1–2 buttons
  • .o-card-work-cta←on /portfolio card foot
  • .o-cta←closing CTA on every page
  • .o-toast-action←action button inside toast
  • .o-dialog←dialog footer actions
  • .o-hero-home←via .m-cta-pair
  • .theme-toggle←uses .button-icon variant
07, Tag

Compact metadata that doesn't shout.

Atom Stable

Tag

Compact non-interactive label for capability, technology, status, or version metadata. Tags are descriptive, never clickable. Use chips for filters.

.tag · /static/system/atoms.css
Usage~85
Variants5
States,
A11yAA ✓
Touchedv1.0
Bus factor1
Live
Default Outline Accent v1.1.0 Pill
Anatomy
  • Public Preview
  • Product design
These are the real metadata tags. They are not interactive controls.
  • iconoptional 12px svg, 6px gap from text
  • textrequired, 12px, weight 500
  • padding3px 10px via component tokens
  • radiusvar(--tag-radius) = 4px (pill = 9999)
  • line-height1.4, tags should never wrap
Variants
ClassUse whenVisual
.tag Default capability or domain label inside cards (e.g., "Sales workflow", "B2B SaaS") Sales workflow
.tag-outline Visible on tinted surfaces where the default fill blends in Outline
.tag-accent One-per-card emphasis, the "featured" or "primary capability" tag Featured
.tag-mono Version, hash, ID, or any tabular value where character-shape needs to lock v1.1.0
.tag-pill Modifier for fully rounded shape, used in cluster groups for a softer rhythm Pill
Accessibility
  • RoleNone, tag is non-interactive. If you need interactivity, use .m-chip instead.
  • Color contrastDefault: 5.2:1 (ink-soft on surface-2). Accent: 4.81:1 (amber-700 on amber-soft).
  • Screen readerReads as plain text. For status meaning, prefer a sentence over a tag (e.g., "Status: shipped" beats <tag>Shipped</tag>).
  • Multiple tagsUse .m-tag-cluster with <ul> + <li> for screen reader list semantics.
Code
<!-- Single tag -->
<span class="tag">Sales workflow</span>

<!-- Cluster (semantic list) -->
<ul class="m-tag-cluster">
  <li class="tag">B2B SaaS</li>
  <li class="tag tag-mono">v2024.Q3</li>
  <li class="tag tag-accent">Featured</li>
</ul>
Tokens used
  • --tag-radius→var(--r-1) = 4px
  • --tag-padding-y→3px
  • --tag-padding-x→10px
  • --tag-bg→var(--color-surface-elevated)
  • --tag-text→var(--color-text-body)
Do / Don't
✓ Do
  • Use Default for descriptive metadata (capability, technology, domain). Most tags should be default.
  • Use .tag-mono for version, hash, ID, anything where character-shape needs to lock visually.
  • Use exactly one .tag-accent per card. It's the "featured" emphasis, multiple accent tags read as decoration.
  • Wrap multiple tags in .m-tag-cluster with semantic <ul> + <li> for screen-reader list semantics.
  • Keep tag text under 15 characters. Tags don't wrap, long text breaks the rhythm.
✗ Don't
  • Don't make tags clickable. If you need interactivity, use .m-chip, that's the filter molecule.
  • Don't use tags to communicate status ("Shipped", "In progress"). A sentence is more accessible than a tag for state meaning.
  • Don't mix accent tags with mono accent text in the same card, two amber sources fight for attention.
  • Don't put more than 4 tags in one cluster. After 4, the visual weight overpowers the title/teaser.
  • Don't style tags with custom colors. The system has 5 variants, add a sixth only after a Tier 3 governance discussion.
Anti-patterns
✓ Right
  • Sales workflow
  • B2B SaaS
  • Featured

3 tags max, one accent for emphasis. Visual rhythm intact.

✗ Wrong
  • Sales
  • B2B
  • SaaS
  • Workflow
  • Inline

5 accent tags, every tag screams. Eye has nowhere to land. Drains the accent's signal entirely.

Used by
  • .m-tag-cluster←contains tag list (semantic ul/li)
  • .o-card-work←capability cluster on card foot
  • Hero tag rows←v1.x hero "Aeonik / Geist Mono / 12 components"
  • Case study capabilities←.o-hero-case meta block
08, Input

Quiet by default. Loud on focus.

Atom Stable

Input

Single-line text field. Hairline at rest, amber focus ring on keyboard tab. Always paired with a label via the .m-field molecule, never floating.

.input · /static/system/atoms.css
Usage4
Variants2
States5
A11yAAA ✓
Touchedv1.1
Bus factor1
Live
Anatomy

Choose one bounded unit of work. This local example does not save or send text.

Click the label, type, or use Tab. The label stays visible and the help text is explicitly connected to this input.
  • fieldfull width by default, wrap in .m-field for max-width
  • padding8px / 12px (sm: 6px / 10px)
  • min-height36px minimum; surrounding spacing also matters for target size
  • border1px visible control boundary at rest, action-text color on focus
  • focus ringOn this documentation page, a single 2px action-text outline with a 2px gap
  • placeholderuses the theme-aware --color-text-placeholder token
Variants
ClassUse whenMin height
.input Standard form fields, contact forms, search 36px
.input-sm Inline filter inputs, dense table editors 36px
States
StateVisualTrigger
defaultSurface bg, hairline borderInitial render
:hoverBorder darkens to --color-border-strongMouse over (hover-capable only)
:focus-visibleAction-color border and one 2px outline on this page, without stacked shadowsTab from keyboard
:invalid(consumer-styled, system does not impose)HTML5 validation API
:disabled50% opacity, cursor: not-alloweddisabled attribute
Accessibility
  • Min target40 × full-width, meets WCAG 2.5.5 by default
  • Color contrastText 18.5:1 (AAA). Placeholder 4.6:1 (AA). Border 3:1 in focus state.
  • LabelAlways pair with a <label> via for=/id= or wrap in .m-field. Never use placeholder as label substitute.
  • RequiredUse the required attribute. Visual indication via .m-field--required. Screen readers announce "required".
  • Help textReference via aria-describedby when not using .m-field-help.
  • KeyboardTab focuses, Esc does NOT clear (browser default). Enter submits the form when applicable.
Code
<!-- Bare input (NEVER ship this, always pair with a label) -->
<input class="input" type="text" placeholder="…">

<!-- With label (canonical), uses .m-field molecule -->
<div class="m-field m-field--required">
  <label class="m-field-label" for="email">Work email</label>
  <input class="input" id="email" type="email" required>
  <span class="m-field-help">We'll only use this to schedule a call.</span>
</div>
Tokens used
  • --input-radius→var(--r-2) = 6px
  • --input-padding-y→8px
  • --input-padding-x→12px
  • --input-min-height→36px
  • --input-border→1px solid var(--color-control-border)
  • --input-border-focus→1px solid var(--color-action-text)
  • --input-focus-ring→0 0 0 3px var(--color-action-text)
Do / Don't
✓ Do
  • Always pair with a <label> via for/id or wrap the input + label inside .m-field.
  • Use the required attribute on required fields. Pair with .m-field--required for the visual indicator.
  • For helper copy, use .m-field-help beneath the input. Explicitly connect its ID with aria-describedby; CSS does not create that relationship.
  • Match input type to the data: email, tel, url, number. Mobile keyboards adapt.
  • Use .input-sm for inline filter inputs and dense table editors only, not for primary forms.
✗ Don't
  • Don't use placeholder as a label substitute. Placeholders disappear on type and fail screen readers.
  • Don't ship a bare <input> in production. Always wrap or label.
  • Don't remove keyboard focus or stack a shadow and multiple outlines. This page uses one 2px outline with a 2px gap.
  • Don't disable inputs without explaining why (use aria-describedby pointing to a help message).
  • Use the documented textarea for longer answers and select for a bounded list of choices.
Try the guidance

Do: keep the label visible

Type to see the label stay in place. This example sends nothing.

Avoid: a placeholder as the only label

Once text is entered, a placeholder-only design loses its visible label. This read-only specimen keeps an accessible name; do not copy the pattern into a form.

Copy the labeled field pattern
<div class="m-field">
  <label for="email">Email address</label>
  <input class="input" id="email" type="email"
    aria-describedby="email-help">
  <p class="m-field-help" id="email-help">How we use your email.</p>
</div>
Used by
  • .m-field←canonical input wrapper (label + input + helper)
  • .o-contact-grid←/contact form fields
  • DS examples←Section 08 live demo (this page)
10, Dividers & dots

The smallest atoms.

.divider, hairline
.divider-strong
Spacer dots:
11, Motion (reference)

Curves and durations live in tokens.

Asymmetric: exits faster than entries. UI animations stay under 320ms. Keyboard-driven actions never animate. Reduced-motion users get opacity-only transitions, no movement.

Curves --ease-out: cubic-bezier(0.23, 1, 0.32, 1) --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1) --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1) --ease-spring: cubic-bezier(0.5, 1.5, 0.5, 1)
Durations --dur-press: 100ms, button :active --dur-instant: 160ms, tooltips, exits --dur-quick: 220ms, dropdowns, hover --dur-base: 320ms, modals, drawers --dur-slow: 480ms, hero reveals
12, Molecules

Atoms compose into purpose.

Small assemblies of atoms, each with a specific job. Choose any of the twenty entries in the contents rail or jump-to search. Examples show the component in use; additional interaction patterns follow in the additions section.

Eyebrow + title

A .m-eyebrow-title , eyebrow + title pair (the most-used molecule in the system)
Selected work

Research-led case studies across enterprise AI, dashboards, healthcare.

01Career arc

Twelve years across enterprise, healthcare, and consumer.

Hero lede

B .m-hero-lede , page hero pattern (eyebrow + h1 + descriptive paragraph)
About

Lead product designer. T-Mobile, Microsoft, Home Depot.

I'm a UX designer with experience shipping enterprise AI systems, analytics platforms, developer tools, and design systems at T-Mobile and Microsoft. My strongest work is the layer between research and product architecture.

Breadcrumb

C .m-breadcrumb , slash-separated chain (case study top, listing meta)

Quote

D .m-quote , pullquote with attribution (research findings, testimonials)

I'd update the deal in my head three times before I'd open the form. By the time I'd type it in, the conversation had already moved on.

Sales rep, 4 years tenure Generative interview · Meridian CRM research

Stat

E .m-stat , value + label (proof strip, summary numbers)
12 yrs UX & product design
17 Case studies in archive
47% CRM adoption failure rate (Forrester)

Metric

F .m-metric , full metric tile with value, label, note, source (case study impact band)
2.4×

Faster deal update task completion

From 6-step modal to inline edit. Measured against baseline click-path in a 12-rep usability study.

Source: Internal usability test, n=12
+31%

Voluntary activity logging

After 6-week pilot with sales operations team.

Source: Pendo event tracking, Q3 2024
−43%

Time spent in CRM per day

Reduction without loss of pipeline data quality.

Card header

G .m-card-header , eyebrow + title + meta (top of work cards, archetype panels)
Enterprise SaaS 4 case studies

CRM & sales workflow redesign

Meridian Senior UX Designer 3 months 2024

Timeline + entry

H .m-career-entry , period + role + note (about page career arc)

Wrap N entries in .m-timeline to enable the vertical spine + per-entry dot. Each entry takes data-reveal; the dot fills with accent the first time the entry crosses the viewport. Spine is desktop-only (≥641px); under that the timeline collapses to a hairline divider per row.

Apr 2025, Dec 2025
T-Mobile Lead Product Designer, Network & AI

Led end-to-end UX design of customer booking and service scheduling workflows tied to retail support and enterprise design system components.

Sep 2023, Mar 2025
Home Depot Sr UX Designer, Pro & B2B

Designed pro-channel checkout, account hierarchy, and credit application flows for $7B+ in annual pro revenue.

CTA pair

I .m-cta-pair , button cluster (primary + secondary action group)

Filter row

J .m-filter-row , wrapping chip row (work page domain filters; ink fill on active, never accent)

Tag cluster

K .m-tag-cluster , wrapping list of static tags (capability tags, technology stack)
  • Sales workflow
  • B2B SaaS
  • Inline editing
  • Mobile-first
  • v2024.Q3
  • Featured

Field

L .m-field , form field (label + input + helper text)
We'll only use this to schedule a call. No newsletter, no list.
Optional, helps me tailor what I send back.

Avatar

M .m-avatar , circular avatar with initials or image (personas, brand, contact)
PT PT PT SD JM RW

Link card

N .m-link-card , clickable navigation block (next case study, related work)
13, Organisms

Atoms and molecules assembled.

Page-level compositions. The header you've been using on this page is already .o-header, that's the canonical site nav. Below: the rest of the organisms that build the live pages in Session 5.

Status pill

A .o-status-pill , live status indicator with pulsing amber dot (home hero)
Open to lead-level roles · 2026 Currently shipping at T-Mobile
Organism Beta

Hero

The unified page-opening composition. Three modifiers cover every hero in the system: --home (split layout with motion graphic), --page (generic eyebrow + title + lede), and --case (case-study breadcrumb + title + visual + meta). Same base, different layout.

.o-hero · /static/system/organisms.css
Usage22
Variants3
States3
A11yAA ✓
Touchedv1.2
Bus factor1
Live · .o-hero--home
Open to lead-level roles · 2026

Peter Tak

Lead product designer working in the layer between research and product architecture.

UX DesignerT-MobileMicrosoft
Live · .o-hero--page
Selected work

Case studies across enterprise AI, analytics, healthcare, and design systems.

17 cases, client work at T-Mobile and Microsoft.

Composition trail

All three variants share the .o-hero base (max-width, centered, container padding). Modifiers compose different inner element sets:

Product design and systems

Make the next step clear.

Start with the person, the task and the decision they need to make.

A working composition of the shared eyebrow, title, lede and action components.
Variants
ModifierUse whenLayoutPadding-block
.o-hero--home The home page only, identity + motion graphic Single column at < 880px, 1.1fr / 1fr split at desktop var(--s-10) var(--s-9)
.o-hero--page Section index pages (Work, About, Contact, 404) Flex column, eyebrow stacked over title over lede var(--s-9) var(--s-7)
.o-hero--page.o-hero--compact Page hero with no lede (e.g., when filter chips follow immediately) Same as --page var(--s-9) var(--s-5)
.o-hero--case Case study pages, breadcrumb-led, title + lede, visual + meta grid Breadcrumb + head + 2-col grid (1.6fr / 1fr) at desktop, stacks at < 880px var(--s-9) var(--s-7)
Migration · v1.1 → v1.2
Legacy classCanonical classStatus
.o-hero-home.o-hero.o-hero--homeAliased, both work; legacy slated for v2.0 removal
.o-hero-page.o-hero.o-hero--pageAliased
.o-hero-page--compact.o-hero.o-hero--page.o-hero--compactAliased
.o-hero-case.o-hero.o-hero--caseAliased

Inner element classes (.o-hero-home-content, .o-hero-page-eyebrow, .o-hero-case-grid, etc.) keep their existing names. The refactor unifies the wrapper only; deeper renaming would break consumers without enough benefit.

Code
<!-- Home -->
<section class="o-hero o-hero--home">
  <div class="o-hero-home-content">…</div>
  <div class="hero-motion-frame">…</div>
</section>

<!-- Page -->
<section class="o-hero o-hero--page">
  <span class="o-hero-page-eyebrow">Work</span>
  <h1 class="o-hero-page-title">…</h1>
  <p class="o-hero-page-lede">…</p>
</section>

<!-- Case -->
<section class="o-hero o-hero--case">
  <ol class="m-breadcrumb">…</ol>
  <div class="o-hero-case-head">…</div>
  <div class="o-hero-case-grid">…</div>
</section>
Tokens used
  • Container max-width→var(--w-default) = 1120px
  • Container inline padding→var(--container-pad)
  • --home padding-block→var(--s-10) var(--s-9)
  • --page padding-block→var(--s-9) var(--s-7)
  • --case padding-block→var(--s-9) var(--s-7)
  • --case grid gap→var(--s-6)
  • Title color→var(--color-text-primary)
  • Lede color→var(--color-text-body)
Do / Don't
✓ Do
  • Pick the right modifier for the page: --home (split layout, motion graphic), --page (eyebrow + title + lede), --case (breadcrumb + visual + meta).
  • Use --page.--compact when no lede follows, sets bottom padding tighter so a filter row reads close to the title.
  • Keep titles within their max-width caps, display-l caps at 22ch, body-l ledes at ~60ch.
  • Inner elements use the legacy single-name classes (.o-hero-home-content, .o-hero-page-title), refactor stops at the wrapper.
  • For new routes: pick the closest existing modifier. Don't invent a new one without a governance discussion.
✗ Don't
  • Don't use .o-hero--home on any page other than /. The motion graphic is identity-specific.
  • Don't put CTAs in .o-hero--case. Hero is for orientation; CTAs come at the closing.
  • Don't mix the legacy class (.o-hero-home) and canonical (.o-hero.o-hero--home) on the same element. Pick one.
  • Don't override the hero's max-width or padding inline. If a page needs different dimensions, it needs a different template.
  • Don't add a hover state to the hero, heroes don't need to perform interactivity.
Organism Stable

Card-work

The case-study listing card used across /portfolio and the home page work index. Composes the project thumbnail, meta strip, title, teaser, capability cluster, and headline metric into a single clickable surface.

.o-card-work · /static/system/organisms.css
Usage15
Variants1
States4
A11yAAA ✓
Touchedv1.2
Bus factor1
Composition trail

Inspect the live production card below. The parts list and source classes explain its composition.

The same card template used on the Work page, populated with public research content. No private case content is exposed. Its image, metadata, title, summary and tags form one link. Tab to inspect the focus state; Enter opens the case.
Variants
ClassUse whenSurface
.o-card-workDefault, used on /portfolio and home archetype indexSurface fill, hairline border, hover darkens border only
data-domain="…"Filter attribute consumed by the work-page filter rowNo visual change, used by JS to show/hide
States
StateVisualTrigger
defaultHairline border, surface fillInitial render
:hoverBorder-color shifts to --color-border-strong. No lift, no shadow (Ive restraint).Mouse over (hover-capable)
:focus-visible2px accent outline, 4px offsetTab from keyboard
:activescale(0.985) tap responseMouse-down or Enter
Accessibility
  • Whole-card linkThe entire <a> is the link target. Inner elements must not include nested links, that breaks screen-reader navigation. Tag list is non-interactive on purpose.
  • Title roleUse <h3> at the page level. Inside the design system showcase, <h4> respects heading hierarchy.
  • KeyboardTab focuses card. Enter activates the link. :focus-visible shows the accent outline; mouse focus does not.
  • Color contrastTitle 18.5:1 (AAA). Teaser 7.2:1 (AAA). Meta 4.6:1 (AA). Accent metric 4.81:1 (AA ✓).
  • Reduced motionCard transitions cap at 220ms. Active scale animation is preserved (sub-100ms tap response is not motion sickness territory).
  • Touch targetWhole card is the target, minimum height ~340px. WCAG 2.5.5 ✓.
Code
<a class="o-card-work" href="/portfolio/multi-role-analytics"
   data-domain="Enterprise SaaS">
  <div class="o-card-work-thumb">
    <span class="o-card-work-thumb-mark">Dashboard</span>
  </div>
  <div class="o-card-work-body">
    <div class="o-card-work-meta">
      <span>T-Mobile</span><span>2025</span><span>8 mo read</span>
    </div>
    <h3 class="o-card-work-title">Multi-Role Analytics Dashboard</h3>
    <p class="o-card-work-teaser">…</p>
    <div class="o-card-work-foot">
      <ul class="m-tag-cluster">
        <li class="tag">Multi-role</li><li class="tag">AI</li>
      </ul>
      <span class="t-mono-s t-accent">1 platform</span>
    </div>
  </div>
</a>
Tokens used
  • --card-radius→var(--r-4) = 18px
  • --card-border→1px solid var(--color-border)
  • --card-border-hover→1px solid var(--color-border-strong)
  • Body padding→var(--s-4) var(--s-5)
  • Title color→var(--color-text-primary)
  • Metric color→var(--color-action-text)
Content states

How the card behaves across data conditions, loading, missing, error, filtered. Most consumer scenarios pass through these states; documenting them prevents inconsistent UX.

StateWhat rendersTrigger
defaultFull card with thumb + meta + title + teaser + footProject data fully populated
loadingSkeleton via .skeleton atoms in each slot. Use aria-busy="true" on the wrapping anchor.Data fetch pending (currently SSR-only, so this is a v2.0 client-side pattern)
empty (no metric)Card renders without the metric span in foot. firstMetric helper auto-skips placeholder values.SuccessMetrics all ", fill in" or empty
empty (no thumb image)Falls back to .o-card-work-thumb-mark mono label only. Thumb area stays sized via aspect-ratio.No project image available
filtered outdisplay: none applied by filter JS. Card preserved in DOM for state-restore.Active data-filter doesn't match data-domain
all-filteredParent .ds-grid swaps to .m-empty-state when zero cards match.Every card in group filtered out
error(Not applicable, cards are SSR; no client-side fetch failure path. Documented for v2.0 if SPA mode added.),
Slot contracts

What can go inside each slot. The card has fixed structure; deviating breaks layout or accessibility.

SlotRequiredAllowedForbidden
.o-card-work-thumbNone, slot can be empty<img>, <picture>, <svg>, <video muted loop>Interactive content (buttons, links, would break whole-card-link semantics)
.o-card-work-thumb-markNone, but recommended as fallback when image absentPlain text only, atom uses .t-mono-sSVG, links, formatted content
.o-card-work-meta1+ <span> elementsPlain text strings only (client name, year, timeline)Links, buttons, meta is descriptive, not navigational
.o-card-work-titleHeading element (h3 default)<h3> or <h4> per page hierarchyLinks, the parent <a> handles navigation
.o-card-work-footOptional.m-tag-cluster with up to 2 tags + 1 metric spanButtons, multiple metrics, >2 tags (visual weight overpowers title)
Do / Don't
✓ Do
  • Make the entire <a> the link target. Card hover changes border-color only, no lift, no shadow (Ive restraint).
  • Use data-domain attribute matching the work group title for filter integration.
  • Keep teaser to 2-3 sentences max. The card is a teaser, not the case study.
  • Show one quantitative metric in the foot (.t-mono-s.t-accent), the most compelling number from the case.
  • Use real project content from data, never lorem ipsum, never "Project name."
✗ Don't
  • Don't nest links inside the card. Inner elements (tags, metric) must not be interactive, that breaks screen-reader navigation.
  • Don't add a hover lift (translateY) or shadow. Border-color shift is the entire hover treatment.
  • Don't put more than 2 tags in the foot. The card visual gets noisy past two.
  • Don't use this card outside the work-listing context. Other listings have their own card variants (or should).
  • Don't override :focus-visible. The 2px accent outline + 4px offset is the keyboard signal.
Organism Stable

Hero-case

The opening composition of every case study page. Frames the project with breadcrumb wayfinding, headline + lede pair, project visual, and definition-list metadata (role / team / timeline / status).

.o-hero-case · /static/system/organisms.css
Usage15
Variants1
States3
A11yAAA ✓
Touchedv1.2
Bus factor1
Live

CRM & sales workflow redesign

Sales reps weren't avoiding the CRM because they were lazy. They were avoiding it because every deal update took 6 clicks through a form that wasn't designed for the way sales actually works.

Project visual
Role
Senior UX Designer
Team
Product, sales enablement, engineering, RevOps
Timeline
3 months
Status
Portfolio case study
Composition trail
Example case introduction

A clearer path through a complex task.

Describe the problem and your contribution before showing the detailed process.

  • Role: designer
  • Scope: one workflow
Live breadcrumb, typography and metadata components. This is sample copy, not a project outcome.
Variants
ClassUse whenLayout
.o-hero-caseStandard case-study openingBreadcrumb top, title + lede left, visual + meta right
, + .o-hero-case--narrow(future) reading-column layout for text-only casesSingle column, max-width 60ch
States
StateVisual / behaviorTrigger
defaultStatic composition; no hover state on the hero itselfInitial render
revealSingle 420ms fade per element block (breadcrumb, head, grid). 80ms initial delay; grid delayed +120ms.Page load (motion.css)
reduced-motionAll reveals collapse to opacity: 1 instantlyprefers-reduced-motion: reduce
Accessibility
  • Heading hierarchyh1 for the case-study title (only h1 on the page). Subsections use h2.
  • BreadcrumbWrapped in <nav aria-label="Breadcrumb">. Last item is plain text (current page), not a link.
  • Definition list<dl> with <dt>/<dd> pairs. Screen readers announce as "Role: Senior UX Designer; Team: Product, sales enablement, …"
  • Visual slotProject photo requires alt describing the work, not the medium ("dashboard showing role-based filters" not "screenshot of dashboard").
  • Color contrastTitle 18.5:1 (AAA). Lede 7.2:1. Meta dt: 4.6:1. Meta dd: 18.5:1.
  • Reduced motionReveal animations disabled, content is visible at opacity: 1 from the start.
Code
<section class="o-hero-case">
  <nav aria-label="Breadcrumb">
    <ol class="m-breadcrumb">
      <li><a href="/portfolio">Work</a></li>
      <li>Enterprise SaaS</li>
      <li>2024</li>
    </ol>
  </nav>
  <div class="o-hero-case-head">
    <h1 class="o-hero-case-title">…</h1>
    <p class="o-hero-case-lede">…</p>
  </div>
  <div class="o-hero-case-grid">
    <div class="o-hero-case-visual">
      <img src="…" alt="…">
    </div>
    <dl class="o-case-meta">
      <div><dt>Role</dt><dd>Senior UX Designer</dd></div>
      <div><dt>Team</dt><dd>…</dd></div>
      <div><dt>Timeline</dt><dd>3 months</dd></div>
      <div><dt>Status</dt><dd>Shipped</dd></div>
    </dl>
  </div>
</section>
Content states

How the hero behaves across data conditions. Hero is the page's opening, these states are unmissable.

StateWhat rendersTrigger
defaultBreadcrumb + title + lede + visual + meta. Reveal animation fires on page load (single 420ms fade per element block).Project fully populated
empty (no visual)Visual slot stays sized via aspect-ratio but renders empty fallback color. Meta block remains.Project has no HeroVisual
empty (no lede)Lede paragraph omitted; head reduces to title only. Spacing collapses gracefully.Project has no Teaser or HeroStatement
empty (no meta)Meta <dl> hidden. Visual takes full grid width.Project has no Role + Team + Timeline + Status
reduced-motionReveal animations collapse to opacity: 1 instantly. Layout unchanged.prefers-reduced-motion: reduce
narrow viewport2-col grid (visual + meta) stacks vertically below 880px. Visual takes full width; meta follows.Viewport < 880px
Slot contracts

What can go inside each hero slot. Hero is the page's first impression; slot violations show up immediately.

SlotRequiredAllowedForbidden
.m-breadcrumb2+ <li> ending with current pageAnchors and plain-text current itemButtons, dropdowns, more than 4 levels deep
.o-hero-case-title<h1> with case-study titleDisplay type via .t-display-l roleMultiple h1s, marketing taglines, branded text
.o-hero-case-lede<p> with 1–2 sentences naming the tensionPlain text + inline emphasisCTAs, lists, multiple paragraphs (lede is one tight paragraph)
.o-hero-case-visualProject hero image or video frame<img> with descriptive alt, <video> muted/looped, <svg> animationGeneric stock images, decorative gradients, links
.o-case-meta<dl> with 4 <dt>/<dd> pairs (Role / Team / Timeline / Status)Plain text in dd elementsMetrics, those live in the Impact Framing section, not meta
Do / Don't
✓ Do
  • Lead with the case-study title as <h1>. Only one h1 per page.
  • Wrap breadcrumb in <nav aria-label="Breadcrumb">. Make the last item plain text, it's the current page.
  • Write a lede that names the tension the case resolves ("Sales reps weren't avoiding the CRM because they were lazy, they were avoiding the friction"). 2 sentences max.
  • Use <dl> + <dt>/<dd> for project meta. Screen readers announce the pairs as a structured list.
  • Fill the visual slot with a real project image. alt describes the work, not the medium.
✗ Don't
  • Don't make the visual slot a generic "design hero" image. The hero is the case-study's evidence, not its mood.
  • Don't use the breadcrumb to link to ambiguous parents (e.g., "Selected work" instead of "Work"). Breadcrumb labels match the route.
  • Don't write a lede that just restates the title. Lede adds tension; title names the case.
  • Don't put metrics in the meta block. Metrics live in the Impact-framing section. Meta is role/team/timeline/status.
  • Don't add CTAs to the hero. Hero is for orientation; CTAs come at the closing.

Archetype card

F .o-card-archetype , archetype panel (home page work-group cards)

Process section

G .o-process , case study process section (label + body + bullets)
02 Generative research

Sales reps weren't avoiding the form. They were avoiding the cognitive load.

I ran 14 contextual interviews with reps across three deal-size segments. Two patterns emerged on the first three calls and held through the rest:

  • Reps mentally compose deal updates in real-time during calls, and 92% reported losing details by the time they reached the form.
  • Mobile usage was 3× higher than the product team estimated; the form was completely unusable on mobile despite "responsive" classification.
  • Activity logging was treated as evidence-gathering for management review, not pipeline accuracy, a structural misalignment.
03 Synthesis

Three workflow paradigms were colliding.

The CRM was modeled around database integrity: every field exists because some report needs it. Sales actually operates on conversational accumulation: information arrives in pieces, gets refined over time, and only crystallizes at deal close. Management wanted auditable evidence: timestamps, ownership, change history.

Outcome

H .o-outcome , outcome / reflection two-column (end of case study)
Outcome / impact
  • Deal update task time reduced 2.4× in 12-rep usability study.
  • Voluntary activity logging increased 31% over 6-week pilot.
  • Mobile sessions tripled after responsive redesign launched.
  • Sales operations adopted the activity-log model as canonical for Q4 reporting.
What I'd do differently
  • Run the mobile flow as a separate research thread, not a follow-up.
  • Pair-design the activity-log model with sales operations earlier, they had strong opinions we surfaced too late.
  • Build a more rigorous baseline measurement; the 6-week pilot lacked control comparison data.

Contact grid

I .o-contact-grid , contact cards (email, LinkedIn, location)
Email hi@petertak.com

Best for hiring conversations and project inquiries.

LinkedIn linkedin.com/in/petertak

Career history, recommendations, recent activity.

Location Seattle, WA

PST timezone. Open to remote, hybrid, or on-site within Pacific time.

Footer

J .o-footer , site footer (the actual one ships at the bottom of every live page)

See the canonical .o-footer rendered at the bottom of this page ↓

TL;DR

K .o-tldr , two-line summary near the top of every case study (eyebrow + lede)
TL;DR

Sales reps weren't avoiding the form, they were avoiding the cognitive load. Inline edit replaced a 6-step modal; deal-update task time dropped 2.4× and voluntary activity logging rose 31%.

Platform tag

L .o-platform-tag , small uppercase chips under the case hero (iOS / Android / Web platform list)
iOS Android Web Internal admin

Feature grid + card

M .o-feature-card · .o-feature-grid , side-by-side feature panels (about-page capabilities, landing sections)

Research-led

Every case starts with contextual interviews and behavior data. Generative research is the cheapest way to find the right problem.

System-minded

Components, tokens, and templates ship as one. Every product surface gets the same vocabulary.

Comparison

N .o-comparison , draggable before/after slider; range input drives a CSS clip-path on the after layer

A live demo lives in the Behavior-hooks section under data-comparison. The component renders two stacked panels (.o-comparison-before + .o-comparison-after), a draggable handle, and a hidden range input that updates --comparison-pos on the wrapper. Keyboard-accessible by default (slide with arrow keys).

Artifact grid + tile

O .o-artifact-grid · .o-artifact-tile , wireframe/artifact tiles in case studies (kind + title + body + decorative wire)
Wireframe

Inline edit pattern

Single-row deal update with field validation, optimistic write, and undo within 8s.

Flow diagram

Activity log model

Conversational accumulation across 3 stages, capture, refine, crystallize at deal close.

Media showcase

P .o-media-showcase · .o-media-placeholder · .o-media-real , case study media block; placeholder when no image yet, real when an img URL exists
Screen Inline-edit pipeline view (mobile)
Inline-edit replaces the 6-step modal; pipeline stays in view during edit.

Competitor table

Q .o-competitor-table , 4-column comparison (Product / Strengths / Gaps / Insight) inside case studies
ProductStrengthsGapsInsight
Salesforce Database integrity, audit history, field-level permissions Mobile, conversational input, real-time refinement Built for the auditor, not the seller
HubSpot Inline edits, mobile-first, gentle defaults Enterprise scale, complex permissions, audit trails Closer to the conversational model, proof inline-edit can ship

Principle list

R .o-principle-list · .o-principle , stack of guiding principles (about page “How I think about design”)

Research is the cheapest path to the right problem.

Five contextual interviews change a roadmap more than five months of solutioning. Every case study here started with field work, not a Figma file.

Systems compound. Snowflakes don't.

Each one-off pattern is a small future tax. Each canonical component is a small future dividend. The math always tilts toward the system.

S .o-persona-grid · .o-persona-card , stakeholder cards with JTBD framing in a compact head and goals/frustrations behind an accordion

Compact head shows avatar (.m-avatar--lg with optional --ink / --accent variant), name, role, the JTBD claim, and a "See goals & frustrations" disclosure with an animated underline + chevron. Click to expand the accordion (grid-template-rows: 0fr → 1fr, 240ms cubic-bezier(0.23, 1, 0.32, 1)). Auto-fit grid; the third card spans full width when alone in its row. Used by case studies via Layout: "personas" with the Persona struct (Name, Role, Initials, AvatarVariant, JobToBeDone, Goals, Frustrations).

The Store Rep

Retail associate, coordination layer by default

When juggling four channels at once, I need to see all active demand in one place, so I can transfer to a free colleague without losing the customer in front of me.

Goals
  • See all active demand in one view, not tab-switch between systems
  • Transfer a customer to a free colleague without losing their context
Frustrations
  • No way to see which teammates are free right now
  • Phone calls break focus mid-appointment, no good way to defer

The Waiting Customer

Retail customer, on hold, in line, or rebooked

When I walk in or call, I just need to know if someone can help today and where, without holding without an estimate or being sent to a store with no capacity.

Goals
  • Get served without waiting for a rep who is already at capacity
  • Get routed to an available rep or a nearby store without having to ask
Frustrations
  • Being told to wait when other reps are free but unaware
  • Being sent to another store with no confirmation that store has availability

Variants shown: .m-avatar--ink (default) and .m-avatar--accent (expanded). Third value .m-avatar--lg alone (no variant) renders as muted surface.

Comparable systems

If you know another system, here's the map.

This older vocabulary map is a starting point, not an equivalence test. A tag is not an interactive chip, and a visual match does not imply matching behavior. See the current Material, Carbon, Fluent and Apple HIG comparison for implemented changes and open gaps.

PlotMaterial (Google)Carbon (IBM)Polaris (Shopify)Fluent (Microsoft)
.button-primaryFilled buttonPrimary buttonPrimary actionPrimary command
.button-secondaryOutlined buttonSecondary buttonSecondary actionSecondary command
.button-accentTonal button,,Brand-accent variant
.button-ghostText buttonTertiary buttonPlain actionSubtle button
.button-iconIcon buttonIcon buttonIcon plain actionIcon button
.tagChip (input variant)TagTagBadge
.tag-accentSelected chipTag (color="cyan")Tag tone="success"Badge appearance="brand"
.input + .m-fieldText fieldText inputText fieldInput
.o-card-workCustom cardTileResource list itemCard
.o-toastSnackbarToast notificationToastToast
.o-dialogDialogModalModalDialog
--color-actionmd-sys-color-primary$button-primary-bgtone-magic surfacecolorBrandBackground

Closest match: Polaris (clear functional naming + token discipline). Furthest: Material (heavier interaction layer). Plot is closer to Carbon in spirit, atomic foundation + per-component documentation.

Roadmap notes

What's not in v1.x, and why.

Honest accounting of what Plot v1 doesn't do. Tells consumers what to expect and what to plan around.

Container queries, not yet

Plot's components currently respond to viewport width via media queries, not to their parent container width via @container queries. For a portfolio at this scale, no measured benefit yet. v2.0 will reconsider once components are reused in container contexts that differ from the viewport (e.g., a card grid inside a sidebar).

"Real matrix" of variant × size × state, not visualized

Each component doc shows variants, sizes, and states as separate tables. The real combinations are variants × sizes × states. For Button that's 5 × 3 × 6 = 90 cells. Most won't have unique visuals. v2.0 will surface the ~12 worth visualizing per component (e.g., "primary + default + hover/focus/active/disabled" + "icon + sm/lg + focus") explicitly. v1.x relies on the consumer reading three tables and composing mentally.

Form components, partial coverage

Plot 1.3 adds native textarea, select, checkbox and radio examples, plus local validation and recovery. Date-range pickers, file uploads and searchable comboboxes remain unimplemented. These new examples are not wired to a server or automatically applied to the contact form.

Theming, single brand

Plot is built for one brand (Peter's portfolio). Multi-brand theming is technically possible, override semantic tokens at :root level, but not validated. v2.0 may explore extracting Plot as a starter kit; until then, treat the amber accent as fixed.

Consolidation plan

v2.0 reductions, what's getting merged.

v1.x maintains backward compatibility. v2.0 will remove the redundant components below. Each row names the duplication, the v2 successor, and the migration path. Disclosed publicly so consumers can plan ahead.

Will be removed (v2.0)Replacement (already in v1)WhyMigration
.m-stat .m-metric + .m-metric--featured Both render "big number + small label." Difference was where they live, not what they are. Single component with a featured modifier covers both jobs. Replace .m-stat classes with .m-metric. No DOM change beyond class rename.
.m-cta-pair .cluster.cluster-3 utility The molecule's only job was "horizontal flex with gap." That's a layout primitive, not a molecule. Removing one molecule from the system. Replace .m-cta-pair with .cluster.cluster-3. Nested buttons stay unchanged.
.m-eyebrow-title Direct HTML, <header> with .t-eyebrow + .t-title-xl Six classes ("eyebrow", "num", "title", "modifier", etc.) for "two atoms in a stack." The structure belongs in HTML, not a class hierarchy. Replace molecule wrappers with native <header> + atom classes. Reduces ~80 lines of CSS.
.o-status-pill .tag.tag--pill.tag--accent.tag--live Both are "rounded amber-tinted label with text." The status-pill's only meaningful difference is the animated dot. Promote that to a tag modifier. Replace .o-status-pill wrapper with .tag + modifiers. Reuse animated dot via .tag-live-dot child.
.o-process · .o-outcome · .o-contact-grid Template-internal patterns (.case-study__process, etc.) Page-specific patterns labeled as "organisms" mislead consumers into reusing them off-context. Templates are the right tier for these. Rename selectors to .case-study__* namespace. No visual change. Documented in Templates section.
Net reduction at v2.0

5 redundant components removed. ~3 molecules and ~3 organisms net shrink. ~200 lines of CSS retired. Every consolidation has a documented migration. The system gets simpler, not larger.

Variant rationalization

Dan Mall: 5 button variants is at the upper limit. Pick a model, peers vs composable modifiers, and apply consistently. v2.0 reduces variant count by treating shape / color / size as composable modifiers instead of variant peers.

Button, 5 variants → 3 + 2 modifiers

v1.x (current)v2.0 (planned)Why
.button-primary.button-primaryMain CTA, kept as-is
.button-secondary.button-secondarySupporting action, kept
.button-icon.button-iconSquare / pill icon-only, kept (genuine third role)
.button-accent.button-primary.--accentDemoted from variant peer to modifier, primary CTA with amber emphasis
.button-ghost.button-secondary.--ghostDemoted, tertiary-emphasis secondary, not its own role

Tag, 5 variants → 1 base + 4 composable modifiers

v1.x (current)v2.0 (planned)Modifier role
.tag.tagBase, kept
.tag-accent.tag.--accentColor modifier (amber emphasis)
.tag-outline.tag.--outlineFill modifier (no fill, hairline border)
.tag-mono.tag.--monoFont modifier (Geist Mono for version/hash/ID)
.tag-pill.tag.--pillShape modifier (full radius)

Why this matters: in v1.x the doc says "use pill OR accent, not both." In v2.0 modifiers are composable, <span class="tag --pill --accent --mono"> just works. The system stops fighting itself.

Naming taxonomy unification

v1.x has 5 different variant-naming conventions across atoms. v2.0 picks one rule and applies it consistently:

Componentv1.xv2.0 rule
Buttonfunctional names (primary, accent, secondary, ghost, icon)Functional roles (primary, secondary, icon) + modifiers
Tagmixed (color/fill/font/shape names)Single base + composable modifiers
Herolocation names (--home, --page, --case)Kept, heroes are inherently location-bound
Toaststate names (default, action)Single base + behavior modifiers (--with-action)

Rule for v2.0: Variants = peers (genuinely different roles). Modifiers = composable (shape, color, size, behavior). When in doubt → modifier.

Education

Tutorial + FAQ.

A 5-minute walkthrough for first-time users, plus answers to questions that come up repeatedly.

Your first case study, 5-minute walkthrough

From "I want to add a new case study" to "live URL." Follow the five steps.

  1. 1

    Add the project to data.go

    Append a new Project{} struct in internal/site/data.go. Required fields: Slug, Title, Client, Year, Teaser, HeroStatement, Role, Timeline, Status, VisualKind, Tags, Capabilities. The site re-reads on Go restart.

  2. 2

    Write the narrative blocks

    Each case study has Sections []Section, a sequence of {ID, Eyebrow, Title, Layout, Body, Bullets}. The case study template (templates/case.gohtml) renders them through .o-process automatically.

  3. 3

    Add the project to a WorkGroup

    Append &projects[N] to the relevant group in the WorkGroups initializer (Enterprise SaaS / Fintech / Healthcare / Design Systems / Zero-to-One). The /portfolio index renders it automatically.

  4. 4

    Add metrics (or don't)

    Add SuccessMetrics []Metric entries with Value + Label. Placeholder values like ", fill in" are auto-hidden, no debug strings will leak to the rendered page.

  5. 5

    Run + verify

    go run ., the site rebuilds from the embedded files. Visit /portfolio/<slug> to see your new case study live. Total time: ~10 minutes from blank cursor to live URL.

Stuck? File an issue with the step number.

FAQ

Questions that come up repeatedly, documented once, here.

Can I use --bg or --ink in new code?

They work, but they're deprecated. Use --color-bg / --color-text-primary instead. The legacy aliases still cascade to the semantic tokens, but they'll be removed in v2.0. See Color section for the full migration table.

I need a button variant that doesn't exist. What do I do?

Don't add a fifth variant inline. Open an issue with the use case. We'll decide between (a) recomposing existing variants, usually the answer, or (b) adding a real new variant if there's a genuinely new role. Five variants is already at the upper limit Dan Mall recommends; six should be earned.

Why isn't this system on npm?

Plot is currently scoped to peter-tak-portfolio. Extracting it as a starter kit is on the v2.0 roadmap. The CSS layer is portable; the Go html/template markup layer would need adaptation for React/Vue/Astro. Email Peter if you want to fork it.

Can I override --color-action for a different brand?

Yes. Override --color-action, --color-action-hover, --color-action-text, and --color-action-focus at :root level. Component tokens read through the semantic layer, so a single override propagates to .button-accent, links, focus rings, and the highlight viz color simultaneously.

How do I report a bug or accessibility issue?

File a GitHub issue at peter-tak-portfolio/issues. For accessibility issues, use the a11y label so they get routed to the next audit cycle (cadence: every MINOR release, see Governance).

What's the difference between an atom, molecule, organism, and template?

Brad Frost's atomic-design framework, applied here:

  • Atom: the smallest reusable unit. A button, a tag, an input. Composes nothing.
  • Molecule: two or more atoms with a specific job. .m-tag-cluster = several tags grouped semantically.
  • Organism: a complete UI region usable across pages. .o-card-work renders the same way on /home and /portfolio.
  • Template: the page-level layout pattern. "Filtered index" describes the /portfolio page shape, atoms / molecules / organisms compose into the template.
Why is the system named "Plot"?

A portfolio is a sequence of plot points, case studies. The system plots a course through them (the career arc). It also visually plots data (charts, metrics, motion graphics). And in old usage, a "plot" is a bounded territory, Plot's bounds are this portfolio. Short, memorable, references the work.

Behavior hooks

Every data-* attribute the system binds.

JavaScript-bound and consumer-set attributes used by components for state, filtering, and behavior. Three categories: user-set state, JS-bound triggers, and internal state markers. Anything not on this list is ad-hoc and should be migrated.

User-set state attributes

Attributes the consumer sets in markup or template logic. The system reads them; it does not change them.

AttributeValuesEffectWhere
data-loading "true" | "false" Button shows inline spinner replacing leading icon; pair with aria-busy="true" for screen-reader announcement. .button
data-domain String, work group title Filter value consumed by the work-page filter row JS to show/hide cards matching the active chip. .o-card-work
data-filter String | "all" Chip filter target. "all" shows everything. Match string compared to data-domain on cards. .m-chip in .m-filter-row
data-work-group String, group title Marks a work-section as a filterable group. JS hides the whole section when no children match the active filter. section wrapping .o-card-work grid
data-theme "light" | "dark" Override OS-level color-scheme preference. Applied on <html>; persists in localStorage via the theme-toggle atom. html
data-icon "sun" | "moon" Marks each SVG purpose for the theme toggle. CSS shows/hides the matching icon based on current theme. svg inside .theme-toggle
aria-pressed "true" | "false" Active state for filter chips. Pair with .is-active class. Required for screen-reader state announcement. .m-chip

JS-bound triggers

Marker attributes the design-system JS scans for and binds behavior to. Add the attribute, get the behavior, no per-component init required.

AttributeValuesBehaviorBound toDemo
data-toggle-switch (no value) Click toggles aria-checked + .is-on class on the switch. .switch 14b ↓
data-tabs (no value) Binds keyboard navigation (← / → / Home / End) and tab-panel association via ARIA. .m-tabs 14d ↓
data-copy-button (no value) Click copies the preceding <pre><code> contents to the clipboard. Updates label to "Copied" for 1.4s. .m-code-snippet-copy 14h ↓
data-fire-toast (no value) | "action" Click fires a toast. "action" variant adds an action button. Any button 14i ↓
data-toast-close (no value) Click dismisses the parent toast (300ms exit transition). Inside .o-toast 14i ↓
data-toast-action (no value) Click fires the toast's action callback then dismisses the toast. Inside .o-toast 14i ↓
data-open-dialog String, dialog id Click opens the <dialog> with matching id. Uses native showModal(), focus trap and Esc-to-close are free. Any button 14j ↓
data-close-dialog (no value) Click closes the parent dialog. Equivalent to pressing Esc. Inside .o-dialog 14j ↓
data-confirm-ship (no value) Demo-only: marks a dialog confirm button to also fire a "Shipped" toast on click. Inside .o-dialog 14j ↓
data-comparison-input (no value) Binds the range input to a CSS custom property --comparison-pos on the parent .o-comparison. Drag updates the clip-path. input[type="range"] ↓
data-reveal (no value) Marks a section for scroll-triggered fade-in via IntersectionObserver. Adds data-reveal-init on JS load, then data-reveal-shown when the section enters the viewport. Any section ,

Internal state markers

Set by the system itself, not by the consumer. Listed here so a developer reading the rendered HTML can decode what's active.

AttributeValuesSet byEffect
data-state "enter" | "exit" Toast JS Drives the @starting-style entry / 300ms exit transitions
data-reveal-init (no value) Reveal observer (motion.css) Marks that JS has initialized the reveal observer for this element. Without it, content stays visible (no-JS graceful degrade)
data-reveal-shown (no value) Reveal observer (motion.css) Fires on viewport entry. Triggers the fade + translateY animation. Set once per element (not toggled on scroll-out).
data-error (no value) Login form server Set on the form when password validation failed. CSS shows the error message.
Utilities

Single-purpose helpers (.u-*).

For the patterns the system reaches for most often. Compose with block classes, never used as the primary styling layer for atoms / molecules / organisms (those have their own classes). Utility soup is an anti-pattern; utility surgery is the rule.

Spacing, margin-bottom / margin-top

Direct mapping to the spacing scale. The class suffix matches the token name: .u-mb-3 = margin-bottom: var(--s-3) = 12px.

ClassPropertyRange
.u-mb-1, .u-mb-9margin-bottomvar(--s-1) = 4px through var(--s-9) = 96px
.u-mt-1, .u-mt-8margin-topvar(--s-1) = 4px through var(--s-8) = 64px

Reading width caps

Typography reading rhythm. Pair with body copy and section heads, never on cards or grid items where the layout dictates width.

ClassValueUse when
.u-max-w-2222chDisplay headlines that should wrap to ~3 lines
.u-max-w-4040chHero ledes
.u-max-w-6060chBody copy, the comfortable reading column
.u-max-w-7272chWide reading column for callouts and tables
.u-max-w-narrowvar(--w-narrow) = 680pxLong-form prose inside case studies

Layout + text helpers

ClassPropertiesUse when
.u-col-stretchdisplay: flex; flex-direction: column; align-items: stretchOverride .example's row default for vertical stacks
.u-text-mutedcolor: var(--color-text-muted)Muted aside copy in long-form passages
.u-text-bodycolor: var(--color-text-body)Default body color override (rare)
Migration progress · v1.1

v1.0 had 191 inline style="…" attributes on this showcase page. v1.1 promotes the most-repeated patterns (margin-bottom, margin-top, max-width) into .u-* utilities and migrates the most common cases. Current count: ~113 remaining. Mixed-property inline styles (e.g., flex composition + padding + background) stay inline until the next batch of utilities ships in v1.2.

14, v1.1 additions

New components shipped in v1.1.

Atoms, molecules, and organisms added since v1.0, token-tier upgrade, kbd/code/switch/spinner/skeleton atoms, plus tooltip / tabs / section-divider / callout / empty-state / code-snippet molecules and toast / dialog organisms. These are documented provisionally below, full .ds-component-doc treatment lands in v1.2.

Documentation gap acknowledged

The components below use the v1.0 informal cluster-header style, not the .ds-component-doc framework that documents button, tag, input, link, card-work, hero-case, and hero (S2-S5, S9-S14 of this audit). Two patterns on one system page is the worst of both. Migration to the canonical framework is scheduled for v1.2 and tracked in the governance changelog. Each entry is marked Beta status to signal "production-ready visuals, provisional docs."

14a Beta Three-tier token model , Nathan Curtis fix: primitive → semantic → component
Tier model now enforced

v1.0 used a single tier. v1.1 adds --gray-* and --amber-* primitives, then semantic tokens like --color-text-primary and --color-action-bg. Legacy aliases (--ink, --accent) still work, they reference the new semantic layer. Components migrate at their own pace.

/* Tier 1, primitive */
--amber-500:    hsl(38 95% 50%);

/* Tier 2, semantic (references primitive) */
--color-action: var(--amber-500);

/* Tier 3, component (references semantic) */
.button-accent {
  background: var(--color-action);
}
14b Beta New atoms , kbd, code (inline + block), switch, spinner, skeleton
Press ⌘ K to open the command palette. Use ↑ ↓ to navigate.

Inline code: the --accent token is now an alias for --color-action, defined at hsl(38 95% 50%).

// Build status
version: "1.1.0",
atoms: 46,    // +5 since v1.0
molecules: 20, // +6 since v1.0
organisms: 15  // +3 since v1.0
Switch: Spinner:
Skeleton (loading state)
14c Beta .m-tooltip , scale-from-origin, 250ms delay first hover, instant on subsequent
hover for tooltip Defined at hsl(38 95% 50%) ⌘K to open Settings
14d Beta .m-tabs , tab list with active indicator, used for case study sections

Start with the task, then test the path with the people who use it.

Section divider

14e Beta .m-section-divider , hairline rule with mono label (long-page navigation)
02 · Generative Research

Callout

14f Beta .m-callout , informational note (default + accent variants)
Confidential project

This case study uses redacted and anonymized data per the original NDA. Specific metrics are within ±10% of actuals.

Featured case

This is the case I'd open with in a portfolio review, it shows research depth, technical fluency, and measurable outcome in one piece.

Empty state

14g Beta .m-empty-state , used when filters return zero results

No cases match this filter

Try a different domain or browse the full archive, most case studies cover multiple disciplines and may show up under several filters.

View all 17 cases
14h Beta .m-code-snippet , code block with language label + copy button
CSS
/* Custom easing curves per Emil Kowalski */
--ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
14i Beta .o-toast , transient notification with @starting-style entry
14j Beta .o-dialog , modal built on native <dialog> (free focus-trap + Esc)
Forms & feedback · 1.3

Let people choose, correct and continue.

Four native controls and three interaction patterns fill the most immediate gaps in Plot. They reuse the site's type, spacing, color and field styles. These are local examples, not a connected service. New patterns remain Beta until tested in a real consumer workflow.

AtomBeta

Checkbox

Choose several independent options. For consent, say exactly what is being accepted and leave the choice unchecked.

.plot-choice + input[type="checkbox"]

Live anatomy

Include in this example review
  1. 1 Group legend
  2. 2 Native checkbox
  3. 3 Clickable label

Tab reaches each enabled choice. Space toggles it. The disabled example is unavailable, not selected. For a single mutually exclusive choice, use radio buttons.

Copy the structure
<fieldset>
  <legend>Include in review</legend>
  <label class="plot-choice">
    <input type="checkbox" name="review" value="requirements">Requirements
  </label>
</fieldset>
AtomBeta

Radio group

Choose one option from a short, visible set. Keep all options in the same named group.

.plot-choice + input[type="radio"]
Example review format
  1. 1 Shared question
  2. 2 One checked value
  3. 3 Plain-language option

Tab enters the group. Arrow keys move between enabled options using native browser behavior. Selecting one option clears the other.

Copy the structure
<fieldset>
  <legend>Review format</legend>
  <label class="plot-choice"><input type="radio" name="format" value="guided" checked>Guided</label>
  <label class="plot-choice"><input type="radio" name="format" value="independent">Independent</label>
</fieldset>
AtomBeta

Select

Choose one item from a bounded list when showing every option would take too much room. Use radio buttons when comparing a few choices is the main task.

.m-field + .plot-select

This changes the selection only. It does not navigate or submit.

  1. 1 External label
  2. 2 Native option list
  3. 3 Associated help

Keyboard and touch selection follow the browser and operating system. A searchable multi-select needs a separate combobox implementation; this control is not one.

Copy the structure
<label class="m-field-label" for="role">Role</label>
<select class="plot-select" id="role" name="role">
  <option value="">Choose a role</option>
  <option value="designer">Designer</option>
</select>
AtomBeta

Textarea

Collect a longer explanation, comment or recovery note. Keep the label visible, allow vertical resizing, and explain any length limit before someone types.

.m-field + .plot-textarea

Up to 500 characters. Example only; avoid private information. Nothing is submitted.

  1. 1 Question
  2. 2 Resizable text area
  3. 3 Limit and privacy context
Copy the structure
<label class="m-field-label" for="notes">Review notes</label>
<textarea class="plot-textarea" id="notes" name="notes"
  rows="4" maxlength="500" aria-describedby="notes-help"></textarea>
<p id="notes-help">Up to 500 characters.</p>
PatternBeta

Validation & recovery

Explain what went wrong next to the field, preserve the answer, and offer a direct way to correct it. A color change or disappearing toast is not enough.

.m-field + .input + .plot-notice + .plot-field-error

Try checking an empty answer, then enter at least 3 characters. This local example makes no network request.

Name one task, such as “Customer intake”.

  1. 1 Focused error summary
  2. 2 Label and preserved input
  3. 3 Field-specific correction
  4. 4 Persistent outcome

The first check validates the answer. After that, editing updates the error without moving focus. Production forms also need server validation, authorization, duplicate-submit protection and a recoverable network-error state.

Implementation contract
Invalid: aria-invalid="true" + associated error text.
Submit: prevent duplicate requests; focus a summary of errors.
Edit: retain input; clear only errors that are resolved.
Success: announce actual success, not merely a button click.
Network error: retain input and provide an explicit retry.
PatternBeta

Progress & loading

Distinguish ready, loading, complete and failed states. Use a measured progress value only when the amount of completed work is known.

progress.plot-progress + .skeleton + role="status"

State simulator, not a live operation. Change the state to inspect its presentation.

Ready. No operation is running.

  1. 1 Explicit state
  2. 2 Indeterminate or measured progress
  3. 3 Text outcome and recovery

Decorative skeletons are hidden from assistive technology. Text still explains the state when motion is reduced. Real content containers should use aria-busy while updating; keep the status announcement outside that busy region.

Copy the structure
<!-- No value means the amount of remaining work is unknown. -->
<progress class="plot-progress" aria-label="Loading results"></progress>
<p role="status">Loading results…</p>
<!-- Use an actual measured value for determinate progress. -->
<progress class="plot-progress" value="2" max="3" aria-label="2 of 3 files loaded"></progress>
PatternBeta

Disclosure

Keep secondary detail available without making it compete with the current task. Keep required instructions and unresolved errors outside collapsed content.

details.plot-disclosure + summary
What happens if evidence is missing?

The reviewer records the missing evidence and the next action. A complete-looking interface is not a substitute for a tested workflow.

Can more than one section stay open?

Yes. These independent disclosures allow comparison without closing the other answer.

  1. 1 Descriptive summary
  2. 2 Native expansion marker
  3. 3 In-flow detail

Enter or Space toggles the focused summary. No focus trap and no keyboard-triggered animation. Native HTML continues working without JavaScript.

Copy the structure
<details class="plot-disclosure">
  <summary>What happens next?</summary>
  <p>The explanation stays in the document flow.</p>
</details>
15, Templates

Page-level layout patterns.

Five documented page patterns combine the same building blocks in different ways. Each entry includes a full-page drawing, its component composition and routes that use it. A new route can adapt a pattern without introducing a separate visual system.

Template Stable

Identity hero page

The home page. Split-layout opening (identity + motion graphic) followed by domain navigation, proof, and a closing CTA. Used once, for the landing page only.

/ · templates/home.gohtml
Layout
Live composition · IdentityReal Plot components, sample content
Independent designer

Make complex work easier to understand.

A short introduction, a clear next step, and work that shows the thinking.

Featured practice
Systems and interfaces

Shared patterns connect individual screens into a coherent experience.

Product design

Tasks, flows and decisions.

Design systems

Components and their behavior.

Composition and responsive behavior

Uses .m-hero-lede, .m-cta-pair, .button, .o-card-archetype and .o-feature-grid. The sample stacks at a narrow container width. The production hero also has its own motion slot.

Composes
  • Hero.o-hero--home with status pill, name, lede, CTA pair, meta chips, and motion graphic
  • Sections.o-section with --bordered, --compact, --breathe modifiers
  • Cards.o-card-archetype for domain navigation
  • Stats.m-stat for proof strip
  • CTA.o-cta + .m-cta-pair
Used on

/, the home page (one instance only)

Template Stable

Filtered index

A directory page with a hero, filter chips, and grouped item grids. The hero opens; the filter row narrows; numbered group sections render the items. Used for the work archive.

/portfolio · templates/work.gohtml
Layout
Live composition · Filtered indexTry the filters
Sample directory

Find a project by its focus.

2 example projects shown

Product
An easier intake flow

Example card describing one focused task.

Systems
A shared component library

Example card describing reusable patterns.

Composition and interaction

Uses .m-filter-row, .m-chip, .tag and .o-card-archetype. This isolated demo filters two sample cards, keeps focus on the active filter and announces the result count. The production index uses linked work cards.

Composes
  • Hero.o-hero--page, Work eyebrow, page title, lede
  • Filter.m-filter-row with .m-chip[data-filter], see Behavior hooks for filter binding
  • Group section.o-section--bordered + .m-eyebrow-title--numbered + .ds-grid.ds-grid-2
  • Cards.o-card-work with data-domain attribute consumed by the filter JS
Used on

/portfolio, five group sections, 15 cards total

Template Stable

Case-study reading

A long-form case study. Opens with breadcrumb + title + lede + visual + meta. Body is a sequence of named narrative blocks (context, research, design, system, validation, outcome). Closes with impact framing, artifacts, and next-case navigation.

/portfolio/<slug> · templates/case.gohtml
Layout
Live composition · Case studySample narrative, not a research result
Example case

Make the next step clear.

Show the problem, the decision and the evidence behind the change.

Role: designerScope: one workflow
The problem

A polished screen can hide an undefined recovery path. Start with the task and its failure conditions.

Evidence to show
  • The original workflow
  • The proposed change
  • The test and its limits
Outcome

Report measured results here only when the evidence exists.

Composition and reading order

Breadcrumb, hero text, metadata, narrative and evidence, then outcome. Uses .tag, .m-hero-lede, .o-feature-card and .m-callout. Stacked reading order preserves the narrative on small screens.

Composes
  • Reading progress.o-reading-progress, fixed-top amber bar, JS-driven via --reading-progress custom property
  • Hero.o-hero--case, full case-study hero with breadcrumb, head, grid, meta
  • Narrative blocks.o-process, repeated for each named section (context, research, design, system, validation, outcome)
  • Impact framing.m-metric grid with the case's measured outcomes
  • Artifacts.o-artifact-grid with key research / design / spec artifacts
  • Closing.m-link-card for next-case navigation, .o-cta for contact
Used on

/portfolio/<slug>, 15 case-study pages

Template Stable

Utility page

Light-content pages without a filter row or narrative arc. Hero opens, content section delivers the message, closing CTA invites action. Used for About, Contact, and the 404.

/about · /contact · /404
Layout
Live composition · Utility pageOne task, one reading column
Get in touch

Start with the question.

A short introduction gives the visitor enough context to choose a next step.

A local field demonstration. This text is not included in the contact link.

Composition and boundaries

Uses .m-field, .input, .m-cta-pair and shared type roles. The field is a specimen, not a submission form. Avoid a sidebar when this short page does not need one.

Composes
  • Hero.o-hero--page, generic page-hero variant
  • Sections.o-section--bordered with .m-eyebrow-title--numbered
  • About body.m-career-entry × N + .o-feature-grid for capabilities
  • Contact body.o-contact-grid + .m-field form fields
  • 404 body.m-empty-state + .m-cta-pair
Used on

/about, /contact, /404

Template Beta

System showcase

A documentation layout with persistent contents, a bounded reading column and individual reference sections. This page uses a jump-to palette; the H.A.R.D. Protocol Library adds criterion search and grouped setup instructions.

/design-system · templates/design-system.gohtml
Layout
Live composition · DocumentationNavigation beside a focused example
Component reference

A primary action

Keep the label specific to what happens next.

When should I use it?

Use one primary action for the current task. Secondary actions should remain available without competing for attention.

<button class="button button-primary">
Composition and navigation

Uses native navigation, .button, .example and .plot-disclosure. The page's full search remains in the left rail or the mobile jump-to palette. This small specimen does not introduce another search box.

Composes
  • Section structure.ds-section with .ds-section-head, own scaffolding, distinct from .o-section
  • Doc framework.ds-component-doc for every documented atom / molecule / organism / template (S2 framework)
  • Tier visualization.ds-chain, .ds-token-list, .ds-composition, .ds-token-table
  • Examples.ds-tier, .ds-tier-badge, .swatch, .ds-grid
Adaptation and evidence

The design-system page and H.A.R.D. Protocol Library share the contents-and-reference scaffold. Their controls differ by task. Reuse is not evidence of usability validation; the documentation pattern remains provisional.

Used on

Design system and H.A.R.D. Protocol Library (with criterion search).

Interaction proof

Interaction shipped, not specified.

A comparison slider built with a native range input, a CSS custom property and clip-path. A small input handler updates the split. Try dragging or using the arrow keys.

Before, v1.0 spec
After, v1.1 ships
Before After
Native control, a small JavaScript handler

A range input updates a CSS custom property --comparison-pos. The "after" panel uses clip-path: inset(0 0 0 var(--comparison-pos)). The handle uses the same custom property to position. Drag, click, or use arrow keys after focus.

Governance

Versioning, component status, audit cadence.

A design system is a product that serves other products. It needs the same rigor: a versioning policy, an explicit status for every component, audit cadence, and a public changelog. Below is the contract.

Versioning policy, semver

The system follows semantic versioning. The current site documentation and additive component release is 1.3.0. Existing classes and tokens remain compatible. This is not a separately published package. Three rules govern what each digit means:

BumpTriggered byExamples
MAJOR (2.0) Breaking visual or API change. Renaming a class, removing a token, changing a component's anatomy in a way that breaks consumers. Removing legacy aliases (--bg, --ink); renaming .button-accent
MINOR (1.x) New components, new variants, new tokens. Existing API stays compatible. Adding .button-icon variant; introducing --card-* tokens
PATCH (1.1.x) Bug fixes, polish, accessibility patches. No API change. Fixing focus-ring contrast; adjusting hairline opacity; correcting alt-text guidance

Component status legend

Every documented component carries a status badge in its .ds-component-doc header. The badge tells consumers whether they can build with confidence or should expect change.

StatusMeaningProduction use
Stable API frozen. Breaking changes require a MAJOR release with a deprecation period. Yes, build with confidence
Beta Production-ready but the API may shift in MINOR releases. Watch the changelog. Yes, with awareness
Experimental Use at your own risk. May be removed without notice. Not part of the contract. Prototype only
Deprecated Slated for removal in the next MAJOR release. Migrate to the named replacement. Migrate ASAP

Audit cadence

Audits keep the system honest. Each audit type has a fixed cadence and a defined scope.

Audit typeCadenceScopeLast run
Accessibility Every MINOR release WCAG 2.1 AA compliance: contrast, keyboard nav, ARIA, reduced-motion, screen-reader announcements per documented component v1.1, 2026-05-03
Browser support Quarterly Modern evergreen + Safari 17+. Verifies @starting-style, :has(), view-transition fallbacks degrade cleanly. v1.1, 2026-05-03
Token integrity Every MINOR release No component reads past Tier 2 (semantic). Component CSS uses Tier 3 tokens or escalates the request. v1.1, 2026-05-03
Performance Quarterly CSS bundle size, paint cost, animation frame budget. Targets: CSS < 60kb gzipped, hero LCP < 1.8s on 3G Fast. ,
Adoption Monthly Where each component is used across the site. Components with zero usage become removal candidates in next MAJOR. v1.1, 2026-05-03

Adoption · v1.0 → v1.2 trend

Where each canonical component renders across the live site, with growth measured release-over-release. A component with zero usage and no upward trend is a deletion candidate at next MAJOR.

ComponentStatusv1.0v1.1v1.2TrendGoal
.buttonStable122228↑ +133%30 by v1.3
.tagStable457285↑ +89%100 by v1.3
.inputStable244→ stable12 (every form field)
.link familyStable263440↑ +54%50 by v1.3
.o-card-workStable111515→ stable15 (matches case count)
.o-card-archetypeStable555→ stable5 (one per domain)
.o-hero (unified, S9)Stable,2022↑ new22 (matches route count)
.o-toastExperimental,11→ showcase onlyPromote to Beta when used in production
.o-dialogExperimental,11→ showcase onlyPromote when first production use
legacy aliases (14 tokens)Deprecated~210~150~100↓ −52%0 by v2.0

Contribution model

How a new component, variant, or token gets into Plot. The bus factor is currently 1; documenting the model is the first step toward 2+.

StepActionOwner
1. ProposeOpen an issue with use case + proposed API + acceptance criteria. Include "what existing component is closest, and why isn't it sufficient?"Anyone
2. TriageDecide: "Add new" / "Recompose existing" / "Reject, too project-specific." Most proposals end up at recompose.Steward (Peter)
3. SpecDraft the .ds-component-doc sections, anatomy, variants, states, a11y, tokens, code, do/don't, before any CSS.Proposer + Steward
4. BuildImplement CSS + HTML in the relevant tier file. Wire up Tier 3 component tokens. Document the new entry in the changelog.Proposer
5. ShipMINOR release. Status: Beta for one release. Promote to Stable in the next MINOR if no production issues.Steward
Bus factor disclosure

Current bus factor: 1. Single steward (Peter). Target: 2 (designer + engineer pair, the SuperFriendly "System DM" model). Documentation completeness audit before next MAJOR is the path there. File a contribution issue to get involved.

Success criteria for v1.x

Plot v1 is successful when these are all true. Reaching all four triggers v2 planning.

CriterionStatus (v1.2)
100% of canonical components reach Stable status (no Beta on Atom or Organism tier)⚠ 9 of 12 Stable (Hero family + 3 Beta)
Every page on the live site uses canonical components, zero raw <button> with custom styles✓ Achieved at v1.1
Time-to-implement: a new case study renders in < 10 min from data file → live URL✓ ~10 min measured
Bus factor: docs sufficient for a new contributor unfamiliar with Peter to ship a component without his help⚠ Aspirational, needs validation by an external trial

Time-to-implement

Consumer estimates for the most common paths. Numbers are rough but measured against actual workflow on this codebase.

TaskEstimate
Use existing button on a new page~30 seconds
Add a new tag with existing variant~30 seconds
Customize a hero variant on a new page~5 min
Add a new case study (existing template)~10 min (data file + run server)
Add a new component variant (e.g., new button modifier)~30 min (CSS + doc + token)
Propose a new component (issue → spec → build → ship)~2 hr proposal + 4–8 hr implementation
Onboard a new contributor (read this page + run locally)~1 day

Risk register

Public list of known risks with mitigation strategy. Listing risks publicly is more trust-building than pretending none exist.

SeverityRiskMitigation
HIGHBus factor 1, single steward. If Peter is unavailable, contributions stall.Documentation completeness audit before next MAJOR. Goal: bus factor 2 by v2.0.
MEDIUMv2.0 will remove 14 legacy alias tokens. Existing consumers using --bg, --ink, --accent will break.Deprecation table published in Section 01. 6-month minimum deprecation period. Migration guide planned for v1.3.
MEDIUMAeonik is licensed per-user with a 2027 renewal. License expiry would force a fallback.Fallback stack defined in --font-display. Site degrades to Helvetica Neue / Arial. No layout breakage on fallback verified at v1.1.
LOWToast and Dialog organisms are Beta, only used in /design-system showcase. Production behavior unverified.Promote to Stable only after first production usage with at least one consumer-facing test path.
LOWSystem is scoped to Go html/template + vanilla JS. Framework-agnosticism unverified for React/Vue consumers.CSS layer is portable. Templates would need adaptation. Disclosed in Scope below.

Scope

Honest disclosure: Plot is purpose-built for this portfolio, not a multi-tenant component library.

Scope: peter-tak-portfolio

Plot is purpose-built for this portfolio. It is not currently packaged for external use (no npm, no CDN). The CSS layer is framework-agnostic (vanilla CSS + custom properties), but the markup layer consumes Go html/template.

Extraction path: tokens, atoms, molecules, organisms are portable. Templates would need adaptation for React / Vue / Astro / etc. Fork the repo if you want a starter kit, happy to discuss.

Internationalization: English (en-US) primary. Korean (ko) supported via font-family fallback (Aeonik covers Hangul; Geist Mono falls back to system mono for code blocks). RTL not tested.

Changelog

September 25, 2026: media example containment

Vertical example frames now use a single non-wrapping column. The Media showcase stays inside its frame without overlapping the next component. Placeholder aspect ratios apply to the visual area; captions keep their own content height on narrow screens. Horizontal examples still wrap. No content is clipped and no fixed height is imposed.

Plot 1.3.0 · September 25, 2026

Added contextual search in the left rail and command palette, seven form and feedback entries, five live template compositions, and a source-linked coverage review. Anatomy now uses live components. Search focus no longer stacks outlines and shadows; the compact shortcut keeps a separate click target.

Fixed the tab example's missing panels and keyboard navigation, labeled the switch examples, and reconciled the current version display. Wide reference tables now have their own keyboard-accessible scroll regions. The mobile search button uses contrasting theme colors. Existing classes, routes and semantic tokens are preserved. No consumer page is automatically migrated.

New patterns are Beta. This release does not establish user validation or whole-site accessibility conformance. The older v2 consolidation section is a proposal, not the current version.

Read the comparison and open gaps · Release metadata

September 24, 2026: navigation, drawings and theme correction

Individual molecule, organism and template links; eleven native SVG anatomy/layout drawings; shared theme corrections; and a focus-contained component search. The HARD pages reuse the same components.

Selected layout, keyboard and contrast checks do not establish whole-site accessibility conformance or user validation.

Read the website update log

Public history of every release. New entries land at the top. Each entry follows added · changed · deprecated · removed · fixed · accessibility.

Archived v1.2 plan

Refactor .o-hero-home, .o-hero-page, .o-hero-case into one .o-hero with modifiers (S9 of this audit). Introduce template-tier documentation (S10). Add real Storybook-style prop dialogs per component.

v1.1, 2026-05-04 (historical)

Added · Tier 3 component tokens (--button-*, --card-*, --input-*, --tag-*) · .ds-component-doc documentation framework · per-component anatomy / variants / states / a11y / token-chain docs for button, tag, input, link, card-work, hero-case · Do/Don't usage rules for button, tag, hero-case · governance section.

Changed · Token documentation reorganized: semantic tier is now canonical; primitives moved into a collapsed <details> reference; legacy aliases moved into a deprecation table with explicit migration targets · .button rewired to consume Tier 3 tokens · numbered eyebrows demoted from --accent-text to --muted per Ive critique · hairline opacity scale collapsed from 6 values to 3 (0.06 / 0.18 / 0.32).

Deprecated · 14 legacy alias tokens (--bg, --surface, --ink, --hairline, --accent, etc.), slated for removal in v2.0. Migration table available in Section 01.

Fixed · case-study hero reveal cascade replaced with single 420ms fade per element block · case-study metric placeholders (", fill in") now hidden from view; real measured values populate the impact-framing section · self-initiated category and badge removed from the work index.

Accessibility · documented per-variant contrast ratios, screen-reader announcements, keyboard interactions, and reduced-motion behavior for every component in v1.1. WCAG 2.1 AA verified across the canonical 6.

v1.0, 2026-05-02

Added · Initial release: tokens, reset, atoms (41), molecules (14), organisms (12). Light + dark mode parity. Aeonik + Geist Mono. Two-tier amber accent (--accent surface vs --accent-text AA-compliant). Custom Emil Kowalski easing curves.

Notes · v1.0 documentation showed components in isolation without anatomy, variants matrix, states matrix, a11y receipts, or token chains. v1.1 audit (Sessions 1–6) addresses these gaps.

Coverage review · September 25, 2026

What Plot covers, and what it still needs.

A focused comparison against four official systems. Their platform scope and scale differ. This review identifies useful patterns for this website; it does not claim component parity, affiliation or a complete accessibility audit.

Official references and the changes they informed
ReferenceUseful emphasisPlot before 1.3This release
Google Material 3Components grouped by purpose, including selection, communication and input.Editorial components were easier to find than task controls. No documented native selection suite or progress element.Checkbox, radio, select, textarea and a state-based progress example.
IBM CarbonSearch scope, anatomy and clear behavior. Its form guidance connects labels, helper text and actions.Jump-to search matched titles and categories only. Error styles lacked a complete correction example.Contextual sidebar search, clear and no-result states, and an input-preserving validation flow.
Microsoft Fluent 2Distinct field, message, disclosure and progress building blocks.Existing tabs changed selection styling without associated panels. Loading primitives existed but lacked a state walkthrough.Linked tab panels with keyboard navigation, native disclosures and loading/success/error inspection.
Apple HIGAdaptable text, familiar interaction, multiple ways to perceive status, and contrast in both appearances.Theme tokens existed. New examples still needed labels, touch targets and explicit focus and motion rules.Native controls, text-based status, visible focus, 44px form targets and reduced-motion treatment. Device and assistive-technology testing remains necessary.

Open gaps, in priority order

PriorityGapWhat is needed before adding it
NextProduction forms and async error recoveryConnect to an actual task, validate on the server, test retry and duplicate submissions, and record assistive-technology results.
NextSearchable combobox, upload and date rangeA real consumer need and tested keyboard, invalid, empty and loading states. Native select is not a substitute for a combobox.
NextComponent-level test and source catalogExecutable stories, visual regressions, public test receipts and consumer references for each component, not only page-level checks.
When neededData table sorting, pagination, menus, trees, drawersTask and dataset requirements, row/selection behavior and focus restoration before building a larger application suite.
When neededLocalization, RTL and multi-brand themesTranslated content, directionality tests and theme consumers. Korean search aliases are not full localization.

Six component specimens and five template compositions use real Plot classes and native controls in place of the earlier box drawings. The Work card shares its template with the production Work page and uses public research content for the specimen. The smaller template compositions illustrate behavior rather than duplicating entire routes.

Interaction implementation used Emil Kowalski's emil-design-eng skill. Search and keyboard navigation respond immediately. Pointer feedback is brief, and reduced-motion preferences remove movement.

    ↑↓ navigate · Enter jump · Esc close ⌘K

    Hire Peter Tak

    This dialog is built on the native <dialog> element. You get focus-trap, scroll-lock, and Escape-to-close for free, plus @starting-style for the entry animation. Press Esc to close, or click the backdrop.

    Confirm: ship v1.1?

    This will publish v1.1 of the design system. Live pages will continue using v1.0 styles until Session 5 migrates them. Backwards-compatible, no downstream breakage.