Experienced gamers planning skill tree builds for Run! Goddess. They use the app in two modes: quick mid-game checks (speed and glanceability) and deliberate pre-game planning sessions (depth and comparison). They expect expertise-level tooling — no hand-holding, no tutorials they can't skip, no dumbed-down interfaces.
Minimal, Playful, Sharp — The foundation is a developer-tool level of efficiency and respect for the user's time (Linear, Raycast energy). But the surface layer should be fun — gaming-aware, with wit and juice. It knows the domain intimately and doesn't take itself too seriously. Think: a speedrunner's favorite tool.
- Confidence & control — "I know exactly what my build does." Clarity, mastery, zero ambiguity.
- Flow & satisfaction — "This is enjoyable to plan with." Smooth interactions, rewarding feedback loops, satisfying micro-moments.
- Trust & efficiency — "This tool just works." Fast, reliable, no friction.
- Visual tone: Clean utility with playful energy. The skeleton is minimal and purposeful (content over chrome), but interactions should feel alive — snappy transitions, satisfying state changes, moments of delight that reward engagement.
- Theme: OKLCH-based dynamic theming from a single hue+chroma source. Dark mode default. Six presets (Sky, Cyan, Green, Rose, Amber, Neutral) plus custom picker.
- References: Mobalytics, PoE Ninja, D2 Armor Picker (utility focus); Linear, Raycast, Arc (quality bar and interaction polish).
- Anti-references: Generic gaming dashboards, cluttered portals, template aesthetics, AI-generated sameness.
- Content over chrome — The skill tree IS the interface. Everything else serves it.
- Juice without jank — Add energy and delight through motion, feedback, and micro-interactions — but never at the cost of performance or clarity. Every animation should feel purposeful.
- Respect the player — Assume expertise. Surface power features, don't hide them behind progressive disclosure. Keyboard shortcuts, gesture systems, and compact displays are features, not complexity.
- Accessible by default — Colorblind palettes, reduced motion, keyboard navigation, screen readers. Accessibility is non-negotiable and already first-class.
- Earned playfulness — Delight comes from polish, not decoration. A perfectly-timed spring animation > a gratuitous particle effect. The tool earns its personality through craft.
- Svelte 5 + TypeScript, hand-built CSS (no component libraries)
- OKLCH color engine generating 100+ CSS variables from source hue/chroma
- Phosphor icons, Inter font, vanilla CSS custom properties
- Client-only PWA, no backend
All design tokens are CSS custom properties on :root. The dynamic color tokens are generated at runtime by themeEngine.ts from a source {h, c} pair; the static layout/typography tokens live in theme.css.
Spacing — Tight 4-point scale used for padding, gaps, and margins.
| Token | Value | Typical use |
|---|---|---|
--spacing-xs |
2px | Hairline gaps, kbd badge padding |
--spacing-sm |
4px | Inner padding, compact gaps |
--spacing-md |
8px | Standard gap between sibling elements |
--spacing-lg |
12px | Section padding, modal padding, outer gaps |
Typography — Whole-pixel rem values (at 16px root) to avoid sub-pixel rendering.
| Token | Value | Typical use |
|---|---|---|
--font-xxs |
0.625rem (10px) | Micro labels |
--font-xs |
0.6875rem (11px) | Keyboard shortcut badges, small captions |
--font-sm |
0.8125rem (13px) | Section titles, descriptions, secondary text |
--font-base |
0.875rem (14px) | Body text, button labels, toast messages |
--font-lg |
1.125rem (18px) | Modal headings |
--font-xl |
1.375rem (22px) | Page-level headings |
--weight-semibold |
600 | Section titles, toast messages |
--weight-bold |
bold | Emphasis, amounts |
--leading-none |
1 | Button labels, single-line elements |
--leading |
1.3 | Body text, descriptions |
--tracking |
0.02em | Default letter spacing |
--tracking-wide |
0.06em | Uppercase section titles |
Border radius — Three tiers plus a pill.
| Token | Value | Use |
|---|---|---|
--radius-sm |
6px | Toasts, kbd badges, inner elements |
--radius |
12px | Buttons, modals, cards, accordions, toggles |
--radius-lg |
19px | HUD pill displays (half of height) |
--radius-full |
999px | Circular/pill shapes, scrollbar thumbs |
Backdrop blur
| Token | Value |
|---|---|
--blur-xs |
2px |
--blur-sm |
4px |
--blur-md |
8px (modal backdrops) |
--blur-lg |
12px |
Transitions — Named easing curves. Components use these for all motion.
| Token | Value | Use |
|---|---|---|
--ease |
0.2s ease | Default hover/active transitions |
--ease-emphasis |
0.3s cubic-bezier(0.16, 1, 0.3, 1) | Expand/collapse, spring-like |
--ease-decel |
0.2s cubic-bezier(0.05, 0.7, 0.1, 1) | Entry animations (toast, fab) |
--ease-accel |
0.15s cubic-bezier(0.3, 0, 0.8, 0.15) | Exit animations |
--ease-standard |
0.2s cubic-bezier(0.2, 0, 0, 1) | Arrow rotations, general motion |
Other
| Token | Value |
|---|---|
--border-width |
1px |
--opacity-disabled |
0.5 |
Generated at runtime from a source ThemeSource { h, c, l? }. All colors are computed in OKLCH and emitted as hex values. Dark and light mode produce different lightness values from the same hue+chroma.
Neutral surfaces — Tinted with the source hue at reduced chroma.
| Token | Dark L | Light L | Use |
|---|---|---|---|
--bg |
0.10 | 0.97 | Page background |
--bg-panel |
0.14 | 0.94 | Side menu, modal backgrounds |
--bg-input |
0.18 | 0.91 | Input fields, header areas |
--surface |
0.22 | 0.88 | Accordion body, elevated content |
--bg-raised |
0.28 | 0.84 | Buttons, toasts, card backgrounds |
--bg-modal |
0.24 | 0.86 | Modal shell background |
--border-subtle |
0.34 | 0.78 | Low-emphasis borders |
--border |
0.42 | 0.68 | Standard borders |
Text
| Token | Dark L | Light L | Use |
|---|---|---|---|
--text |
0.93 | 0.18 | Primary content |
--text-muted |
0.78 | 0.38 | Secondary labels, descriptions |
--text-disabled |
0.55 | 0.55 | Disabled controls, subtle hints |
Accent — Full source chroma, the most vibrant color in the palette.
| Token | Use |
|---|---|
--accent |
Primary accent: active states, progress fills, selected indicators |
--accent-light |
Brighter variant: gradient endpoints, open-state arrow icons |
--border-focus |
Focus rings on interactive elements |
Tinted containers — Accent-colored surface/text pair for highlighted regions.
| Token | Use |
|---|---|
--bg-tinted |
Accent-tinted background (keyboard shortcut flash, selected chips) |
--text-on-tinted |
Text color readable against --bg-tinted |
Semantic: Danger — Warm red/orange, lightly harmonized toward theme hue.
| Token | Use |
|---|---|
--accent-danger |
Danger accent (destructive button text, error icons) |
--danger-bg |
Danger container background |
--danger-border |
Danger container border |
--danger-text |
Text on danger containers |
Semantic: Success — Theme hue at boosted chroma (neutral themes use blue).
| Token | Use |
|---|---|
--accent-success |
Success accent |
--success-bg |
Success container background |
--success-border |
Success container border |
--success-text |
Text on success containers |
Region palettes — Fixed-hue palettes for the three skill tree regions. Not harmonized to theme.
Each region (orange, yellow, blue) gets:
--region-{name}-accent— Region accent color--region-{name}-light— Brighter variant--region-{name}-bg-available/bg-active/bg-maxed— Node state backgrounds--region-{name}-text/text-maxed— Region text colors
Colorblind mode shifts orange to red-orange (hue 20) and yellow to cleaner yellow (hue 105) for wider separation.
Node states
| Token | Use |
|---|---|
--node-locked-bg/border/text |
Locked (unlearnable) nodes |
--node-flash-color |
Brief flash on level change |
--node-badge-text |
Level badge text (hard white/dark) |
Shadows — Dark mode uses --bg hex with alpha; light mode uses rgba(0,0,0,...).
| Token | Use |
|---|---|
--shadow |
Standard elevation (toasts, dropdowns) |
--shadow-sm |
Subtle elevation |
--shadow-lg |
Deep elevation (modals) |
--shadow-node |
Skill tree nodes |
--shadow-node-hex |
Hex grid nodes |
--shadow-lateral |
Side menu lateral shadow |
--shadow-inset |
Inset depth |
--shadow-drop-icon |
CSS filter: drop-shadow for icons |
Overlays
| Token | Use |
|---|---|
--backdrop-overlay |
Modal backdrop (rgba 0.35-0.4 black) |
--backdrop-overlay-heavy |
Action sheet backdrop (rgba 0.5-0.55 black) |
--backdrop-overlay-context |
Context menu backdrop (rgba 0.25 black) |
Mode-dependent filters
| Token | Dark | Light | Use |
|---|---|---|---|
--brightness-hover |
brightness(1.12) |
brightness(0.92) |
Hover/active brightness shift |
--node-brightness-locked |
brightness(0.7) |
brightness(1.1) saturate(0.3) |
Locked node dimming |
--shadow-text |
Heavy multi-shadow | Lighter multi-shadow | Text shadow on tree nodes |
| Token | Value | Use |
|---|---|---|
--bar-pad |
= --spacing-lg |
HUD safe-area padding |
--tab-height |
38px | Bottom nav bar tab height |
--safe-top/right/bottom/left |
env(safe-area-inset-*) |
Device safe areas |
Z-index scale — Strict layering, no arbitrary values.
| Token | Value | Layer |
|---|---|---|
--z-index-hud |
5 | HUD controls |
--z-index-side-menu |
10 | Side menu panel |
--z-index-context-menu-backdrop |
18 | Context menu backdrop |
--z-index-context-menu |
20 | Context menus |
--z-index-modal |
30 | Modals |
--z-index-toast |
40 | Toast notifications |
--z-index-tooltip |
50 | Tooltips (topmost) |
Recurring visual and interaction conventions used across the codebase. These are not abstracted into shared utilities — they are hand-applied per component. This section documents the conventions so new components match existing ones.
Almost every tappable/clickable element follows this exact recipe:
- Base:
background: var(--bg-raised),border: var(--border-width) solid var(--border),border-radius: var(--radius),color: var(--text-muted). - Hover (mouse only):
@media (hover: hover) { :hover { filter: var(--brightness-hover); } }— brightness shift, never a background color swap. - Active/pressed:
filter: var(--brightness-hover); transform: scale(0.96);— a subtle press-in with the same brightness as hover. Some components usescale(0.97)orscale(0.985)for lighter press feel. - Focus-visible:
outline: 2px solid var(--border-focus); outline-offset: 2px;— always 2px solid, always--border-focus, always 2px offset. Some inset contexts useoutline-offset: -2pxinstead. - Disabled:
opacity: var(--opacity-disabled); cursor: not-allowed; border-color: var(--border-subtle); background: var(--bg-input); color: var(--text-disabled); filter: none; transform: none; - Tap highlight:
-webkit-tap-highlight-color: transparent;on all custom interactive elements.
The global focus policy (app.css) strips outline from all elements, then re-applies it only to text inputs via :focus-visible. Component-level :focus-visible rules handle buttons, links, and custom controls individually.
A consistent way to add theme-color warmth to surfaces without making them feel like accent buttons. Uses color-mix():
- Subtle tint (~6-8%):
color-mix(in srgb, var(--accent) 8%, var(--bg-raised))— open accordion headers, collapsible section open states, code blocks. - Medium tint (~12-20%):
color-mix(in srgb, var(--accent) 18%, var(--border))— onboarding cards, scrollbar thumbs, selected segment backgrounds. - Strong tint (~50%+):
color-mix(in srgb, var(--accent) 55%, var(--border))— accent-blended border used for modal shells, scrollbar thumbs on hover, active left-accent borders.
The modal shell border uses a double color-mix: color-mix(in srgb, color-mix(in srgb, var(--accent) 55%, var(--border)) 50%, transparent) — accent blended into border, then faded to 50% opacity.
Used in SideMenuSection, CollapsibleSection, and Accordion for group headers:
- Font:
--font-sm(13px),--weight-semibold(600),letter-spacing: 0.06em(or--tracking-wide),text-transform: uppercase. - Color:
--text-muted, shifting to--textwhen open/active. - Layout: flex row with
justify-content: space-between,gap: var(--spacing-md).
Used by Accordion and CollapsibleSection for smooth open/close:
- Body:
display: grid; grid-template-rows: 0fr;collapsed,1frexpanded. Transition:var(--ease-emphasis)(0.3s spring-like). - Content:
min-height: 0; opacity: 0;collapsed,opacity: 1expanded with a slight delay (0.2s 0.05s ease). - Arrow: Rotates from
-90deg(closed) to0deg(open) viavar(--ease-standard). Color shifts from--text-mutedat ~0.4-0.5 opacity to--accent-lightat ~0.7-0.8 opacity. - Reduced motion: All three transitions set to
nonevia@media (prefers-reduced-motion: reduce).
Used by modals, context menus, side menu, and onboarding overlays:
- Position:
position: fixed; left: 0; top: 0; width: 100%; height: 100%(orvar(--vv-height)for visual viewport). - Background:
var(--backdrop-overlay)for modals,--backdrop-overlay-heavyfor action sheets,--backdrop-overlay-contextfor context menus. - Blur:
backdrop-filter: blur(var(--blur-md))on modals; context menus and side menu use lighter or no blur. - Safe area padding:
calc(var(--spacing-lg) + var(--safe-*))on all four sides. - Entry:
transition: fadewith 140ms duration.
Two independent systems:
- OS-level (
@media (prefers-reduced-motion: reduce)) — Sets allanimation-durationandtransition-durationto0.01ms. Applied globally inapp.css. - App-level (
.no-animationsclass on<html>) — Setsanimation: none; transition: none; scroll-behavior: autoon all elements. Additionally hides decorative elements:.node-flash,.node-ring,.level-splash,.tree-link-base. Exception: toast progress bars keep their animation so duration is still communicated.
Components check prefersNoAnimations() or $animationsDisabled in script to skip transition configs.
When the user switches theme presets, themeApply.ts adds .theme-transitioning to <html> for 400ms. During this window, a global CSS rule applies transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, fill 0.3s ease, stroke 0.3s ease to all elements. This creates a smooth crossfade between theme palettes without requiring per-component transition rules.
The primary interactive element. Accepts an optional icon (Phosphor component), text via slot, and an optional arrow indicator.
Sizes:
button-md(default):min-height: 38px; min-width: 38px; padding: --spacing-sm --spacing-lg; font-size: --font-base.button-sm(smallprop):height: 32px; min-width: 32px; font-size: --font-sm. Used in compact HUD toolbars.
Tone variants (mutually exclusive):
- Default:
--bg-raisedbackground,--borderborder,--text-mutedtext. negative:--danger-bg/--danger-border/--danger-text. For destructive actions.positive:--success-bg/--success-border/--success-text. For confirmations.accent:--accentbackground and border,--bgtext (inverted). For primary CTAs.
Modifiers:
ghost: Transparent background. Can combine with tones (e.g.,negative ghostshows no background until hover/active).button-flash: Animated scale-up + accent glow triggered by keyboard shortcut.transform: scale(1.08); background: var(--bg-tinted); border-color: var(--accent); box-shadow: 0 0 12px accent@40%.with-icon: Flex layout withgap: --spacing-md.with-arrow: Right-aligned caret (right or down).
Features:
toastMessage/toastNegative: Auto-shows a toast on click.shortcut/flashOnAction: Keyboard shortcut badge in tooltip and flash animation on trigger.description+descriptionIcon: Two-line button with subtitle row.- Tooltip integration via
use:tooltip.
Joins adjacent buttons with shared borders, suppressing inner radii. Two modes:
separatorSide="left"(default): left button keeps its right border.separatorSide="right": right button keeps its left border.fillmode: children flex equally, text wraps, icons collapse at small widths.
Displays at the bottom of the viewport, stacked, max 3 visible. Auto-dismisses after 2600ms by default (configurable). Deduplicates by message or key.
Tones:
positive(default):--bg-raisedbackground,--border-subtleborder,--text-mutedtext. Icon:CheckCircleIconin--accentcolor.negative:--danger-bg/--danger-border/--danger-text. Icon:WarningCircleIcon. Uses a distinct shake-like entry animation (toast-enter-negative).
Features:
- Progress bar: 2px
::afterpseudo-element at the bottom, animatingscaleX(1)toscaleX(0)over the toast duration. - Action toasts: Two-row layout with dismiss (X) and action button. Action button is pill-shaped with accent tint.
- Permanent toasts:
durationMs: 0hides the progress bar. - Spinner variant: Shows a
Spinnercomponent instead of/alongside the status icon. - HUD overlap avoidance: Measures overlap with
.bot-right-actionsand.bot-left-actionsand shifts upward.
Single-modal-at-a-time system driven by a writable store. Five modal types:
- ConfirmModal — Title + optional message + Cancel/Confirm buttons. Supports
confirmNegative(danger tone) andconfirmPositive(success tone). Width: narrow (320px floor). - InputModal — Number input with label, optional label detail, stepper. Width: narrow.
- TextInputModal — Text input with label, placeholder, maxLength. Width: narrow.
- LoadBuildModal — Build import with paste/file input. Width: standard (380px floor).
- ResetTreeChoicesModal (Action Sheet) — Bottom-anchored sheet with choice cards. Swipe-to-dismiss via pointer events. Uses rounded
border-radius: 28px, gradient background blending--surfaceand--accent, and heavier backdrop overlay. Width: wide (up to 40rem).
Shell styling: --bg-panel background, accent-blended border, double shadow (--shadow + --shadow-lg). Centered vertically with margin: auto. Entry animation: scale from 0.96 + translateY(8px) with cubicOut easing.
Keyboard: Focus trap (Tab/Shift+Tab), Escape to cancel, Enter to confirm. Auto-focuses confirm button on open.
Rich choice cards used inside modals (currently ResetTreeChoicesModal). Each choice has:
- An icon in a tinted badge (
--choice-accentat 20% with inset highlight) - Label + description text
- Optional amount highlighting
Tone variants (via --choice-accent CSS property):
orange:--region-orange-accentblue:--region-blue-accentyellow:--region-yellow-accentdanger:--accent-dangerwith--danger-bg/border/textoverrides
Staggered entry animation: each choice and header animates in with increasing delay (action-sheet-item-in).
Radio-group-style selector with equal-width segments. Supports an optional header row with icon, label, description, and keyboard shortcut badge.
- Segment selected:
color: --text; background: color-mix(in srgb, var(--surface) 78%, var(--accent))— surface tinted with accent. - Segment unselected hover:
color-mix(in srgb, var(--bg-raised) 80%, var(--text) 20%). - Leading icon mode: Icon sits outside the segments in a
--bg-inputarea, separated by a subtle border.
Full-width button-style row with label, optional description, optional icon, and a toggle track.
- Track off:
color-mix(in srgb, var(--border) 80%, transparent)with matching border. - Track on:
color-mix(in srgb, var(--accent-light) 90%, transparent)with--accent-lightborder. - Thumb: 24px circle, white (
oklch(0.97 0 0)), slides 20px on toggle. Uses--ease-emphasisfor spring-like motion. Shadow via--shadow.
Expandable section with bordered container. Similar to CollapsibleSection but wrapped in its own --bg-raised box with --radius corners.
- Open state: Header gets subtle accent tint (
accent 6%), bottom border appears, icon shifts to--accent-light. - Body:
--surfacebackground (slightly darker/lighter than the header). - Uses the standard expand/collapse animation pattern (grid-template-rows).
Borderless expandable section for use inside scrolling panels (side menu pages). Sticky header with top/bottom borders.
- Header background:
--bg-raised, shifts to accent-tinted (8%) when open. - Hover:
color-mix(in srgb, var(--text) 8%, var(--bg-raised)). - Active:
color-mix(in srgb, var(--text) 12%, var(--bg-raised))+scale(0.96).
Horizontal fill bar with tick marks at quintile boundaries.
- Track:
--bg-raised, 8px tall, 4px radius. - Fill:
linear-gradient(90deg, var(--accent), var(--accent-light)), scales from 0 to 1 viascaleX(). - Ticks: Generated as a
linear-gradientoverlay, 2px wide lines in--bg-panelcolor.
Rotating arc indicator. Two tones:
default: Arc color--accent, track--text-mutedat 22%.negative: Arc color--danger-text, track--danger-textat 22%.
Portal-positioned, centered text bubble. Entry: tooltip-in 0.12s scale+fade animation.
- Background:
--bg-panel, border:--border-subtle, shadow:--shadow. - Supports multi-line (
.tooltip-line), keyboard shortcut badges (.tooltip-shortcut+.kbd), and action grids (.tooltip-action-grid) for node level previews. - Touch devices: no transform adjustments; keyboard-only shortcut badges hidden via
@media (hover: none).
Keyboard shortcut key display: --surface/--bg-input blended background, --border at 60%, --radius-sm, --text-disabled text. Groups via .kbd-group, separated by .kbd-sep.
Full-viewport overlay for tabbed content (settings, controls). Uses --bg-modal background with BottomNavBar for tab switching.
- Keyboard: Escape to close, Ctrl+Tab / Ctrl+Shift+Tab to cycle tabs.
- Safe area padding:
--safe-topon content area.