Identity
Forma
A new visual language for an architecture studio, made to grow.
Discuss a similar projectLIVE PREVIEW / FICTIONAL DESIGNER, PROJECTS, PRICES & TESTIMONIALS
Available for select projects
I’m Alex, an independent designer and developer turning ambitious ideas into digital experiences people love.
Selected work
Identity
A new visual language for an architecture studio, made to grow.
Discuss a similar projectHow I work
01 / A clear point of view
A considered identity gives every detail a place to belong.
02 / Made with intention
From the first hello to the final hover, every moment feels considered.
03 / Ready to go further
A smooth handoff keeps the good work moving into the real world.
04 / Built around real people
Make a useful promise clear, then give people a confident next step.
Ways to work together
For a considered visual direction.
$1,200/ month
Monthly design retainer
From the first idea to a live site.
$2,400/ month
Monthly design & development retainer
Kind words
The rare kind of collaborator who makes the idea better, then makes it happen.
Jamie Chen
Founder, Forma · Example testimonial
Something in mind?
Your turn.
npx shadcn@latest add https://gear5-ui.vercel.app/r/portfolio-template.jsonInstalls the page component, five blocks, and safe-link helper. Requires React and Tailwind CSS 4. Replace all example content before publishing; the service links open your configured email address.
import { PortfolioTemplate } from "@/components/gear5/portfolio-template";
export default function Portfolio() {
return <PortfolioTemplate name="Your name" initials="yn."
email="you@example.com" accent="#d9fc87"
title={"Thoughtful design.\nUnexpected details."}
description="Describe the work you do and the people you help."
projects={[{
id: "studio", title: "Studio", category: "Brand & web design",
description: "The story behind the project.", href: "/work/studio",
symbol: "s.", color: "#d5e6d3",
}]}
testimonials={[]}
services={[]}
/>;
}"use client";
import { useId, type CSSProperties } from "react";
import { OrbitHero } from "@/components/gear5/orbit-hero";
import { ProjectGallery, type GalleryProject } from "@/components/gear5/project-gallery";
import { SpotlightBento, type BentoFeature } from "@/components/gear5/spotlight-bento";
import { PricingSwitch, type PricingPlan } from "@/components/gear5/pricing-switch";
import {
TestimonialDeck,
deckTestimonials,
type DeckTestimonial,
} from "@/components/gear5/testimonial-deck";
import { sanitizeHref } from "@/lib/gear5/sanitize";
export interface PortfolioTemplateProps {
name?: string;
initials?: string;
accent?: string;
email?: string;
title?: string;
description?: string;
availability?: string;
projects?: GalleryProject[];
process?: BentoFeature[];
testimonials?: DeckTestimonial[];
projectLinkLabel?: string;
services?: PricingPlan[];
labels?: {
work?: string;
process?: string;
services?: string;
contact?: string;
contactEyebrow?: string;
contactTitle?: string;
footer?: string;
};
}
const servicesDefault: PricingPlan[] = [
{
id: "design",
name: "Design partner",
description: "For a considered visual direction.",
monthlyPrice: "$1,200",
yearlyPrice: "$1,000",
monthlyNote: "Monthly design retainer",
yearlyNote: "$12,000 billed yearly",
features: [
"Brand & interface design",
"One request at a time",
"Direct collaboration",
],
href: "#contact",
},
{
id: "full",
name: "Design + development",
description: "From the first idea to a live site.",
monthlyPrice: "$2,400",
yearlyPrice: "$2,000",
monthlyNote: "Monthly design & development retainer",
yearlyNote: "$24,000 billed yearly",
features: [
"Everything in design",
"React & Next.js development",
"Launch support",
],
href: "#contact",
featured: true,
},
];
const projectsDefault: GalleryProject[] = [
{
id: "forma",
title: "Forma",
category: "Identity",
description:
"A new visual language for an architecture studio, made to grow.",
artworkAlt: "Abstract pale sage identity artwork marked f.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center bg-[#d5e6d3] font-serif text-8xl text-[#283329]">
f.
</div>
),
href: "#contact",
linkLabel: "Discuss a similar project",
},
{
id: "mono",
title: "Mono",
category: "Product",
description: "Turning a busy creative workflow into a calmer workspace.",
artworkAlt: "Abstract lavender product artwork marked m.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center bg-[#c9c4e4] font-mono text-7xl text-[#302b48]">
m↗
</div>
),
href: "#contact",
linkLabel: "Discuss a similar project",
},
{
id: "terrain",
title: "Terrain",
category: "Digital",
description: "An independent publication for the places in between.",
artworkAlt: "Warm sand-colored publication artwork marked t.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center bg-[#ead3b9] font-serif text-7xl italic text-[#49362b]">
t*
</div>
),
href: "#contact",
linkLabel: "Discuss a similar project",
},
];
/** Complete sample portfolio with an editable gallery and process grid. Replace sample content before publishing. */
export function PortfolioTemplate({
name = "Alex Morgan",
initials = "am.",
accent = "#d9fc87",
email = "hello@example.com",
title = "Thoughtful design.\nUnexpected details.",
description = "I’m Alex, an independent designer and developer turning ambitious ideas into digital experiences people love.",
availability = "Available for select projects",
projects = projectsDefault,
process: processFeatures,
testimonials = deckTestimonials,
projectLinkLabel = "Discuss a similar project",
services = servicesDefault,
labels = {},
}: PortfolioTemplateProps) {
const id = useId().replace(/:/g, "");
const work = `${id}-work`,
processId = `${id}-process`,
pricing = `${id}-services`,
contact = `${id}-contact`;
const emailHref = sanitizeHref(`mailto:${email}`);
return (
<div
style={{ "--g5-accent": accent } as CSSProperties}
className="bg-[#0b0c0e] p-4 text-[#f4f4ef] sm:p-8"
>
<div className="mx-auto max-w-6xl">
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 px-3 py-3">
<a
href={`#${id}-home`}
aria-label={name}
className="font-serif text-3xl font-bold tracking-[-.06em]"
>
{initials}
</a>
<nav
aria-label={name}
className="flex flex-wrap gap-5 text-xs text-[#c2c4bd]"
>
<a href={`#${work}`}>{labels.work ?? "Work"}</a>
<a href={`#${processId}`}>{labels.process ?? "Process"}</a>
<a href={`#${pricing}`}>{labels.services ?? "Services"}</a>
<a href={`#${contact}`} className="text-[var(--g5-accent)]">
{labels.contact ?? "Let’s talk"} ↗
</a>
</nav>
</header>
<div id={`${id}-home`}>
<OrbitHero
headingLevel="h1"
eyebrow={availability}
title={title}
description={description}
mark={initials}
accent={accent}
action={{ label: labels.work ?? "Selected work", href: `#${work}` }}
secondaryAction={{
label: labels.contact ?? "Let’s talk",
href: `#${contact}`,
}}
/>
</div>
<div id={work} className="mt-6 scroll-mt-24">
<ProjectGallery
accent={accent}
title="A few things I’ve made."
eyebrow={labels.work ?? "Selected work"}
projects={projects.map((project) => ({
...project,
linkLabel: project.linkLabel ?? projectLinkLabel,
href: project.href === "#contact" ? emailHref : project.href,
}))}
/>
</div>
<div id={processId} className="mt-6 scroll-mt-24">
<SpotlightBento
accent={accent}
eyebrow={labels.process ?? "How I work"}
title="A considered path from first sketch to launch."
features={processFeatures}
/>
</div>
<div id={pricing} className="mt-6 scroll-mt-24">
<PricingSwitch
title="A good fit for your next chapter."
eyebrow={labels.services ?? "Ways to work together"}
accent={accent}
plans={services.map((plan) => ({
...plan,
href: plan.href === "#contact" ? emailHref : plan.href,
}))}
actionLabel={labels.contact ?? "Let’s talk"}
/>
</div>
<div className="mt-6">
<TestimonialDeck testimonials={testimonials} accent={accent} />
</div>
<section
id={contact}
className="mt-6 scroll-mt-24 rounded-3xl bg-[var(--g5-accent)] p-8 text-[#111214] sm:p-12"
>
<p className="font-mono text-xs uppercase">
{labels.contactEyebrow ?? "Something in mind?"}
</p>
<h2 className="mt-4 text-4xl font-medium tracking-[-.06em] sm:text-6xl">
{labels.contactTitle ?? "Let’s make it happen."}
</h2>
<a
href={emailHref}
className="mt-8 inline-flex min-h-11 items-center gap-8 rounded-full border border-black/30 px-6 py-3 text-sm font-semibold"
>
{email}
<span aria-hidden="true">↗</span>
</a>
</section>
<footer className="flex flex-wrap justify-between gap-3 px-3 pt-8 text-xs text-[#b4b6af]">
<span>{name}</span>
<span>{labels.footer ?? "Made with care. Built with Gear5."}</span>
</footer>
</div>
</div>
);
}
"use client";
import { useRef, type CSSProperties } from "react";
import { sanitizeHref } from "@/lib/gear5/sanitize";
export interface OrbitHeroProps {
eyebrow?: string;
title?: string;
description?: string;
action?: { label: string; href: string };
secondaryAction?: { label: string; href: string };
accent?: string;
headingLevel?: "h1" | "h2";
artworkLabels?: [string, string, string];
mark?: string;
className?: string;
}
/** Pointer-responsive artwork is decorative; content and links work without motion. */
export function OrbitHero({
eyebrow = "Independent designer & developer",
title = "Ideas into\nsomething real.",
description = "Thoughtful digital experiences. A little unexpected. Always made for people.",
action = { label: "Explore my work", href: "#work" },
secondaryAction = { label: "Let’s talk", href: "#contact" },
accent = "#d9fc87",
headingLevel: Heading = "h2",
artworkLabels = ["Design", "Build", "Make it move"],
mark = "G5",
className = "",
}: OrbitHeroProps) {
const art = useRef<HTMLDivElement>(null);
return (
<section
style={{ "--g5-accent": accent } as CSSProperties}
className={`overflow-hidden rounded-3xl bg-[#111214] p-6 text-[#f4f4ef] sm:p-10 ${className}`}
>
<div className="grid items-center gap-8 lg:grid-cols-[1.1fr_1fr]">
<div>
<p className="mb-6 flex items-center gap-2 font-mono text-[11px] tracking-[.12em] text-[#c2c4bd] uppercase">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-[var(--g5-accent)]"
/>
{eyebrow}
</p>
<Heading className="whitespace-pre-line text-[clamp(2.6rem,6.5vw,5.7rem)] leading-[.99] font-medium tracking-[-.075em]">
{title}
</Heading>
<p className="mt-6 max-w-md text-sm leading-7 text-[#b4b6af] sm:text-base">
{description}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a
href={sanitizeHref(action.href)}
className="inline-flex min-h-11 items-center gap-5 rounded-full bg-[var(--g5-accent)] px-6 py-3 text-sm font-semibold text-[#111214] transition-opacity hover:opacity-85 motion-reduce:transition-none"
>
{action.label}
<span aria-hidden="true">↗</span>
</a>
<a
href={sanitizeHref(secondaryAction.href)}
className="inline-flex min-h-11 items-center rounded-full border border-white/20 px-5 py-3 text-sm transition-colors hover:bg-white/10 motion-reduce:transition-none"
>
{secondaryAction.label}
</a>
</div>
</div>
<div
ref={art}
aria-hidden="true"
className="relative mx-auto flex aspect-square w-full max-w-[460px] items-center justify-center [perspective:900px]"
onPointerMove={(event) => {
if (event.pointerType === "touch" || !art.current) return;
const box = event.currentTarget.getBoundingClientRect();
art.current.style.setProperty(
"--g5-x",
`${((event.clientY - box.top) / box.height - 0.5) * -14}deg`,
);
art.current.style.setProperty(
"--g5-y",
`${((event.clientX - box.left) / box.width - 0.5) * 14}deg`,
);
}}
onPointerLeave={() => {
art.current?.style.setProperty("--g5-x", "0deg");
art.current?.style.setProperty("--g5-y", "0deg");
}}
>
<div className="absolute inset-6 rounded-full border border-dashed border-white/15" />
<div className="absolute inset-16 rounded-full border border-white/10" />
<div className="relative flex size-[65%] items-center justify-center rounded-[2.5rem] border border-white/15 bg-[#1e2023] shadow-[0_30px_80px_#0008] transition-transform duration-500 motion-safe:[transform:rotateX(var(--g5-x,0deg))_rotateY(var(--g5-y,0deg))_rotate(-12deg)] motion-reduce:transition-none">
<span className="font-mono text-[clamp(4rem,10vw,8rem)] font-bold tracking-[-.12em] text-[var(--g5-accent)]">
{mark}
</span>
<span className="absolute inset-5 rounded-[1.7rem] border border-white/10" />
</div>
<span className="absolute start-0 top-[17%] rounded-xl border border-white/20 bg-[#282b2e] px-5 py-3 font-mono text-xs shadow-xl motion-safe:-rotate-12">
{artworkLabels[0]}
<span className="ms-4 text-[var(--g5-accent)]">✳</span>
</span>
<span className="absolute end-0 top-[40%] rounded-xl bg-[var(--g5-accent)] px-5 py-3 font-mono text-xs text-[#111214] shadow-xl motion-safe:rotate-6">
{artworkLabels[1]}
<span className="ms-4">↗</span>
</span>
<span className="absolute bottom-[12%] start-[12%] rounded-xl border border-white/20 bg-[#282b2e] px-5 py-3 font-mono text-xs shadow-xl motion-safe:rotate-6">
{artworkLabels[2]}
<span className="ms-4 text-[var(--g5-accent)]">⌘</span>
</span>
</div>
</div>
</section>
);
}
/**
* Security primitive: neutralise the one class of URL that turns "render a
* link" into "run arbitrary script" — `javascript:` and other executable
* schemes reaching an `href`/`src` from data the app did not author itself
* (a CMS field, a query param, a user's own profile link).
*/
const SAFE_SCHEMES = new Set(["http:", "https:", "mailto:", "tel:", "sms:"]);
/**
* Returns `url` unchanged if its scheme is safe to navigate to, or `"#"`
* otherwise. Relative URLs (no scheme) are always safe and pass through.
*/
export function sanitizeHref(url: string): string {
const trimmed = url.trim();
if (!trimmed) return "#";
// Browsers discard tabs and newlines when parsing a URL scheme.
const schemeProbe = trimmed.replace(/[\t\r\n]/g, "");
if (!/^[a-z][a-z0-9+.-]*:/i.test(schemeProbe)) return trimmed;
try {
const scheme = new URL(trimmed, "https://example.com").protocol;
return SAFE_SCHEMES.has(scheme) ? trimmed : "#";
} catch {
return "#";
}
}
"use client";
import { useId, useState, type CSSProperties, type ReactNode } from "react";
import { sanitizeHref } from "@/lib/gear5/sanitize";
export interface GalleryProject {
id: string;
title: string;
category: string;
description: string;
/** Add a locally hosted image, optimized with your framework, or a small SVG preview. */
artwork?: ReactNode;
artworkAlt?: string;
href?: string;
linkLabel?: string;
}
const exampleProjects: GalleryProject[] = [
{
id: "forma",
title: "Forma",
category: "Identity",
description: "A quiet visual language for architecture with big ideas.",
artworkAlt: "Abstract sage-green identity card showing a serif f.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center overflow-hidden bg-[#c8d5c1] text-[#273028]">
<span className="font-serif text-[clamp(5rem,18vw,10rem)] tracking-[-.12em]">
f.
</span>
</div>
),
linkLabel: "Read the Forma case study",
},
{
id: "mono",
title: "Mono",
category: "Product",
description: "A calmer way to keep a small team moving forward.",
artworkAlt: "Lavender product tile marked m with an upward arrow.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center overflow-hidden bg-[#d2ccef] text-[#302b48]">
<span className="font-mono text-[clamp(4rem,15vw,8rem)] tracking-[-.16em]">
m↗
</span>
</div>
),
linkLabel: "Read the Mono case study",
},
{
id: "terrain",
title: "Terrain",
category: "Digital",
description: "A digital home that makes room for the natural world.",
artworkAlt: "Warm sand-colored creative title card marked t.",
artwork: (
<div className="flex aspect-[5/4] items-center justify-center overflow-hidden bg-[#e8cdb5] text-[#49362b]">
<span className="font-serif text-[clamp(4rem,15vw,8rem)] italic">
t*
</span>
</div>
),
linkLabel: "Read the Terrain case study",
},
];
export interface ProjectGalleryProps {
projects?: GalleryProject[];
title?: string;
eyebrow?: string;
allLabel?: string;
emptyLabel?: string;
accent?: string;
className?: string;
}
/** A filtered, image-forward portfolio grid with a keyboard-friendly detail preview. */
export function ProjectGallery({
projects = exampleProjects,
title = "Selected work, up close.",
eyebrow = "A few things I’ve made",
allLabel = "Everything",
emptyLabel = "Add projects to see them here.",
accent = "#d9fc87",
className = "",
}: ProjectGalleryProps) {
const id = useId();
const [category, setCategory] = useState(allLabel);
const [selectedId, setSelectedId] = useState(projects[0]?.id ?? "");
const categories = [
allLabel,
...new Set(projects.map((project) => project.category)),
];
const visible =
category === allLabel
? projects
: projects.filter((project) => project.category === category);
const selected =
visible.find((project) => project.id === selectedId) ?? visible[0];
function moveSelection(index: number) {
const project = visible[index];
if (project) setSelectedId(project.id);
}
return (
<section
style={{ "--g5-accent": accent } as CSSProperties}
aria-labelledby={`${id}-title`}
className={`rounded-3xl bg-[#111214] p-6 text-[#f4f4ef] sm:p-10 ${className}`}
>
<div className="flex flex-wrap items-end justify-between gap-5">
<div>
<p className="font-mono text-[11px] tracking-[.12em] text-[#b4b6af] uppercase">
{eyebrow}
</p>
<h2
id={`${id}-title`}
className="mt-3 text-3xl font-medium tracking-[-.05em] sm:text-4xl"
>
{title}
</h2>
</div>
{categories.length > 2 && (
<div aria-label="Filter projects" className="flex flex-wrap gap-2">
{categories.map((option) => (
<button
key={option}
type="button"
aria-pressed={category === option}
onClick={() => setCategory(option)}
className={`min-h-10 rounded-full border px-4 py-2 text-xs transition-colors motion-reduce:transition-none ${
category === option
? "border-transparent bg-[var(--g5-accent)] font-medium text-[#111214]"
: "border-white/15 text-[#c2c4bd] hover:bg-white/5"
}`}
>
{option === allLabel ? allLabel : option}
</button>
))}
</div>
)}
</div>
{visible.length === 0 ? (
<p className="mt-8 rounded-2xl border border-white/10 px-5 py-12 text-center text-sm text-[#b4b6af]">
{emptyLabel}
</p>
) : (
<>
<div
role="group"
aria-label="Projects"
className="mt-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-3"
>
{visible.map((project, index) => (
<button
key={project.id}
id={`${id}-project-${project.id}`}
type="button"
aria-pressed={selected?.id === project.id}
aria-controls={`${id}-detail`}
onClick={() => moveSelection(index)}
onKeyDown={(event) => {
let next: number;
if (event.key === "Home") next = 0;
else if (event.key === "End") next = visible.length - 1;
else if (
event.key === "ArrowRight" ||
event.key === "ArrowDown"
)
next = (index + 1) % visible.length;
else if (event.key === "ArrowLeft" || event.key === "ArrowUp")
next = (index - 1 + visible.length) % visible.length;
else return;
event.preventDefault();
moveSelection(next);
document
.getElementById(`${id}-project-${visible[next].id}`)
?.focus();
}}
className={`group min-w-0 overflow-hidden rounded-2xl border text-start transition-[border-color,opacity] motion-reduce:transition-none ${
selected?.id === project.id
? "border-[var(--g5-accent)]"
: "border-white/10 hover:border-white/35"
}`}
>
<div
aria-hidden={!project.artworkAlt}
role={project.artworkAlt ? "img" : undefined}
aria-label={project.artworkAlt}
className="overflow-hidden [&>div]:transition-transform [&>div]:duration-500 motion-safe:group-hover:[&>div]:scale-[1.03] motion-reduce:[&>div]:transition-none"
>
{project.artwork ?? (
<div className="flex aspect-[5/4] items-center justify-center bg-[#26292b] font-serif text-7xl text-white/70">
{project.title.slice(0, 1)}
</div>
)}
</div>
<span className="flex items-center justify-between gap-3 px-4 py-4">
<span className="min-w-0">
<span className="block truncate text-sm font-medium">
{project.title}
</span>
<span className="mt-1 block font-mono text-[10px] text-[#b4b6af]">
{project.category}
</span>
</span>
<span
aria-hidden="true"
className="text-sm text-[var(--g5-accent)]"
>
{selected?.id === project.id ? "↗" : "+"}
</span>
</span>
</button>
))}
</div>
{selected && (
<article
id={`${id}-detail`}
aria-live="polite"
aria-atomic="true"
className="mt-4 grid overflow-hidden rounded-2xl border border-white/10 bg-[#181a1c] sm:grid-cols-[.8fr_1fr]"
>
<div
aria-hidden={!selected.artworkAlt}
role={selected.artworkAlt ? "img" : undefined}
aria-label={selected.artworkAlt}
className="min-h-48 overflow-hidden sm:min-h-64"
>
{selected.artwork ?? (
<div className="flex h-full min-h-48 items-center justify-center bg-[#26292b] font-serif text-8xl text-white/70 sm:min-h-64">
{selected.title.slice(0, 1)}
</div>
)}
</div>
<div className="flex flex-col justify-center p-6 sm:p-8">
<p className="font-mono text-[10px] text-[var(--g5-accent)] uppercase">
{selected.category}
</p>
<h3 className="mt-3 text-2xl font-medium tracking-[-.04em]">
{selected.title}
</h3>
<p className="mt-3 max-w-md text-sm leading-7 text-[#b4b6af]">
{selected.description}
</p>
{selected.href && (
<a
href={sanitizeHref(selected.href)}
className="mt-6 inline-flex min-h-11 w-fit items-center gap-4 rounded-full border border-white/20 px-5 py-3 text-sm transition-colors hover:bg-white/5 motion-reduce:transition-none"
>
{selected.linkLabel ?? "View project"}
<span
aria-hidden="true"
className="text-[var(--g5-accent)]"
>
↗
</span>
</a>
)}
</div>
</article>
)}
</>
)}
</section>
);
}
"use client";
import {
useEffect,
useId,
useRef,
type CSSProperties,
type ReactNode,
type PointerEvent,
} from "react";
import { sanitizeHref } from "@/lib/gear5/sanitize";
export interface BentoFeature {
id: string;
eyebrow: string;
title: string;
description: string;
size?: "standard" | "wide" | "tall";
visual?: ReactNode;
href?: string;
linkLabel?: string;
}
const defaults: BentoFeature[] = [
{
id: "direction",
eyebrow: "01 / A clear point of view",
title: "Know what makes you, you.",
description: "A considered identity gives every detail a place to belong.",
size: "wide",
visual: (
<div
aria-hidden="true"
className="flex items-center gap-3 overflow-hidden"
>
<span className="font-serif text-6xl tracking-[-.08em]">Aa</span>
<span className="h-px flex-1 bg-current/30" />
<span className="font-mono text-xs">FIND YOUR FORM</span>
</div>
),
},
{
id: "details",
eyebrow: "02 / Made with intention",
title: "The little things add up.",
description:
"From the first hello to the final hover, every moment feels considered.",
size: "tall",
visual: (
<div aria-hidden="true" className="flex gap-2">
{["#d9fc87", "#d2ccef", "#e8cdb5"].map((color, index) => (
<span
key={color}
style={{
backgroundColor: color,
transform: `translateY(${index * -5}px)`,
}}
className="size-10 rounded-xl"
/>
))}
</div>
),
},
{
id: "delivery",
eyebrow: "03 / Ready to go further",
title: "Beautiful, then usable.",
description:
"A smooth handoff keeps the good work moving into the real world.",
visual: (
<div
aria-hidden="true"
className="flex items-center gap-2 font-mono text-sm"
>
<span className="rounded-full border border-current/20 px-3 py-2">
Brief
</span>
<span className="text-[var(--g5-accent)]">→</span>
<span className="rounded-full border border-current/20 px-3 py-2">
Launch
</span>
</div>
),
},
{
id: "people",
eyebrow: "04 / Built around real people",
title: "A thoughtful way to move forward.",
description:
"Make a useful promise clear, then give people a confident next step.",
visual: (
<div
aria-hidden="true"
className="flex items-center gap-3 font-mono text-xs text-white/60"
>
<span className="size-8 rounded-full border border-white/20 bg-[var(--g5-accent)]/20" />
MADE FOR PEOPLE
</div>
),
},
];
export interface SpotlightBentoProps {
features?: BentoFeature[];
title?: string;
eyebrow?: string;
accent?: string;
className?: string;
}
/** Unequal content tiles with a restrained mouse spotlight and static touch fallback. */
export function SpotlightBento({
features = defaults,
title = "Good ideas, with room to breathe.",
eyebrow = "The details make it",
accent = "#d9fc87",
className = "",
}: SpotlightBentoProps) {
const id = useId();
const reducedMotion = useRef(false);
useEffect(() => {
const preference = window.matchMedia?.(
"(prefers-reduced-motion: reduce)",
);
if (!preference) return;
const update = () => {
reducedMotion.current = preference.matches;
};
update();
preference.addEventListener?.("change", update);
return () => preference.removeEventListener?.("change", update);
}, []);
return (
<section
style={{ "--g5-accent": accent } as CSSProperties}
aria-labelledby={`${id}-title`}
className={`rounded-3xl bg-[#111214] p-6 text-[#f4f4ef] sm:p-10 ${className}`}
>
<p className="font-mono text-[11px] tracking-[.12em] text-[#b4b6af] uppercase">
{eyebrow}
</p>
<h2
id={`${id}-title`}
className="mt-3 max-w-2xl text-3xl font-medium tracking-[-.05em] sm:text-4xl"
>
{title}
</h2>
{features.length === 0 ? (
<p className="mt-6 text-sm text-[#b4b6af]">
Add a feature to start your grid.
</p>
) : (
<div className="mt-8 grid auto-rows-[minmax(12rem,auto)] gap-3 sm:grid-cols-2 lg:grid-cols-3">
{features.map((feature) => {
const Tag = feature.href ? "a" : "article";
const linkProps = feature.href
? { href: sanitizeHref(feature.href) }
: {};
return (
<Tag
key={feature.id}
{...linkProps}
onPointerMove={(event: PointerEvent<HTMLElement>) => {
if (event.pointerType === "touch" || reducedMotion.current)
return;
const box = event.currentTarget.getBoundingClientRect();
event.currentTarget.style.setProperty(
"--g5-x",
`${event.clientX - box.left}px`,
);
event.currentTarget.style.setProperty(
"--g5-y",
`${event.clientY - box.top}px`,
);
}}
className={`group relative isolate flex min-w-0 flex-col justify-between overflow-hidden rounded-2xl border border-white/10 bg-[#181a1c] p-6 transition-[border-color] hover:border-white/30 focus-visible:border-[var(--g5-accent)] motion-reduce:transition-none ${
feature.href ? "focus-visible:outline-offset-4" : ""
} ${
feature.size === "wide"
? "sm:col-span-2"
: feature.size === "tall"
? "sm:row-span-2"
: ""
}`}
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none"
style={{
backgroundImage:
"radial-gradient(340px circle at var(--g5-x, 50%) var(--g5-y, 50%), color-mix(in srgb, var(--g5-accent) 12%, transparent), transparent 80%)",
}}
/>
<div>
<p className="font-mono text-[10px] text-[var(--g5-accent)] uppercase">
{feature.eyebrow}
</p>
<h3 className="mt-5 text-2xl font-medium tracking-[-.04em]">
{feature.title}
</h3>
<p className="mt-3 max-w-sm text-sm leading-7 text-[#b4b6af]">
{feature.description}
</p>
</div>
<div className="mt-8 flex min-h-12 items-end justify-between gap-4">
{feature.visual && <div>{feature.visual}</div>}
{feature.href && (
<span className="ms-auto inline-flex items-center gap-2 text-xs text-[#c2c4bd]">
{feature.linkLabel ?? "Explore"}
<span
aria-hidden="true"
className="text-[var(--g5-accent)]"
>
↗
</span>
</span>
)}
</div>
</Tag>
);
})}
</div>
)}
</section>
);
}
"use client";
import { useId, useState, type CSSProperties } from "react";
import { sanitizeHref } from "@/lib/gear5/sanitize";
export interface PricingPlan {
id: string;
name: string;
description: string;
monthlyPrice: string;
yearlyPrice: string;
monthlyNote: string;
yearlyNote: string;
features: string[];
href: string;
featured?: boolean;
}
const plansDefault: PricingPlan[] = [
{
id: "starter",
name: "Starter",
description: "A little room to explore.",
monthlyPrice: "$12",
yearlyPrice: "$9",
monthlyNote: "$12 billed each month",
yearlyNote: "$108 billed once a year",
features: ["3 active projects", "Unlimited ideas", "Community support"],
href: "#contact",
},
{
id: "studio",
name: "Studio",
description: "For your next big thing.",
monthlyPrice: "$29",
yearlyPrice: "$24",
monthlyNote: "$29 billed each month",
yearlyNote: "$288 billed once a year",
features: ["Unlimited projects", "Shared workspaces", "Priority support"],
href: "#contact",
featured: true,
},
];
export interface PricingSwitchProps {
plans?: PricingPlan[];
title?: string;
eyebrow?: string;
monthlyLabel?: string;
yearlyLabel?: string;
billingLabel?: string;
unitLabel?: string;
actionLabel?: string;
featuredLabel?: string;
accent?: string;
className?: string;
}
/** Prices and billing notes are explicit caller data; no checkout or automatic billing. */
export function PricingSwitch({
plans = plansDefault,
title = "Small start. Big possibilities.",
eyebrow = "Simple pricing",
monthlyLabel = "Monthly",
yearlyLabel = "Yearly",
billingLabel = "Billing period",
unitLabel = "/ month",
actionLabel = "Get started",
featuredLabel = "Popular",
accent = "#d9fc87",
className = "",
}: PricingSwitchProps) {
const id = useId();
const [yearly, setYearly] = useState(false);
return (
<section
style={{ "--g5-accent": accent } as CSSProperties}
className={`rounded-3xl bg-[#111214] p-6 text-[#f4f4ef] sm:p-10 ${className}`}
>
<p className="font-mono text-[11px] tracking-[.12em] text-[#b4b6af] uppercase">
{eyebrow}
</p>
<h2 className="mt-3 text-3xl font-medium tracking-[-.05em] sm:text-4xl">
{title}
</h2>
<fieldset className="mt-7 flex w-fit gap-1 rounded-full border border-white/20 p-1">
<legend className="sr-only">{billingLabel}</legend>
{[
[false, monthlyLabel],
[true, yearlyLabel],
].map(([value, label]) => (
<label
key={String(value)}
className={`relative cursor-pointer rounded-full px-5 py-2.5 text-sm transition-colors focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-white motion-reduce:transition-none ${yearly === value ? "bg-[var(--g5-accent)] text-[#111214]" : "text-[#c2c4bd]"}`}
>
<input
type="radio"
name={`${id}-billing`}
checked={yearly === value}
onChange={() => setYearly(Boolean(value))}
className="sr-only"
/>
{label}
</label>
))}
</fieldset>
<div className="mt-7 grid gap-4 sm:grid-cols-2">
{plans.map((plan) => (
<article
key={plan.id}
className={`relative flex flex-col rounded-2xl border p-6 ${plan.featured ? "border-[var(--g5-accent)] bg-[#1b2017]" : "border-white/15 bg-[#181a1c]"}`}
>
<div className="flex items-center justify-between gap-3">
<h3 className="text-lg font-medium">{plan.name}</h3>
{plan.featured && (
<span className="rounded-full bg-[var(--g5-accent)] px-3 py-1 text-[10px] font-semibold text-[#111214]">
{featuredLabel}
</span>
)}
</div>
<p className="mt-2 text-sm text-[#b4b6af]">{plan.description}</p>
<div aria-live="polite" aria-atomic="true" className="my-7">
<p className="text-sm text-[#b4b6af]">
<span className="me-2 text-5xl font-medium tracking-[-.06em] text-[#f4f4ef]">
{yearly ? plan.yearlyPrice : plan.monthlyPrice}
</span>
{unitLabel}
</p>
<p className="mt-3 text-xs text-[#b4b6af]">
{yearly ? plan.yearlyNote : plan.monthlyNote}
</p>
</div>
<ul className="mb-7 space-y-3 text-sm text-[#d5d6cf]">
{plan.features.map((feature) => (
<li key={feature} className="flex gap-3">
<span aria-hidden="true" className="text-[var(--g5-accent)]">
✓
</span>
{feature}
</li>
))}
</ul>
<a
href={sanitizeHref(plan.href)}
className={`mt-auto flex min-h-11 items-center justify-between rounded-full px-5 py-3 text-sm font-medium transition-opacity hover:opacity-80 motion-reduce:transition-none ${plan.featured ? "bg-[var(--g5-accent)] text-[#111214]" : "border border-white/20 text-[#f4f4ef]"}`}
>
{actionLabel}
<span aria-hidden="true">↗</span>
</a>
</article>
))}
</div>
</section>
);
}
"use client";
import { useState, type CSSProperties } from "react";
export interface DeckTestimonial {
id: string;
quote: string;
name: string;
role: string;
}
export const deckTestimonials: DeckTestimonial[] = [
{
id: "a",
quote:
"The rare kind of collaborator who makes the idea better, then makes it happen.",
name: "Jamie Chen",
role: "Founder, Forma · Example testimonial",
},
{
id: "b",
quote:
"Every interaction feels considered. Our product finally looks as good as it works.",
name: "Morgan Lee",
role: "Product lead, Mono · Example testimonial",
},
{
id: "c",
quote:
"From the first conversation to the last detail, the whole process was a joy.",
name: "Sam Rivera",
role: "Editor, Terrain · Example testimonial",
},
];
export interface TestimonialDeckProps {
testimonials?: DeckTestimonial[];
title?: string;
eyebrow?: string;
previousLabel?: string;
nextLabel?: string;
positionLabel?: (current: number, total: number) => string;
accent?: string;
className?: string;
}
/** Manual navigation; nothing auto-plays or moves focus when the quote changes. */
export function TestimonialDeck({
testimonials = deckTestimonials,
title = "Good work. Better company.",
eyebrow = "Kind words",
previousLabel = "Previous testimonial",
nextLabel = "Next testimonial",
positionLabel = (current, total) => `${current} / ${total}`,
accent = "#d9fc87",
className = "",
}: TestimonialDeckProps) {
const [selected, setSelected] = useState(0);
const active = Math.min(selected, Math.max(0, testimonials.length - 1));
const item = testimonials[active];
return (
<section
style={{ "--g5-accent": accent } as CSSProperties}
className={`rounded-3xl bg-[#111214] p-6 text-[#f4f4ef] sm:p-10 ${className}`}
>
<p className="font-mono text-[11px] tracking-[.12em] text-[#b4b6af] uppercase">
{eyebrow}
</p>
<h2 className="mt-3 text-3xl font-medium tracking-[-.05em] sm:text-4xl">
{title}
</h2>
{item && (
<>
<div
className="relative mt-8 min-h-64 rounded-2xl border border-white/15 bg-[#181a1c] p-6 transition-transform duration-300 motion-safe:hover:-translate-y-1 motion-reduce:transition-none sm:p-8"
aria-live="polite"
aria-atomic="true"
>
<span
aria-hidden="true"
className="font-serif text-6xl leading-none text-[var(--g5-accent)]"
>
“
</span>
<blockquote className="mt-2 max-w-2xl text-xl leading-relaxed tracking-[-.02em] sm:text-2xl">
{item.quote}
</blockquote>
<div className="mt-7 flex items-center gap-3">
<span
aria-hidden="true"
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[var(--g5-accent)] text-sm font-medium text-[#111214]"
>
{item.name.slice(0, 1)}
</span>
<div>
<p className="text-sm font-medium">{item.name}</p>
<p className="mt-1 text-xs text-[#b4b6af]">{item.role}</p>
</div>
</div>
</div>
<div className="mt-5 flex items-center justify-between">
<span className="font-mono text-xs text-[#b4b6af]">
{positionLabel(active + 1, testimonials.length)}
</span>
<div className="flex gap-2">
<button
type="button"
aria-label={previousLabel}
disabled={testimonials.length < 2}
onClick={() =>
setSelected(
(active - 1 + testimonials.length) % testimonials.length,
)
}
className="flex size-11 items-center justify-center rounded-full border border-white/20 hover:bg-white/10 disabled:opacity-40"
>
←
</button>
<button
type="button"
aria-label={nextLabel}
disabled={testimonials.length < 2}
onClick={() => setSelected((active + 1) % testimonials.length)}
className="flex size-11 items-center justify-center rounded-full border border-white/20 hover:bg-white/10 disabled:opacity-40"
>
→
</button>
</div>
</div>
</>
)}
</section>
);
}