Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/dev.void/llms.txt

Use this file to discover all available pages before exploring further.

The dev.void portfolio pairs three carefully chosen typefaces with a cohesive Framer Motion animation language to reinforce its space-exploration narrative. Space Grotesk gives the site its crisp, geometric backbone; Cormorant Garamond adds editorial grace to quoted passages; and JetBrains Mono grounds all technical labels in authentic monospace authority. On top of typography, Framer Motion drives six distinct animation patterns — from page-level blur transitions to spring-based cursor tracking — each chosen to make interactions feel weightless and cinematic without sacrificing performance.

Typography System

Font Families

All three families are loaded via a single Google Fonts @import at the very top of assets/main.css:
@import "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@100..800&display=swap";

Space Grotesk

Tailwind class: font-sansWeights loaded: 300, 400, 500, 600, 700Applied globally to html, body, and all h1h6 elements. This is the dominant typeface for every heading, paragraph, nav item, and button label. Headings additionally receive letter-spacing: -0.025em (Tailwind tracking-tight) to tighten the naturally wide geometric letterforms at large sizes.

Cormorant Garamond

Tailwind class: font-serifWeights loaded: 400, 600 (normal + italic)Used sparingly for font-serif italic flourishes — specifically the testimonial quote bodies in AsteroidQuotes (font-serif italic text-lg text-slate-300). The contrast between the spindly serifs and the surrounding sans-serif text creates a deliberate editorial tension, like a mission log entry written by hand.

JetBrains Mono

Tailwind class: font-monoWeights loaded: 100–800 (variable)Applied globally to code, kbd, samp, and pre elements, and explicitly via font-mono on UI labels, badges, role tags, tech-stack chips, and section identifiers. Its even, spaced-out letterforms read well at very small sizes (e.g., text-[10px] uppercase tracking-widest on role labels).

Base Styles from main.css

/* html / :host — site-wide default */
html, :host {
  font-family: Space Grotesk, sans-serif;
}

/* body — reinforced + smoothing */
body {
  font-family: Space Grotesk, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Headings — tight tracking */
h1, h2, h3, h4, h5, h6 {
  font-family: Space Grotesk, sans-serif;
  letter-spacing: -0.025em;
}

/* Code elements */
code, kbd, samp, pre {
  font-family: JetBrains Mono, monospace;
}

How to Change Fonts

1

Choose replacement families on Google Fonts

Visit fonts.google.com and select your alternatives. Construct a new @import URL that includes all the weight and style variants you need. For example, to swap Space Grotesk for Inter:
@import "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&family=JetBrains+Mono:wght@100..800&display=swap";
2

Replace the @import at the top of main.css

Open assets/main.css and replace only the first line (the @import statement). Do not touch anything else in this line — it is the only external network dependency the CSS file has.
3

Update font-family references in main.css

Search for every Space Grotesk occurrence in main.css and replace with your new family name. There are four occurrences: the html rule, the body rule, the headings rule, and the .font-sans utility class.
grep -n "Space Grotesk" assets/main.css
# Then replace each:
sed -i 's/Space Grotesk/Inter/g' assets/main.css
4

(Preferred) Rebuild from source

For a permanent, build-safe change, edit tailwind.config.js — update theme.extend.fontFamily.sans, .serif, and .mono — then run npm run build. The compiled main.css will be regenerated with correct class names throughout.
Cormorant Garamond and JetBrains Mono are intentionally narrow in scope. If you swap them out, audit font-serif italic usages in AsteroidQuotes.js and font-mono usages on badge/label elements to ensure the replacement typefaces look correct at those specific sizes and weights.

Reduced-Motion Accessibility

main.css includes a prefers-reduced-motion media query that respects the operating system’s accessibility setting. When a user has enabled “Reduce Motion” in their OS preferences, all CSS animation and transition durations are forced to near-zero, and the aurora SVG filter is removed:
@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .aurora-filter {
    filter: none;
  }
}
This media query only governs CSS animations and transitions. Framer Motion animations are JavaScript-driven and are not automatically suppressed by this block. If you add new Framer Motion animations, check window.matchMedia('(prefers-reduced-motion: reduce)').matches in your component and conditionally set durations to 0 or skip the animation entirely.
The CometCursor component already handles a related case — it returns null on coarse-pointer (touch) devices, avoiding the cursor entirely where it would be meaningless.

Framer Motion Animation Patterns

All six patterns below are used in production components. Each is documented with the exact prop values taken from the compiled component source.

1. Page Transition — PageWrapper

The outermost route wrapper fades, lifts, and blurs every page in and out. The custom cubic-bezier easing [0.22, 1, 0.36, 1] is an “expo out” curve that produces an energetic snap-in feel.
// PageWrapper — applied to every route's root element
<motion.div
  initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
  animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
  exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }}
  transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
  {children}
</motion.div>
To slow down all page transitions globally — useful for demo recordings or when debugging layout shifts — increase the duration value on the PageWrapper transition. Changing 0.6 to 1.2 will halve the speed of every route change sitewide.

2. Scroll-Triggered Entrance — MissionLogTimeline, FlightRecorderCase

Section content enters from below as the user scrolls it into the viewport. The once: true option means the animation only fires on the first reveal — elements do not re-animate when the user scrolls back up. The margin: '-100px' offset triggers the animation slightly before the element fully enters the visible area, so content is already mid-transition when the eye reaches it.
// MissionLogTimeline, FlightRecorderCase — per-card entrance
<motion.div
  initial={{ opacity: 0, y: 50 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: '-100px' }}
  transition={{ duration: 0.6 }}
>
  {content}
</motion.div>

3. Spring Cursor — CometCursor

The custom cursor uses useMotionValue to track raw mouse coordinates and useSpring to interpolate the cursor element’s position with physical spring damping. The result is a cursor that lags slightly behind the pointer and overshoots on fast movements — like a comet trailing its nucleus.
// CometCursor — spring-based cursor tracking
import { useMotionValue, useSpring } from 'framer-motion';

const rawX = useMotionValue(-100);
const rawY = useMotionValue(-100);

const x = useSpring(rawX, { stiffness: 300, damping: 25, mass: 0.5 });
const y = useSpring(rawY, { stiffness: 300, damping: 25, mass: 0.5 });

// Applied to cursor element:
<motion.div style={{ x, y, translateX: '-50%', translateY: '-50%' }} />
The same x/y spring values drive both the inner dot and the outer ring simultaneously, keeping them physically linked while the scale transforms diverge on click.

4. Continuous Rotation — OrbitingSatellites

Orbital rings and satellites use an infinite, perfectly linear rotation driven by Framer Motion. The ease: 'linear' setting is critical here — any easing curve would cause the ring to visibly speed up and slow down each revolution.
// OrbitingSatellites — eternal orbital spin
<motion.div
  animate={{ rotate: 360 }}
  transition={{
    duration: 20,
    repeat: Infinity,
    ease: 'linear'
  }}
/>
Individual satellites use different duration values per orbital ring (20 s, 35 s, 50 s from inner to outer) to simulate realistic orbital velocity differences. Note that TelemetryRadar drives its sweep via requestAnimationFrame and React state rather than Framer Motion, so it is not covered by this pattern.

5. Float Animation — AsteroidQuotes

Each testimonial card hovers in a slow, looping float. The y and rotate keyframes are defined as three-point arrays (start → peak → return), which Framer Motion automatically interpolates as a continuous cycle. Each card receives a different yOffset and delay so all three never align — the staggered phase creates organic, living movement.
// AsteroidQuotes — per-card floating idle animation
<motion.div
  animate={{
    y:      [yOffset, yOffset - 15, yOffset],   // 15px vertical travel
    rotate: [-1, 1, -1]                          // 2° rotation sway
  }}
  transition={{
    y:      { duration: 6, repeat: Infinity, ease: 'easeInOut', delay: entry.delay },
    rotate: { duration: 8, repeat: Infinity, ease: 'easeInOut', delay: entry.delay }
  }}
/>
The y and rotate sub-transitions have intentionally different durations (6 s vs. 8 s). Because they drift out of phase with each other, the combined motion never feels repetitive even though both are simple three-keyframe loops.

6. Shared Layout Expansion — MissionPatchWall

Project patch cards expand into a full modal using Framer Motion’s shared layout system. The layoutId prop tells Framer Motion that the small card and the large modal are the same element at different sizes — it automatically animates the border-radius, position, and dimensions between the two states. AnimatePresence wraps the modal so it can animate out when dismissed.
// MissionPatchWall — patch card with shared layout expansion

// Thumbnail (always mounted):
<motion.div layoutId={`patch-container-${project.id}`} onClick={() => setOpen(id)}>
  {/* small circular card */}
</motion.div>

// Modal (conditionally mounted via AnimatePresence):
<AnimatePresence>
  {open && (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      className="fixed inset-0 z-50 …backdrop…"
      onClick={() => setOpen(null)}
    >
      <motion.div layoutId={`patch-container-${project.id}`}>
        {/* expanded modal content */}
      </motion.div>
    </motion.div>
  )}
</AnimatePresence>
The backdrop fade (opacity: 0 → 1) and the card expansion (layoutId morph) run simultaneously but are independently controlled, so the background dims at its own pace while the card reshapes itself.

Animation Quick-Reference

PatternComponent(s)Key Props
Page blur transitionPageWrapperfilter: blur, ease [0.22,1,0.36,1], 0.6 s
Scroll entranceMissionLogTimeline, FlightRecorderCasewhileInView, viewport.once, 0.6 s
Spring cursorCometCursoruseSpring, stiffness 300, damping 25
Continuous rotationOrbitingSatellitesrotate: 360, repeat: Infinity, linear, 20/35/50 s
Floating idleAsteroidQuotesThree-keyframe y/rotate array, staggered delay
Shared layout modalMissionPatchWalllayoutId, AnimatePresence

Build docs developers (and LLMs) love