# Folio: designer portfolio

> A bold five-page portfolio: a huge intro with inline colour pills, a work grid that reveals each result on hover and focus, an about bento, a case study with a sticky fact sheet and contents list, writing and a uses page.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/template-portfolio).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/template-portfolio
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/template-portfolio-light-2036b4e96dc27ae4.png
- Registry item: https://ui.ballmac.com/r/template-portfolio.json
- Categories: templates
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://ui.ballmac.com/r/template-portfolio.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://ui.ballmac.com/r/blur-fade.json`, `https://ui.ballmac.com/r/copy-button.json`, `https://ui.ballmac.com/r/marquee.json`, `https://ui.ballmac.com/r/table-of-contents.json`, `https://ui.ballmac.com/r/media.json`

## Notes from the registry

Pages are at /portfolio, /portfolio/work, /portfolio/work/[slug], /portfolio/writing and /portfolio/uses. Edit your projects, posts and kit in portfolio-data.ts; the palette is the portfolioCss string in portfolio-theme.tsx.

## Files

### `registry/ballmac/components/templates/portfolio/portfolio-fonts.ts`

```ts
// Ballmac UI: Portfolio template fonts. https://ui.ballmac.com/templates/template-portfolio
import { Bricolage_Grotesque, Geist_Mono } from "next/font/google"

export const portfolioSans = Bricolage_Grotesque({ variable: "--portfolio-sans", subsets: ["latin"], axes: ["opsz", "wdth"], display: "swap", preload: false })
export const portfolioMono = Geist_Mono({ variable: "--portfolio-mono", subsets: ["latin"], display: "swap", preload: false })
```

### `registry/ballmac/components/templates/portfolio/portfolio-data.ts`

```ts
import type { MediaSource } from "@/components/ballmac/media"

// Ballmac UI: Portfolio sample content. https://ui.ballmac.com/templates/template-portfolio

export type PortfolioProject = {
  slug: string
  title: string
  client: string
  year: string
  discipline: "Product" | "Brand" | "Systems"
  blurb: string
  result: string
  tags: string[]
  /** Which painted cover to draw (0 to 5). */
  cover: number
  /** Your own image instead of the painted art: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. */
  image?: MediaSource
  /** Describes `image` when it is a plain URL. */
  imageAlt?: string
}

export const projects: PortfolioProject[] = [
  { slug: "fernhill", title: "Rebuilding onboarding", client: "Fernhill", year: "2026", discipline: "Product", blurb: "A first-run flow that gets teams to their first invoice in one sitting.", result: "+34% activation", tags: ["Product design", "Research", "Prototyping"], cover: 0 },
  { slug: "lumen", title: "A design system in six weeks", client: "Lumen Health", year: "2025", discipline: "Systems", blurb: "Tokens, 60 components and the docs that made engineers actually use them.", result: "-40% handoff time", tags: ["Design systems", "Documentation"], cover: 1 },
  { slug: "paddock", title: "Identity for a riding school", client: "Paddock", year: "2025", discipline: "Brand", blurb: "A mark, a typeface pairing and a handbook for a school that outgrew its logo.", result: "3 offices, 1 look", tags: ["Brand", "Print", "Wayfinding"], cover: 2 },
  { slug: "halcyon", title: "A calmer banking app", client: "Halcyon", year: "2024", discipline: "Product", blurb: "Spending insights that read like a sentence, not a spreadsheet.", result: "4.8 App Store rating", tags: ["Mobile", "Data visualisation"], cover: 3 },
  { slug: "tessera", title: "Pricing page, tested", client: "Tessera", year: "2024", discipline: "Product", blurb: "Six experiments, one clear winner and a page the sales team stopped apologising for.", result: "+22% trial starts", tags: ["Experimentation", "Copy"], cover: 4 },
  { slug: "quanta", title: "Tokens that travel", client: "Quanta", year: "2023", discipline: "Systems", blurb: "A theming layer that let one product ship in nine brands.", result: "9 brands, 1 codebase", tags: ["Design systems", "Tokens"], cover: 5 },
]

export type PortfolioPost = { slug: string; title: string; date: string; read: number; tag: "Process" | "Craft" | "Career"; excerpt: string }

export const posts: PortfolioPost[] = [
  { slug: "p1", title: "The first-run checklist nobody wants to ship", date: "2026-08-14", read: 7, tag: "Process", excerpt: "Why the thing that tests best is usually the thing that annoys the team." },
  { slug: "p2", title: "Stop designing empty states last", date: "2026-05-02", read: 5, tag: "Craft", excerpt: "Empty is the first thing every new user sees. Treat it that way." },
  { slug: "p3", title: "What I learned from deleting 40 components", date: "2026-02-19", read: 9, tag: "Process", excerpt: "A design system is a garden. A small, tidy one beats a sprawling one." },
  { slug: "p4", title: "Going independent after nine years", date: "2025-11-07", read: 6, tag: "Career", excerpt: "The money, the loneliness and the three clients that made it work." },
  { slug: "p5", title: "A defence of the boring button", date: "2025-06-23", read: 4, tag: "Craft", excerpt: "Novelty has a cost. Spend it where it pays." },
  { slug: "p6", title: "How I run a design review", date: "2025-03-11", read: 8, tag: "Process", excerpt: "Four questions, one hour and no one defending their pixels." },
]

export const uses: { group: string; items: { name: string; note: string }[] }[] = [
  { group: "Hardware", items: [{ name: "MacBook Pro 14″", note: "M3 Pro, 36 GB. Mostly plugged into a desk shelf in Lisbon." }, { name: "Studio Display", note: "The only monitor I have trusted for colour." }, { name: "Keychron Q1", note: "Brown switches. Loud enough that my partner has opinions." }, { name: "iPad Pro + Pencil", note: "Sketches, critique markup and reading." }] },
  { group: "Software", items: [{ name: "Figma", note: "Design, prototypes and the occasional diagram." }, { name: "Linear", note: "Where projects and promises go to be counted." }, { name: "Things", note: "Personal tasks. Never work tasks." }, { name: "Raycast", note: "Launcher, clipboard history and window layouts." }] },
  { group: "Type", items: [{ name: "Bricolage Grotesque", note: "This site, because I wanted something with a personality." }, { name: "Geist Mono", note: "Small labels and code." }, { name: "Söhne", note: "For client work where budget allows." }] },
  { group: "Reading", items: [{ name: "Refactoring UI", note: "Still the best short book on interface design." }, { name: "The Design of Everyday Things", note: "Reread every two years." }, { name: "Field notebooks", note: "Paper, always. Three at a time." }] },
]
```

### `registry/ballmac/components/templates/portfolio/portfolio-theme.tsx`

```tsx
// Ballmac UI: Portfolio template shell. https://ui.ballmac.com/templates/template-portfolio
"use client"

import * as React from "react"
import { ArrowUpRight, Menu, X } from "lucide-react"

import { CopyButton } from "@/components/ballmac/copy-button"
import { portfolioMono, portfolioSans } from "@/components/ballmac/templates/portfolio/portfolio-fonts"
import { cn } from "@/lib/utils"
import { Media, type MediaSource } from "@/components/ballmac/media"

type PortfolioPage = "home" | "work" | "case" | "writing" | "uses"
type PortfolioHrefs = Record<PortfolioPage, string>

const defaultHrefs: PortfolioHrefs = { home: "/portfolio", work: "/portfolio/work", case: "/portfolio/work/fernhill", writing: "/portfolio/writing", uses: "/portfolio/uses" }

/** Portfolio's palette: gallery white and ink with one acid lime. In dark mode the lime becomes the light on a black wall. */
const portfolioCss = `
.portfolio-theme,body:not(:has([data-gallery])):has(.portfolio-theme){--background:oklch(0.988 0.003 100);--foreground:oklch(0.16 0.01 100);--card:oklch(1 0 0);--card-foreground:oklch(0.16 0.01 100);--popover:oklch(1 0 0);--popover-foreground:oklch(0.16 0.01 100);--primary:oklch(0.16 0.01 100);--primary-foreground:oklch(0.988 0.003 100);--secondary:oklch(0.958 0.008 100);--secondary-foreground:oklch(0.16 0.01 100);--muted:oklch(0.958 0.008 100);--muted-foreground:oklch(0.46 0.012 100);--accent:oklch(0.94 0.014 110);--accent-foreground:oklch(0.16 0.01 100);--border:oklch(0.16 0.01 100 / 14%);--input:oklch(0.16 0.01 100 / 20%);--ring:oklch(0.16 0.01 100);--surface:oklch(0.968 0.006 100);--destructive:oklch(0.54 0.22 28);--chart-1:oklch(0.9 0.19 125);--chart-2:oklch(0.72 0.17 38);--chart-3:oklch(0.78 0.12 235);--chart-4:oklch(0.76 0.13 300);--chart-5:oklch(0.86 0.09 75);--portfolio-on-accent:oklch(0.16 0.01 100);--radius:1rem}
.dark .portfolio-theme,.dark body:not(:has([data-gallery])):has(.portfolio-theme){--background:oklch(0.14 0.004 100);--foreground:oklch(0.96 0.006 100);--card:oklch(0.18 0.005 100);--card-foreground:oklch(0.96 0.006 100);--popover:oklch(0.2 0.006 100);--popover-foreground:oklch(0.96 0.006 100);--primary:oklch(0.96 0.006 100);--primary-foreground:oklch(0.16 0.01 100);--secondary:oklch(0.23 0.006 100);--secondary-foreground:oklch(0.96 0.006 100);--muted:oklch(0.22 0.006 100);--muted-foreground:oklch(0.72 0.012 100);--accent:oklch(0.26 0.01 110);--accent-foreground:oklch(0.96 0.006 100);--border:oklch(1 0 0 / 12%);--input:oklch(1 0 0 / 16%);--ring:oklch(0.9 0.19 125);--surface:oklch(0.165 0.005 100);--destructive:oklch(0.7 0.19 28);--chart-1:oklch(0.9 0.19 125);--chart-2:oklch(0.72 0.17 38);--chart-3:oklch(0.76 0.12 235);--chart-4:oklch(0.74 0.13 300);--chart-5:oklch(0.84 0.09 75);--portfolio-on-accent:oklch(0.16 0.01 100)}
body:not(:has([data-gallery])):has(.portfolio-theme){font-family:var(--portfolio-sans),ui-sans-serif,system-ui,sans-serif}
`

/** A painted cover: abstract shapes in the chart colours standing in for a screenshot. */
function Cover({ variant, className, image, imageAlt }: { variant: number; className?: string; image?: MediaSource; imageAlt?: string }) {
  const v = ((variant % 6) + 6) % 6
  if (image) return <Media media={image} alt={imageAlt} fill className={cn("aspect-[4/3] w-full", className)} />
  return (
    <div aria-hidden="true" className={cn("relative isolate aspect-[4/3] w-full overflow-hidden", className)}>
      {v === 0 && (<><div className="bg-chart-5 absolute inset-0" /><div className="bg-card absolute inset-x-[12%] top-[14%] bottom-0 rounded-t-2xl shadow-xl"><div className="bg-chart-1 m-[7%] h-[14%] w-[40%] rounded-md" /><div className="bg-muted mx-[7%] h-[8%] w-[70%] rounded" /><div className="bg-muted mx-[7%] mt-[3%] h-[8%] w-[54%] rounded" /><div className="mx-[7%] mt-[7%] grid grid-cols-3 gap-[3%]">{[0, 1, 2].map((i) => <div key={i} className="bg-secondary aspect-square rounded-lg" />)}</div></div></>)}
      {v === 1 && (<><div className="bg-foreground absolute inset-0" /><div className="absolute inset-[10%] grid grid-cols-4 gap-[4%]">{[chart(1), chart(2), chart(3), chart(4), chart(5), chart(1), chart(3), chart(2)].map((c, i) => <div key={i} className={cn("rounded-xl", c)} />)}</div></>)}
      {v === 2 && (<><div className="bg-chart-2 absolute inset-0" /><div className="absolute top-1/2 left-1/2 aspect-square w-[46%] -translate-x-1/2 -translate-y-1/2"><div className="bg-primary-foreground size-full rounded-full" /><div className="bg-foreground absolute inset-y-0 start-0 w-1/2 rounded-s-full" /></div></>)}
      {v === 3 && (<><div className="bg-chart-3 absolute inset-0" /><div className="bg-foreground absolute top-[10%] bottom-[-8%] left-1/2 w-[38%] -translate-x-1/2 rounded-[2rem] p-[3%]"><div className="bg-card h-full rounded-[1.4rem] p-[8%]"><div className="bg-chart-1 h-[10%] w-[60%] rounded" /><div className="mt-[10%] flex h-[34%] items-end gap-[6%]">{[40, 70, 55, 90, 65].map((h, i) => <div key={i} className="bg-foreground flex-1 rounded-t" style={{ height: `${h}%` }} />)}</div></div></div></>)}
      {v === 4 && (<><div className="bg-chart-4 absolute inset-0" /><div className="absolute inset-[12%] grid grid-cols-3 gap-[4%]">{[0, 1, 2].map((i) => <div key={i} className={cn("bg-card rounded-2xl p-[10%]", i === 1 && "bg-foreground -translate-y-[6%]")}><div className={cn("h-[10%] w-[60%] rounded", i === 1 ? "bg-chart-1" : "bg-muted")} /><div className={cn("mt-[18%] h-[22%] w-[50%] rounded", i === 1 ? "bg-primary-foreground" : "bg-secondary")} /></div>)}</div></>)}
      {v === 5 && (<><div className="bg-secondary absolute inset-0" />{[0, 1, 2, 3, 4].map((i) => <div key={i} className={cn("absolute w-[34%] aspect-square rounded-full mix-blend-multiply", chart((i % 5) + 1))} style={{ left: `${8 + i * 14}%`, top: `${i % 2 ? 40 : 16}%` }} />)}</>)}
    </div>
  )
}
function chart(n: number) {
  return ["bg-chart-1", "bg-chart-2", "bg-chart-3", "bg-chart-4", "bg-chart-5"][(n - 1) % 5]
}

const links: { key: PortfolioPage; label: string }[] = [
  { key: "work", label: "Work" },
  { key: "writing", label: "Writing" },
  { key: "uses", label: "Uses" },
]

type PortfolioShellProps = React.ComponentProps<"div"> & {
  /** The page being shown, so its nav link is marked current. */
  page: PortfolioPage
  /** Override where pages live (used by previews). */
  hrefs?: Partial<PortfolioHrefs>
}

/** Portfolio's frame: a wordmark with an availability chip, a quiet nav and a huge email in the footer. */
function PortfolioShell({ page, hrefs: overrides, className, style, children, ...props }: PortfolioShellProps) {
  const hrefs = { ...defaultHrefs, ...overrides }
  const [open, setOpen] = React.useState(false)
  React.useEffect(() => {
    const classes = [portfolioSans.variable, portfolioMono.variable].filter(Boolean)
    document.body.classList.add(...classes)
    return () => document.body.classList.remove(...classes)
  }, [])
  const active = page === "case" ? "work" : page
  return (
    <div
      data-slot="portfolio"
      className={cn("portfolio-theme bg-background text-foreground relative min-h-dvh overflow-x-clip", portfolioSans.variable, portfolioMono.variable, className)}
      style={{ fontFamily: "var(--portfolio-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
      {...props}
    >
      <style>{portfolioCss}</style>
      <header className="bg-background/80 sticky top-0 z-40 backdrop-blur-xl">
        <div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-8">
          <a href={hrefs.home} className="focus-visible:ring-ring/50 flex items-center gap-3 rounded-lg outline-none focus-visible:ring-[3px]">
            <span className="bg-chart-1 text-[var(--portfolio-on-accent)] flex size-9 items-center justify-center rounded-full text-sm font-bold tracking-tight" aria-hidden="true">IC</span>
            <span className="font-semibold tracking-tight">Ines Calder</span>
          </a>
          <span className="text-muted-foreground hidden items-center gap-2 text-xs lg:flex" style={{ fontFamily: "var(--portfolio-mono)" }}>
            <span className="relative flex size-2"><span className="bg-chart-2 absolute inset-0 rounded-full" /><span className="bg-chart-2 absolute inset-0 rounded-full opacity-60 motion-safe:animate-ping" /></span>
            Booking Q1 2027 · Lisbon
          </span>
          <nav aria-label="Main" className="hidden items-center gap-1 md:flex">
            {links.map((l) => (
              <a key={l.key} href={hrefs[l.key]} aria-current={active === l.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground aria-[current=page]:text-foreground aria-[current=page]:bg-accent focus-visible:ring-ring/50 rounded-full px-4 py-2 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]">
                {l.label}
              </a>
            ))}
            <a href="#contact" className="bg-foreground text-background focus-visible:ring-ring/50 ms-2 inline-flex h-10 items-center gap-1.5 rounded-full px-5 text-sm font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Say hello <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
          </nav>
          <button type="button" aria-label={open ? "Close menu" : "Open menu"} aria-expanded={open} aria-controls="portfolio-mobile-menu" onClick={() => setOpen((v) => !v)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-full outline-none focus-visible:ring-[3px] md:hidden">
            {open ? <X className="size-5" aria-hidden="true" /> : <Menu className="size-5" aria-hidden="true" />}
          </button>
        </div>
        {open && (
          <nav id="portfolio-mobile-menu" aria-label="Mobile" className="px-4 pb-4 md:hidden">
            {[{ key: "home" as const, label: "Home" }, ...links].map((l) => <a key={l.key} href={hrefs[l.key]} className="hover:bg-accent block rounded-2xl px-4 py-3 text-lg font-medium">{l.label}</a>)}
          </nav>
        )}
      </header>
      {children}
      <footer id="contact" className="mt-24 border-t">
        <div className="mx-auto max-w-7xl px-4 pt-16 pb-10 sm:px-8">
          <p className="text-muted-foreground text-sm" style={{ fontFamily: "var(--portfolio-mono)" }}>Have a product that needs care?</p>
          <div className="mt-4 flex flex-wrap items-center gap-3">
            <a href="mailto:hello@inescalder.example" className="focus-visible:ring-ring/50 rounded-xl text-[clamp(2rem,7.4vw,6.5rem)] leading-none font-semibold tracking-[-0.045em] break-all outline-none hover:underline hover:decoration-[0.08em] hover:underline-offset-[0.12em] focus-visible:ring-[3px]">hello@inescalder.example</a>
            <CopyButton value="hello@inescalder.example" ariaLabel="Copy email address" className="size-11" />
          </div>
          <div className="text-muted-foreground mt-14 flex flex-wrap items-center justify-between gap-4 border-t pt-6 text-sm">
            <p>© 2026 Ines Calder. Designed and built in Lisbon.</p>
            <ul className="flex gap-5">{["LinkedIn", "Read.cv", "Are.na", "RSS"].map((l) => <li key={l}><a href="#" className="hover:text-foreground transition-colors">{l}</a></li>)}</ul>
          </div>
        </div>
      </footer>
    </div>
  )
}

export { Cover, PortfolioShell, defaultHrefs as portfolioDefaultHrefs, type PortfolioHrefs, type PortfolioPage, type PortfolioShellProps }
```

### `registry/ballmac/components/templates/portfolio/portfolio-home.tsx`

```tsx
// Ballmac UI: Portfolio home page. https://ui.ballmac.com/templates/template-portfolio
"use client"

import * as React from "react"
import { ArrowDownRight, ArrowUpRight, BookOpen, Headphones, Hammer, MapPin } from "lucide-react"

import { BlurFade } from "@/components/ballmac/blur-fade"
import { Marquee } from "@/components/ballmac/marquee"
import { posts, projects } from "@/components/ballmac/templates/portfolio/portfolio-data"
import { Cover, PortfolioShell, type PortfolioHrefs } from "@/components/ballmac/templates/portfolio/portfolio-theme"
import { cn } from "@/lib/utils"

const mono = { fontFamily: "var(--portfolio-mono)" } as const

const quotes = [
  ["Ines made the hard thing feel obvious. Our activation rate said so.", "Sasha Petrova, Fernhill"],
  ["The calmest designer I have ever worked with, and the sharpest.", "Marcus Lindqvist, Halcyon"],
  ["She cut our component library in half and everyone thanked her.", "Priya Raman, Lumen Health"],
  ["We hired Ines for a logo and got a company worth working for.", "Tom Beltrán, Paddock"],
]

function Pill({ className, children }: { className?: string; children?: React.ReactNode }) {
  return <span aria-hidden="true" className={cn("mx-1 inline-block h-[0.72em] w-[1.6em] rounded-full align-[-0.06em] sm:mx-2", className)}>{children}</span>
}

type PortfolioHomeProps = React.ComponentProps<"div"> & { hrefs?: Partial<PortfolioHrefs> }

/** The Portfolio home page: a bold intro, a work grid whose cards reveal a result on hover and focus, an about bento, kind words and recent writing. */
function PortfolioHome({ hrefs, ...props }: PortfolioHomeProps) {
  const h = { work: hrefs?.work ?? "/portfolio/work", case: hrefs?.case ?? "/portfolio/work/fernhill", writing: hrefs?.writing ?? "/portfolio/writing" }
  return (
    <PortfolioShell page="home" hrefs={hrefs} {...props}>
      <main>
        <section className="mx-auto max-w-7xl px-4 pt-14 pb-10 sm:px-8 sm:pt-24">
          <BlurFade>
            <p className="text-muted-foreground text-sm" style={mono}>Independent product designer · 9 years · Lisbon</p>
            <h1 className="mt-6 max-w-6xl text-[clamp(2.6rem,8.6vw,8rem)] leading-[0.94] font-semibold tracking-[-0.05em] text-balance">
              I design products people <Pill className="bg-chart-1" />finish <Pill className="bg-chart-2" />using, and teams <Pill className="bg-chart-3" />enjoy shipping.
            </h1>
            <div className="mt-10 flex flex-wrap items-center gap-3">
              <a href="#work" className="bg-foreground text-background focus-visible:ring-ring/50 inline-flex h-12 items-center gap-2 rounded-full px-6 font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Selected work <ArrowDownRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
              <a href={h.writing} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex h-12 items-center rounded-full border px-6 font-semibold outline-none transition-colors focus-visible:ring-[3px]">Read my writing</a>
            </div>
          </BlurFade>
        </section>

        <section aria-label="Skills" className="border-y py-5">
          <Marquee speed={40} gap={40} pauseOnHover>
            {["Product design", "Design systems", "Prototyping", "Research", "Brand", "Workshops", "Art direction", "Front-end"].map((s) => <span key={s} className="text-muted-foreground flex items-center gap-10 text-2xl font-medium tracking-tight sm:text-3xl">{s}<span className="bg-chart-1 size-2.5 rounded-full" aria-hidden="true" /></span>)}
          </Marquee>
        </section>

        <section id="work" aria-labelledby="pf-work" className="mx-auto max-w-7xl scroll-mt-20 px-4 py-20 sm:px-8">
          <div className="flex items-end justify-between gap-4">
            <h2 id="pf-work" className="text-4xl font-semibold tracking-[-0.04em] sm:text-6xl">Selected work</h2>
            <a href={h.work} className="focus-visible:ring-ring/50 inline-flex items-center gap-1.5 rounded-full text-sm font-semibold underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]">All projects <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
          </div>
          <ul className="mt-10 grid gap-x-6 gap-y-12 sm:grid-cols-2">
            {projects.slice(0, 4).map((p, i) => (
              <li key={p.slug} className={cn(i % 2 === 1 && "sm:mt-16")}>
                <a href={h.case} className="group focus-visible:ring-ring/50 block rounded-3xl outline-none focus-visible:ring-[3px] focus-visible:ring-offset-4 focus-visible:ring-offset-background">
                  <div className="relative overflow-hidden rounded-3xl border">
                    <Cover variant={p.cover} image={p.image} imageAlt={p.imageAlt} className="transition-transform duration-700 ease-out group-hover:scale-[1.04] group-focus-visible:scale-[1.04] motion-reduce:transition-none" />
                    <div className="bg-chart-1 text-[var(--portfolio-on-accent)] absolute end-4 bottom-4 flex translate-y-3 items-center gap-2 rounded-full px-4 py-2 text-sm font-bold opacity-0 shadow-lg transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 motion-reduce:transition-none">
                      {p.result} <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" />
                    </div>
                  </div>
                  <div className="mt-5 flex items-start justify-between gap-4">
                    <div>
                      <h3 className="text-2xl font-semibold tracking-[-0.03em] text-balance">{p.title}</h3>
                      <p className="text-muted-foreground mt-1 text-pretty">{p.blurb}</p>
                    </div>
                    <p className="text-muted-foreground shrink-0 text-sm" style={mono}>{p.client}<br />{p.year}</p>
                  </div>
                  <p className="sr-only">Result: {p.result}</p>
                </a>
              </li>
            ))}
          </ul>
        </section>

        <section aria-labelledby="pf-about" className="mx-auto max-w-7xl px-4 pb-20 sm:px-8">
          <h2 id="pf-about" className="sr-only">About</h2>
          <div className="grid gap-4 md:grid-cols-4 md:grid-rows-2">
            <div className="bg-chart-1 text-[var(--portfolio-on-accent)] relative flex min-h-72 flex-col justify-end overflow-hidden rounded-3xl p-7 md:col-span-2 md:row-span-2">
              <div aria-hidden="true" className="absolute -top-8 -end-8 size-56 rounded-full border-[28px] border-current opacity-15" />
              <div aria-hidden="true" className="absolute top-6 start-6 flex size-24 items-center justify-center rounded-full bg-primary text-4xl font-semibold text-background">IC</div>
              <p className="text-3xl leading-tight font-semibold tracking-[-0.03em] text-balance sm:text-4xl">Hi, I’m Ines. I turn tangled products into ones that feel obvious.</p>
              <p className="mt-3 max-w-md text-pretty opacity-80">Nine years across fintech, health and developer tools. Before going independent I led design at Fernhill and Lumen Health.</p>
            </div>
            <div className="bg-card rounded-3xl border p-6">
              <p className="text-muted-foreground flex items-center gap-2 text-xs" style={mono}><MapPin className="size-3.5" aria-hidden="true" />Based in</p>
              <p className="mt-3 text-3xl font-semibold tracking-[-0.03em]">Lisbon</p>
              <p className="text-muted-foreground mt-1 text-sm">Working across Europe and the US East Coast.</p>
            </div>
            <div className="bg-foreground text-background rounded-3xl p-6">
              <dl className="space-y-5">
                <div><dd className="text-5xl font-semibold tracking-[-0.04em] tabular-nums">9</dd><dt className="text-sm opacity-75">years designing</dt></div>
                <div><dd className="text-5xl font-semibold tracking-[-0.04em] tabular-nums">40+</dd><dt className="text-sm opacity-75">products launched</dt></div>
              </dl>
            </div>
            <div className="bg-card rounded-3xl border p-6 md:col-span-2">
              <p className="text-muted-foreground text-xs" style={mono}>Right now</p>
              <ul className="mt-4 grid gap-4 sm:grid-cols-3">
                {[[Hammer, "Building", "A pricing experiment kit for Tessera"], [BookOpen, "Reading", "The Timeless Way of Building"], [Headphones, "Listening", "Fela Kuti, on repeat"]].map(([Icon, k, v]) => {
                  const I = Icon as typeof Hammer
                  return <li key={k as string}><I className="size-5" aria-hidden="true" /><p className="mt-3 text-sm font-semibold">{k as string}</p><p className="text-muted-foreground text-sm text-pretty">{v as string}</p></li>
                })}
              </ul>
            </div>
          </div>
        </section>

        <section aria-label="Kind words" className="border-y py-10">
          <Marquee speed={32} gap={48} reverse pauseOnHover>
            {quotes.map(([q, who]) => (
              <figure key={who} className="w-[26rem] max-w-[80vw]">
                <blockquote className="text-xl leading-snug font-medium tracking-[-0.02em] text-balance">“{q}”</blockquote>
                <figcaption className="text-muted-foreground mt-3 text-sm" style={mono}>{who}</figcaption>
              </figure>
            ))}
          </Marquee>
        </section>

        <section aria-labelledby="pf-writing" className="mx-auto max-w-7xl px-4 pt-20 sm:px-8">
          <div className="flex items-end justify-between gap-4">
            <h2 id="pf-writing" className="text-4xl font-semibold tracking-[-0.04em] sm:text-5xl">Recent writing</h2>
            <a href={h.writing} className="focus-visible:ring-ring/50 inline-flex items-center gap-1.5 rounded-full text-sm font-semibold underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]">All posts <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
          </div>
          <ul className="mt-8 divide-y border-y">
            {posts.slice(0, 3).map((p) => (
              <li key={p.slug}>
                <a href={h.writing} className="hover:bg-accent/60 focus-visible:ring-ring/50 group -mx-3 grid grid-cols-[1fr_auto] items-baseline gap-4 rounded-2xl px-3 py-5 outline-none transition-colors focus-visible:ring-[3px] sm:grid-cols-[1fr_8rem_6rem]">
                  <span className="text-xl font-semibold tracking-[-0.02em] text-balance sm:text-2xl">{p.title}</span>
                  <span className="text-muted-foreground hidden text-sm sm:block" style={mono}>{p.tag}</span>
                  <span className="text-muted-foreground text-end text-sm tabular-nums" style={mono}>{p.read} min</span>
                </a>
              </li>
            ))}
          </ul>
        </section>
      </main>
    </PortfolioShell>
  )
}

export { PortfolioHome, type PortfolioHomeProps }
```

### `registry/ballmac/components/templates/portfolio/portfolio-work.tsx`

```tsx
// Ballmac UI: Portfolio work page. https://ui.ballmac.com/templates/template-portfolio
"use client"

import * as React from "react"
import { ArrowUpRight } from "lucide-react"

import { projects } from "@/components/ballmac/templates/portfolio/portfolio-data"
import { Cover, PortfolioShell, type PortfolioHrefs } from "@/components/ballmac/templates/portfolio/portfolio-theme"
import { cn } from "@/lib/utils"

type Filter = "All" | "Product" | "Brand" | "Systems"
const filters: Filter[] = ["All", "Product", "Systems", "Brand"]

type PortfolioWorkProps = React.ComponentProps<"div"> & { hrefs?: Partial<PortfolioHrefs> }

/** The Portfolio work page: every project, filtered by discipline, in a staggered grid with a result on hover. */
function PortfolioWork({ hrefs, ...props }: PortfolioWorkProps) {
  const [filter, setFilter] = React.useState<Filter>("All")
  const shown = projects.filter((p) => filter === "All" || p.discipline === filter)
  const caseHref = hrefs?.case ?? "/portfolio/work/fernhill"
  return (
    <PortfolioShell page="work" hrefs={hrefs} {...props}>
      <main className="mx-auto max-w-7xl px-4 pt-14 sm:px-8 sm:pt-20">
        <p className="text-muted-foreground text-sm" style={{ fontFamily: "var(--portfolio-mono)" }}>Work · 2023 to 2026</p>
        <h1 className="mt-4 max-w-4xl text-[clamp(2.6rem,7vw,6rem)] leading-[0.95] font-semibold tracking-[-0.05em] text-balance">Six projects I’m proud of, and what they changed.</h1>
        <div role="group" aria-label="Filter by discipline" className="mt-10 flex flex-wrap gap-2">
          {filters.map((f) => {
            const n = f === "All" ? projects.length : projects.filter((p) => p.discipline === f).length
            return (
              <button key={f} type="button" aria-pressed={filter === f} onClick={() => setFilter(f)} className={cn("focus-visible:ring-ring/50 h-11 rounded-full border px-5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", filter === f ? "bg-foreground text-background border-transparent" : "hover:bg-accent")}>
                {f} <span className={cn("ms-1 tabular-nums", filter === f ? "opacity-70" : "text-muted-foreground")}>{n}</span>
              </button>
            )
          })}
        </div>
        <p className="sr-only" role="status">{shown.length} projects shown</p>
        <ul className="mt-12 grid gap-x-6 gap-y-14 sm:grid-cols-2 lg:grid-cols-3">
          {shown.map((p) => (
            <li key={p.slug}>
              <a href={caseHref} className="group focus-visible:ring-ring/50 block rounded-3xl outline-none focus-visible:ring-[3px] focus-visible:ring-offset-4 focus-visible:ring-offset-background">
                <div className="relative overflow-hidden rounded-3xl border">
                  <Cover variant={p.cover} image={p.image} imageAlt={p.imageAlt} className="transition-transform duration-700 group-hover:scale-[1.04] group-focus-visible:scale-[1.04] motion-reduce:transition-none" />
                  <span className="bg-chart-1 text-[var(--portfolio-on-accent)] absolute end-3 bottom-3 inline-flex translate-y-2 items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-bold opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 motion-reduce:transition-none">{p.result}<ArrowUpRight className="size-3.5 rtl:-scale-x-100" aria-hidden="true" /></span>
                </div>
                <div className="mt-4">
                  <p className="text-muted-foreground text-xs" style={{ fontFamily: "var(--portfolio-mono)" }}>{p.client} · {p.year} · {p.discipline}</p>
                  <h2 className="mt-1.5 text-xl font-semibold tracking-[-0.025em] text-balance">{p.title}</h2>
                  <p className="text-muted-foreground mt-1 text-sm text-pretty">{p.blurb}</p>
                  <p className="sr-only">Result: {p.result}</p>
                </div>
              </a>
            </li>
          ))}
        </ul>
      </main>
    </PortfolioShell>
  )
}

export { PortfolioWork, type PortfolioWorkProps }
```

### `registry/ballmac/components/templates/portfolio/portfolio-case.tsx`

```tsx
// Ballmac UI: Portfolio case study page. https://ui.ballmac.com/templates/template-portfolio
"use client"

import * as React from "react"
import { ArrowRight } from "lucide-react"

import { TableOfContents } from "@/components/ballmac/table-of-contents"
import { projects } from "@/components/ballmac/templates/portfolio/portfolio-data"
import { Cover, PortfolioShell, type PortfolioHrefs } from "@/components/ballmac/templates/portfolio/portfolio-theme"
import { cn } from "@/lib/utils"

const mono = { fontFamily: "var(--portfolio-mono)" } as const
const h2 = "scroll-mt-24 text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl"
const body = "text-muted-foreground mt-5 max-w-2xl text-lg leading-relaxed text-pretty"

const meta = [
  ["Role", "Lead product designer"],
  ["Team", "2 designers, 5 engineers, 1 PM"],
  ["Timeline", "14 weeks, Mar to Jun 2026"],
  ["Tools", "Figma, Linear, Maze, Framer"],
]

const process = [
  ["Week 1–2", "Listen", "Watched 14 new teams try to get started. Nine gave up before inviting a colleague."],
  ["Week 3–5", "Frame", "Reduced the goal to one sentence: send your first invoice in one sitting."],
  ["Week 6–10", "Build and test", "Four prototypes, 22 tests. We cut the setup from nine steps to three."],
  ["Week 11–14", "Ship and measure", "Rolled out behind a flag to 10, then 50, then 100 percent of new signups."],
]

const results = [["+34%", "of new teams activate in their first week"], ["−58%", "time to first invoice"], ["9 → 3", "setup steps"], ["4.7", "onboarding satisfaction, up from 3.9"]]

type PortfolioCaseProps = React.ComponentProps<"div"> & { hrefs?: Partial<PortfolioHrefs> }

/** The Portfolio case study: a sticky fact sheet, a contents list that follows your reading, the process as a timeline and results in big numbers. */
function PortfolioCase({ hrefs, ...props }: PortfolioCaseProps) {
  const next = projects[1]
  return (
    <PortfolioShell page="case" hrefs={hrefs} {...props}>
      <main>
        <header className="mx-auto max-w-7xl px-4 pt-14 sm:px-8 sm:pt-20">
          <p className="text-muted-foreground text-sm" style={mono}><a href={hrefs?.work ?? "/portfolio/work"} className="underline-offset-4 hover:underline">Work</a> / Fernhill</p>
          <h1 className="mt-4 max-w-5xl text-[clamp(2.6rem,7.4vw,6.5rem)] leading-[0.95] font-semibold tracking-[-0.05em] text-balance">Rebuilding onboarding so teams reach their first invoice in one sitting.</h1>
          <Cover variant={0} className="mt-12 aspect-[16/8] rounded-3xl border" />
        </header>

        <div className="mx-auto mt-16 grid max-w-7xl gap-12 px-4 sm:px-8 lg:grid-cols-[16rem_minmax(0,1fr)] xl:grid-cols-[16rem_minmax(0,1fr)_14rem]">
          <aside className="lg:sticky lg:top-24 lg:self-start">
            <dl className="space-y-5">
              {meta.map(([k, v]) => <div key={k}><dt className="text-muted-foreground text-xs" style={mono}>{k}</dt><dd className="mt-1 text-sm font-medium text-pretty">{v}</dd></div>)}
            </dl>
          </aside>

          <article className="min-w-0 space-y-20">
            <section aria-labelledby="overview">
              <h2 id="overview" className={h2}>Overview</h2>
              <p className={body}>Fernhill is invoicing software for small studios. It was good at the second month and bad at the first day: almost half of new teams never sent an invoice. I led a 14-week rebuild of the first-run experience.</p>
            </section>
            <section aria-labelledby="problem">
              <h2 id="problem" className={h2}>The problem</h2>
              <p className={body}>The old flow asked for nine things before it showed any value: company details, tax settings, a logo, bank info, a template, a first customer. Every step was reasonable. Together they were a wall.</p>
              <blockquote className="bg-chart-1 text-[var(--portfolio-on-accent)] mt-8 max-w-2xl rounded-3xl p-8 text-2xl leading-snug font-semibold tracking-[-0.025em] text-balance">“I just wanted to send one invoice. I closed the tab at step six.”<footer className="mt-4 text-sm font-medium opacity-75" style={mono}>Participant 7, studio owner</footer></blockquote>
            </section>
            <section aria-labelledby="process">
              <h2 id="process" className={h2}>Process</h2>
              <ol className="mt-8 divide-y border-y">
                {process.map(([when, title, text]) => (
                  <li key={title} className="grid gap-2 py-6 sm:grid-cols-[8rem_10rem_1fr] sm:gap-6">
                    <span className="text-muted-foreground text-sm" style={mono}>{when}</span>
                    <span className="text-xl font-semibold tracking-[-0.02em]">{title}</span>
                    <span className="text-muted-foreground text-pretty">{text}</span>
                  </li>
                ))}
              </ol>
            </section>
            <section aria-labelledby="solution">
              <h2 id="solution" className={h2}>The solution</h2>
              <p className={body}>One screen asks the only question that matters: who are you invoicing? Everything else is a default you can change later, and the first invoice is a draft by the time you finish typing the customer’s name.</p>
              <div className="mt-8 grid gap-4 sm:grid-cols-2"><Cover variant={4} className="rounded-3xl border" /><Cover variant={3} className="rounded-3xl border" /></div>
            </section>
            <section aria-labelledby="results">
              <h2 id="results" className={h2}>Results</h2>
              <dl className="mt-8 grid gap-4 sm:grid-cols-2">
                {results.map(([v, l], i) => (
                  <div key={l} className={cn("rounded-3xl p-7", i === 0 ? "bg-chart-1 text-[var(--portfolio-on-accent)]" : "bg-secondary")}>
                    <dd className="text-6xl font-semibold tracking-[-0.05em] tabular-nums">{v}</dd>
                    <dt className={cn("mt-2 text-pretty", i === 0 ? "opacity-80" : "text-muted-foreground")}>{l}</dt>
                  </div>
                ))}
              </dl>
            </section>
            <section aria-labelledby="reflection">
              <h2 id="reflection" className={h2}>What I’d do differently</h2>
              <p className={body}>I would have involved sales earlier. They knew which questions prospects asked on day one, and two of them became default answers we had to bolt on after launch.</p>
            </section>
          </article>

          <aside className="hidden xl:block">
            <div className="sticky top-24">
              <TableOfContents title="On this page" offset={96} items={[{ id: "overview", title: "Overview" }, { id: "problem", title: "The problem" }, { id: "process", title: "Process" }, { id: "solution", title: "The solution" }, { id: "results", title: "Results" }, { id: "reflection", title: "Reflection" }]} />
            </div>
          </aside>
        </div>

        <section aria-label="Next project" className="mx-auto mt-24 max-w-7xl px-4 sm:px-8">
          <a href={hrefs?.case ?? "/portfolio/work/lumen"} className="group bg-foreground text-background focus-visible:ring-ring/50 grid items-center gap-8 overflow-hidden rounded-3xl p-6 outline-none focus-visible:ring-[3px] focus-visible:ring-offset-4 focus-visible:ring-offset-background sm:grid-cols-[1fr_20rem] sm:p-10">
            <div><p className="text-sm opacity-60" style={mono}>Next project</p><p className="mt-3 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-6xl">{next.title}</p><p className="mt-5 inline-flex items-center gap-2 font-semibold">{next.client}<ArrowRight className="size-5 transition-transform group-hover:translate-x-1 motion-reduce:transition-none rtl:rotate-180 rtl:group-hover:-translate-x-1" aria-hidden="true" /></p></div>
            <Cover variant={next.cover} image={next.image} imageAlt={next.imageAlt} className="rounded-2xl" />
          </a>
        </section>
      </main>
    </PortfolioShell>
  )
}

export { PortfolioCase, type PortfolioCaseProps }
```

### `registry/ballmac/components/templates/portfolio/portfolio-writing.tsx`

```tsx
// Ballmac UI: Portfolio writing page. https://ui.ballmac.com/templates/template-portfolio
"use client"

import * as React from "react"
import { Search } from "lucide-react"

import { posts } from "@/components/ballmac/templates/portfolio/portfolio-data"
import { PortfolioShell, type PortfolioHrefs } from "@/components/ballmac/templates/portfolio/portfolio-theme"
import { cn } from "@/lib/utils"

const mono = { fontFamily: "var(--portfolio-mono)" } as const
type Tag = "All" | "Process" | "Craft" | "Career"
const tags: Tag[] = ["All", "Process", "Craft", "Career"]
const dateFmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" })

type PortfolioWritingProps = React.ComponentProps<"div"> & { hrefs?: Partial<PortfolioHrefs> }

/** The Portfolio writing page: posts grouped by year, with a topic filter, a search and the reading time of each. */
function PortfolioWriting({ hrefs, ...props }: PortfolioWritingProps) {
  const [tag, setTag] = React.useState<Tag>("All")
  const [query, setQuery] = React.useState("")
  const q = query.trim().toLowerCase()
  const shown = posts.filter((p) => (tag === "All" || p.tag === tag) && (!q || (p.title + p.excerpt).toLowerCase().includes(q)))
  const years = [...new Set(shown.map((p) => p.date.slice(0, 4)))]
  return (
    <PortfolioShell page="writing" hrefs={hrefs} {...props}>
      <main className="mx-auto max-w-5xl px-4 pt-14 sm:px-8 sm:pt-20">
        <p className="text-muted-foreground text-sm" style={mono}>Writing</p>
        <h1 className="mt-4 text-[clamp(2.6rem,7vw,5.5rem)] leading-[0.95] font-semibold tracking-[-0.05em] text-balance">Notes on making things people use.</h1>
        <div className="mt-10 flex flex-wrap items-center gap-3">
          <div role="group" aria-label="Filter by topic" className="flex flex-wrap gap-2">
            {tags.map((t) => <button key={t} type="button" aria-pressed={tag === t} onClick={() => setTag(t)} className={cn("focus-visible:ring-ring/50 h-10 rounded-full border px-4 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", tag === t ? "bg-foreground text-background border-transparent" : "hover:bg-accent")}>{t}</button>)}
          </div>
          <div className="relative w-full sm:ms-auto sm:w-64">
            <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3.5 size-4 -translate-y-1/2" aria-hidden="true" />
            <input type="search" aria-label="Search posts" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search posts" className="bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-10 w-full rounded-full border pe-4 ps-10 text-sm outline-none focus-visible:ring-[3px]" />
          </div>
        </div>
        <p className="sr-only" role="status">{shown.length} posts</p>
        {years.length === 0 && <p className="text-muted-foreground mt-16 rounded-3xl border border-dashed p-12 text-center">No posts match. Try another topic.</p>}
        {years.map((y) => (
          <section key={y} aria-labelledby={`pw-${y}`} className="mt-14 grid gap-4 sm:grid-cols-[8rem_1fr]">
            <h2 id={`pw-${y}`} className="text-muted-foreground text-sm" style={mono}>{y}</h2>
            <ul className="divide-y border-y">
              {shown.filter((p) => p.date.startsWith(y)).map((p) => (
                <li key={p.slug}>
                  <a href="#" className="hover:bg-accent/60 focus-visible:ring-ring/50 -mx-3 block rounded-2xl px-3 py-6 outline-none transition-colors focus-visible:ring-[3px]">
                    <p className="text-muted-foreground text-xs" style={mono}>{dateFmt.format(new Date(p.date))} · {p.tag} · {p.read} min</p>
                    <h3 className="mt-2 text-2xl font-semibold tracking-[-0.03em] text-balance">{p.title}</h3>
                    <p className="text-muted-foreground mt-1.5 text-pretty">{p.excerpt}</p>
                  </a>
                </li>
              ))}
            </ul>
          </section>
        ))}
      </main>
    </PortfolioShell>
  )
}

export { PortfolioWriting, type PortfolioWritingProps }
```

### `registry/ballmac/components/templates/portfolio/portfolio-uses.tsx`

```tsx
// Ballmac UI: Portfolio uses page. https://ui.ballmac.com/templates/template-portfolio
import * as React from "react"

import { uses } from "@/components/ballmac/templates/portfolio/portfolio-data"
import { PortfolioShell, type PortfolioHrefs } from "@/components/ballmac/templates/portfolio/portfolio-theme"

const mono = { fontFamily: "var(--portfolio-mono)" } as const

type PortfolioUsesProps = React.ComponentProps<"div"> & { hrefs?: Partial<PortfolioHrefs> }

/** The Portfolio uses page: the tools, hardware and books behind the work, in plain lists. */
function PortfolioUses({ hrefs, ...props }: PortfolioUsesProps) {
  return (
    <PortfolioShell page="uses" hrefs={hrefs} {...props}>
      <main className="mx-auto max-w-5xl px-4 pt-14 sm:px-8 sm:pt-20">
        <p className="text-muted-foreground text-sm" style={mono}>Uses · updated September 2026</p>
        <h1 className="mt-4 text-[clamp(2.6rem,7vw,5.5rem)] leading-[0.95] font-semibold tracking-[-0.05em] text-balance">What’s on my desk, and why.</h1>
        <p className="text-muted-foreground mt-6 max-w-xl text-lg text-pretty">People ask. Nothing here is sponsored, and most of it is older than it should be.</p>
        <div className="mt-16 space-y-16">
          {uses.map((g) => (
            <section key={g.group} aria-labelledby={`pu-${g.group}`} className="grid gap-6 sm:grid-cols-[10rem_1fr]">
              <h2 id={`pu-${g.group}`} className="text-2xl font-semibold tracking-[-0.03em]">{g.group}</h2>
              <ul className="divide-y border-y">
                {g.items.map((i) => (
                  <li key={i.name} className="grid gap-1 py-5 sm:grid-cols-[14rem_1fr] sm:gap-6">
                    <span className="font-semibold">{i.name}</span>
                    <span className="text-muted-foreground text-pretty">{i.note}</span>
                  </li>
                ))}
              </ul>
            </section>
          ))}
        </div>
      </main>
    </PortfolioShell>
  )
}

export { PortfolioUses, type PortfolioUsesProps }
```

### `registry/ballmac/app/portfolio/page.tsx`

```tsx
// Ballmac UI: Portfolio template route. https://ui.ballmac.com/templates/template-portfolio
import type { Metadata } from "next"

import { PortfolioHome } from "@/components/ballmac/templates/portfolio/portfolio-home"

export const metadata: Metadata = {
  title: "Ines Calder: product designer",
  description: "Independent product designer in Lisbon. Selected work, writing and how to get in touch.",
}

export default function Page() {
  return <PortfolioHome />
}
```

### `registry/ballmac/app/portfolio/work/page.tsx`

```tsx
// Ballmac UI: Portfolio template route. https://ui.ballmac.com/templates/template-portfolio
import type { Metadata } from "next"

import { PortfolioWork } from "@/components/ballmac/templates/portfolio/portfolio-work"

export const metadata: Metadata = {
  title: "Work · Ines Calder",
  description: "Six projects and what they changed.",
}

export default function Page() {
  return <PortfolioWork />
}
```

### `registry/ballmac/app/portfolio/work/[slug]/page.tsx`

```tsx
// Ballmac UI: Portfolio template route. https://ui.ballmac.com/templates/template-portfolio
import type { Metadata } from "next"

import { PortfolioCase } from "@/components/ballmac/templates/portfolio/portfolio-case"

export const metadata: Metadata = {
  title: "Rebuilding onboarding · Ines Calder",
  description: "A case study: how a first-run flow got teams to their first invoice in one sitting.",
}

// One layout for every case study. Load the project by params.slug from your data when you add more.
export default function Page() {
  return <PortfolioCase />
}
```

### `registry/ballmac/app/portfolio/writing/page.tsx`

```tsx
// Ballmac UI: Portfolio template route. https://ui.ballmac.com/templates/template-portfolio
import type { Metadata } from "next"

import { PortfolioWriting } from "@/components/ballmac/templates/portfolio/portfolio-writing"

export const metadata: Metadata = {
  title: "Writing · Ines Calder",
  description: "Notes on making things people use.",
}

export default function Page() {
  return <PortfolioWriting />
}
```

### `registry/ballmac/app/portfolio/uses/page.tsx`

```tsx
// Ballmac UI: Portfolio template route. https://ui.ballmac.com/templates/template-portfolio
import type { Metadata } from "next"

import { PortfolioUses } from "@/components/ballmac/templates/portfolio/portfolio-uses"

export const metadata: Metadata = {
  title: "Uses · Ines Calder",
  description: "The tools, hardware and books behind the work.",
}

export default function Page() {
  return <PortfolioUses />
}
```
