## 1. Project Overview & Quickstart (amir20/LICENSE)

# LICENSE

Open-source repository amir20/LICENSE

### Repository Details
- **Repository:** [amir20/LICENSE](https://github.com/amir20/LICENSE)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (amir20/amir20.github.io)

## File: CLAUDE.md

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Personal portfolio website (amirraminfar.com) built with **Nuxt 4** (Vue 3), styled with **UnoCSS**, and deployed as a static site on **Netlify**.

## Commands

- **Install**: `bun install`
- **Dev server**: `bun run dev`
- **Build** (static generation): `bun run build`
- **Preview build**: `bun run preview`
- **Format**: `bun run format`

No test or lint commands are configured.

## Architecture

- **Package manager**: bun (see `packageManager` in `package.json`)
- **Source dir**: Nuxt 4 `app/` layout (`app/pages`, `app/components`, …); `public/` stays at the root
- **Runtime**: bun only. Scripts use `bun --bun` so Nuxt/Vite run on bun instead of Node; no Node install is needed
- **Main branch**: `source` (not `main`)

### Key Tech

- **UnoCSS** for utility-first styling (configured in `uno.config.ts`) with Tailwind-compatible reset and `@apply` directive support
- **D3.js** (d3-selection, d3-shape, d3-timer) powers the interactive wave animation in `app/components/Waves.vue`
- **Google Fonts**: IBM Plex Mono (headings) and Work Sans (body), self-hosted at build via `@nuxtjs/google-fonts`
- **Phosphor icons** via `@iconify-json/ph`

### Wave Animation System

The wave background is the site's signature visual element:
- `app/components/Waves.vue` renders 6 layered SVG waves animated with D3
- Mouse position drives hue rotation on the waves
- `app/composables/useWaveMultiplier.ts` provides shared state for wave height
- Each page sets its own wave multiplier in `onMounted` (Home: 9, About: 4, Projects: 2.5, project detail pages: 1.5)

### Styling

- Custom colors defined in `uno.config.ts`: `cream` (#f5f3ee)
- Dark mode: media-based (`prefers-color-scheme`)
- Accent colors: brown (#b8513d) and gold (#d4a87c) defined in `app/assets/css/custom.css`
- Max line width: 160 characters (Prettier + EditorConfig)

### Deployment

- Netlify deploys from `source` branch
- Build output: `.output/public` (Netlify's Nuxt integration picks it up)
- Prerendered routes: `/`, `/about`, `/projects`, `/projects/gruper`, `/projects/dozzle`
- Route animations use the View Transitions API only (`app.viewTransition`, styled in `custom.css`). Don't add a Vue `pageTransition`: Nuxt holds the view transition open until `page:finish`, so an out-in page transition freezes the screen and the waves

---

## File: DESIGN.json

{
  "schemaVersion": 2,
  "generatedAt": "2026-05-02T00:00:00Z",
  "title": "Design System: AmirRaminfar.me",
  "extensions": {
    "colorMeta": {
      "cream": {
        "role": "neutral",
        "displayName": "Cream",
        "canonical": "oklch(96.5% 0.008 80)",
        "tonalRamp": [
          "oklch(20% 0.008 80)",
          "oklch(32% 0.008 80)",
          "oklch(44% 0.008 80)",
          "oklch(56% 0.008 80)",
          "oklch(68% 0.008 80)",
          "oklch(80% 0.008 80)",
          "oklch(89% 0.008 80)",
          "oklch(96.5% 0.008 80)"
        ]
      },
      "ink-deep": {
        "role": "neutral",
        "displayName": "Ink Deep",
        "canonical": "oklch(20% 0.005 60)",
        "tonalRamp": [
          "oklch(15% 0.005 60)",
          "oklch(20% 0.005 60)",
          "oklch(30% 0.005 60)",
          "oklch(42% 0.005 60)",
          "oklch(55% 0.005 60)",
          "oklch(70% 0.005 60)",
          "oklch(84% 0.005 60)",
          "oklch(94% 0.005 60)"
        ]
      },
      "ink-cream": {
        "role": "neutral",
        "displayName": "Ink Cream",
        "canonical": "oklch(94% 0.008 80)",
        "tonalRamp": [
          "oklch(20% 0.008 80)",
          "oklch(32% 0.008 80)",
          "oklch(44% 0.008 80)",
          "oklch(56% 0.008 80)",
          "oklch(68% 0.008 80)",
          "oklch(80% 0.008 80)",
          "oklch(88% 0.008 80)",
          "oklch(94% 0.008 80)"
        ]
      },
      "body-light": {
        "role": "neutral",
        "displayName": "Body Light",
        "canonical": "oklch(38% 0.012 260)",
        "tonalRamp": [
          "oklch(15% 0.012 260)",
          "oklch(25% 0.012 260)",
          "oklch(38% 0.012 260)",
          "oklch(50% 0.012 260)",
          "oklch(62% 0.012 260)",
          "oklch(74% 0.012 260)",
          "oklch(86% 0.012 260)",
          "oklch(96% 0.012 260)"
        ]
      },
      "body-dark": {
        "role": "neutral",
        "displayName": "Body Dark",
        "canonical": "oklch(86% 0.008 260)",
        "tonalRamp": [
          "oklch(20% 0.008 260)",
          "oklch(32% 0.008 260)",
          "oklch(44% 0.008 260)",
          "oklch(56% 0.008 260)",
          "oklch(66% 0.008 260)",
          "oklch(76% 0.008 260)",
          "oklch(86% 0.008 260)",
          "oklch(95% 0.008 260)"
        ]
      },
      "surface-night": {
        "role": "neutral",
        "displayName": "Surface Night",
        "canonical": "oklch(20% 0.015 270)",
        "tonalRamp": [
          "oklch(12% 0.015 270)",
          "oklch(20% 0.015 270)",
          "oklch(30% 0.015 270)",
          "oklch(42% 0.015 270)",
          "oklch(55% 0.015 270)",
          "oklch(68% 0.015 270)",
          "oklch(82% 0.015 270)",
          "oklch(94% 0.015 270)"
        ]
      },
      "ember-brown": {
        "role": "primary",
        "displayName": "Ember Brown",
        "canonical": "oklch(54% 0.14 35)",
        "tonalRamp": [
          "oklch(22% 0.06 35)",
          "oklch(32% 0.09 35)",
          "oklch(42% 0.12 35)",
          "oklch(54% 0.14 35)",
          "oklch(66% 0.13 35)",
          "oklch(78% 0.10 35)",
          "oklch(88% 0.06 35)",
          "oklch(96% 0.02 35)"
        ]
      },
      "ember-brown-deep": {
        "role": "primary",
        "displayName": "Ember Brown Deep",
        "canonical": "oklch(45% 0.13 35)",
        "tonalRamp": [
          "oklch(20% 0.06 35)",
          "oklch(30% 0.09 35)",
          "oklch(38% 0.12 35)",
          "oklch(45% 0.13 35)",
          "oklch(58% 0.13 35)",
          "oklch(72% 0.10 35)",
          "oklch(85% 0.06 35)",
          "oklch(95% 0.02 35)"
        ]
      },
      "warm-gold": {
        "role": "primary",
        "displayName": "Warm Gold",
        "canonical": "oklch(76% 0.09 65)",
        "tonalRamp": [
          "oklch(28% 0.05 65)",
          "oklch(40% 0.07 65)",
          "oklch(52% 0.09 65)",
          "oklch(64% 0.10 65)",
          "oklch(76% 0.09 65)",
          "oklch(85% 0.07 65)",
          "oklch(92% 0.04 65)",
          "oklch(97% 0.015 65)"
        ]
      },
      "warm-gold-bright": {
        "role": "primary",
        "displayName": "Warm Gold Bright",
        "canonical": "oklch(83% 0.08 65)",
        "tonalRamp": [
          "oklch(30% 0.05 65)",
          "oklch(42% 0.07 65)",
          "oklch(54% 0.09 65)",
          "oklch(66% 0.09 65)",
          "oklch(78% 0.08 65)",
          "oklch(83% 0.08 65)",
          "oklch(91% 0.05 65)",
          "oklch(97% 0.02 65)"
        ]
      },
      "tide-deep": {
        "role": "tertiary",
        "displayName": "Tide Deep",
        "canonical": "oklch(22% 0.04 240)",
        "tonalRamp": [
          "oklch(14% 0.03 240)",
          "oklch(22% 0.04 240)",
          "oklch(32% 0.06 240)",
          "oklch(42% 0.08 240)",
          "oklch(54% 0.09 235)",
          "oklch(66% 0.10 230)",
          "oklch(78% 0.09 225)",
          "oklch(90% 0.05 225)"
        ]
      },
      "tide-shore": {
        "role": "tertiary",
        "displayName": "Tide Shore",
        "canonical": "oklch(74% 0.10 230)",
        "tonalRamp": [
          "oklch(20% 0.04 240)",
          "oklch(32% 0.06 235)",
          "oklch(44% 0.08 232)",
          "oklch(56% 0.09 230)",
          "oklch(66% 0.10 230)",
          "oklch(74% 0.10 230)",
          "oklch(84% 0.07 228)",
          "oklch(94% 0.03 225)"
        ]
      }
    },
    "typographyMeta": {
      "display": {
        "displayName": "Display",
        "purpose": "The single page title per route. Always carries view-transition-name: title."
      },
      "body": {
        "displayName": "Body",
        "purpose": "Paragraph copy. Hold to ~66ch by constraining the column width."
      },
      "label": {
        "displayName": "Label",
        "purpose": "Uppercase nav links and small navigational chrome. The only place small caps appear."
      }
    },
    "shadows": [
      {
        "name": "glass-rest",
        "value": "0 1px 3px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.6)",
        "purpose": "GlassButton at rest in light mode."
      },
      {
        "name": "glass-hover",
        "value": "0 4px 12px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.8)",
        "purpose": "GlassButton on hover, paired with translateY(-1px)."
      },
      {
        "name": "glass-rest-dark",
        "value": "0 1px 3px rgba(0,0,0,0.20), inset 0 1px 0 rgba(255,255,255,0.06)",
        "purpose": "GlassButton at rest in dark mode."
      },
      {
        "name": "thumb-rest",
        "value": "0 1px 2px rgba(0,0,0,0.05)",
        "purpose": "Project thumbnail. The only non-glass shadow in the system."
      }
    ],
    "motion": [
      {
        "name": "ease-glass",
        "value": "cubic-bezier(0.22, 1, 0.36, 1)",
        "purpose": "Ease-out-quart for GlassButton hover lift; matches the calm tide cadence."
      },
      {
        "name": "duration-state",
        "value": "300ms",
        "purpose": "GlassButton state transitions. Long enough to feel deliberate, short enough to feel responsive."
      },
      {
        "name": "view-transition-default",
        "value": "browser default",
        "purpose": "Used by view-transition-name on title, nav, and selected route underline."
      }
    ],
    "breakpoints": [
      { "name": "md", "value": "768px" },
      { "name": "lg", "value": "1024px" }
    ]
  },
  "components": [
    {
      "name": "Glass Button (Light)",
      "kind": "button",
      "refersTo": "button-glass",
      "description": "External-link button. Translucent over the live tide; lifts on hover.",
      "html": "<a href=\"#\" class=\"ds-glass-btn\"><svg class=\"ds-glass-btn__icon\" viewBox=\"0 0 256 256\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"></svg>GitHub</a>",
      "css": ".ds-glass-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 12px; font: 500 14px/1 'Roboto', system-ui, sans-serif; letter-spacing: 0.02em; color: #374151; text-decoration: none; background: rgba(255,255,255,0.45); border: 1px solid rgba(255,255,255,0.5); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 1px 3px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.6); transition: all 300ms cubic-bezier(0.22, 1, 0.36, 1); } .ds-glass-btn__icon { width: 18px; height: 18px; } .ds-glass-btn:hover { background: rgba(255,255,255,0.65); border-color: rgba(255,255,255,0.7); color: #1a3f47; box-shadow: 0 4px 12px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.8); transform: translateY(-1px); } .ds-glass-btn:active { transform: translateY(0); background: rgba(255,255,255,0.5); }"
    },
    {
      "name": "Glass Button (Dark)",
      "kind": "button",
      "refersTo": "button-glass-dark",
      "description": "Dark-mode variant of the external-link button.",
      "html": "<a href=\"#\" class=\"ds-glass-btn-dark\"><svg class=\"ds-glass-btn-dark__icon\" viewBox=\"0 0 256 256\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"></svg>LinkedIn</a>",
      "css": ".ds-glass-btn-dark { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 12px; font: 500 14px/1 'Roboto', system-ui, sans-serif; letter-spacing: 0.02em; color: #d1d5db; text-decoration: none; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 1px 3px rgba(0,0,0,0.20), inset 0 1px 0 rgba(255,255,255,0.06); transition: all 300ms cubic-bezier(0.22, 1, 0.36, 1); } .ds-glass-btn-dark__icon { width: 18px; height: 18px; } .ds-glass-btn-dark:hover { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.20); color: #ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.10); transform: translateY(-1px); } .ds-glass-btn-dark:active { transform: translateY(0); background: rgba(255,255,255,0.10); }"
    },
    {
      "name": "Nav Link (Active)",
      "kind": "nav",
      "refersTo": "nav-link-active",
      "description": "Top nav link in its active-route state. The 2px underline carries view-transition-name: selected and slides between routes.",
      "html": "<nav class=\"ds-nav\"><a class=\"ds-nav__link\" href=\"#\">Home</a><a class=\"ds-nav__link is-active\" href=\"#\">About Me</a><a class=\"ds-nav__link\" href=\"#\">Projects</a></nav>",
      "css": ".ds-nav { display: flex; gap: 20px; padding: 4px 0; } .ds-nav__link { position: relative; padding: 8px; font: 500 14px/1 'Roboto', system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; text-decoration: none; transition: color 200ms cubic-bezier(0.22, 1, 0.36, 1); } .ds-nav__link:hover { color: #1a1a1a; } .ds-nav__link.is-active { color: #1a1a1a; } .ds-nav__link.is-active::before { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: #b8513d; }"
    },
    {
      "name": "Display Heading",
      "kind": "custom",
      "refersTo": "typography.display",
      "description": "The single page title per route. Playfair Display with optical sizing and tight tracking.",
      "html": "<h1 class=\"ds-display\">Hi there, my name is Amir.</h1>",
      "css": ".ds-display { font-family: 'Playfair Display', Georgia, serif; font-optical-sizing: auto; font-weight: 500; font-size: clamp(2.75rem, 6vw, 3.75rem); line-height: 1.1; letter-spacing: -0.02em; color: #1a1a1a; margin: 0 0 24px 0; }"
    },
    {
      "name": "Body Paragraph with Inline Link",
      "kind": "custom",
      "refersTo": "link-inline",
      "description": "Body text with the warm inline link treatment. Underlined with a 40% gold decoration tint.",
      "html": "I am a software engineer, mentor and parent in San Francisco. Currently an engineering manager at <a class=\"ds-body__link\" href=\"#\">Netflix</a>.",
      "css": ".ds-body { font-family: 'Roboto', system-ui, sans-serif; font-weight: 400; font-size: 1.125rem; line-height: 1.7; letter-spacing: 0.01em; color: #374151; max-width: 66ch; margin: 0; } .ds-body__link { color: #b8513d; text-decoration: underline; text-decoration-color: rgba(212, 168, 124, 0.4); text-decoration-thickness: 1px; text-underline-offset: 4px; transition: color 200ms ease; } .ds-body__link:hover { color: #943f2f; }"
    },
    {
      "name": "Project Item",
      "kind": "card",
      "refersTo": "project-thumb",
      "description": "A row in the projects list. Image left, text right; intentionally not a card.",
      "html": "<article class=\"ds-project\"><a class=\"ds-project__media\" href=\"#\"></a><a class=\"ds-project__title\" href=\"#\">Dozzle for Docker</a>A web application that provides an interface to monitor your Docker containers and logs across multiple hosts, Docker Swarm, and Kubernetes.</article>",
      "css": ".ds-project { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; } @media (min-width: 768px) { .ds-project { flex-direction: row; gap: 32px; } } .ds-project__media { flex-shrink: 0; } .ds-project__thumb { width: 240px; height: 132px; border-radius: 16px; border: 1px solid #e5e7eb; box-shadow: 0 1px 2px rgba(0,0,0,0.05); background: linear-gradient(135deg, oklch(54% 0.09 230) 0%, oklch(22% 0.04 240) 100%); } .ds-project__title { display: block; font-family: 'Roboto', system-ui, sans-serif; font-size: 1.125rem; font-weight: 500; color: #1a1a1a; text-decoration: none; margin-bottom: 8px; } .ds-project__desc { font-family: 'Roboto', system-ui, sans-serif; font-size: 1.125rem; line-height: 1.7; color: #374151; margin: 0; max-width: 60ch; }"
    }
  ],
  "narrative": {
    "northStar": "The Coastal Atelier",
    "overview": "The site is an engineer's workshop with the ocean visible through the window. Warm cream paper, hand-set serif headlines, and a working tide that reacts to where you point. Every signature element is built, not bought: a six-layer animated D3 wave system, ambient raindrops, view-transitioned title morphs between pages. The interface is the proof of taste. Adjectives are not needed.\n\nDensity is calm. One column of generous text on cream paper, with a single anchored signature: the tide along the bottom. The page makes room for the work, not the other way around. This is not a SaaS landing, not a dev terminal, not a LinkedIn profile in HTML; it is closer to a small magazine cover that animates when nobody is reading it.\n\nThe system explicitly rejects the dev-portfolio category reflex: no neon-on-black, no monospace headers, no \"$ whoami\", no hero metrics, no identical card grids. When dark mode appears, it stays warm-tinted toward gold, never blue-black.",
    "keyCharacteristics": [
      "Warm cream surface (#f5f3ee) with optical-sized serif display type",
      "A living, mouse-reactive tide animation as the persistent background signature",
      "Two warm accents only: ember brown in light mode, warm gold in dark mode",
      "Generous vertical air, narrow text column (≤66ch), no card-grid filler",
      "View transitions on the title and nav between routes"
    ],
    "rules": [
      { "name": "The Two Warms Rule", "body": "Light mode uses Ember Brown for accent; dark mode uses Warm Gold. They are not interchangeable across modes. A brown link in dark mode looks like a bug; a gold link in light mode looks like a sticker.", "section": "colors" },
      { "name": "The Tide Stays in the Tide", "body": "The blue-teal range is the property of Waves.vue. Nothing else on the page is allowed to borrow from it: not a button, not a focus ring, not a tag. The tide reads as scene because nothing else is wearing its colors.", "section": "colors" },
      { "name": "The No-Pure-Black Rule", "body": "#000 and #fff are forbidden. Surface and ink are always tinted toward cream.", "section": "colors" },
      { "name": "The One Title Rule", "body": "Each route has exactly one display heading and it owns view-transition-name: title. Subheads use body weight at body size, not a stepped-down display.", "section": "typography" },
      { "name": "The Optical Sizing Rule", "body": "Playfair must declare font-optical-sizing: auto. The font has a real opsz axis and the difference at hero size is visible.", "section": "typography" },
      { "name": "The Flat-By-Default Rule", "body": "Surfaces are flat. The only earned shadows in this system live on the glass buttons and on project thumbnails. No decorative elevation anywhere else.", "section": "elevation" },
      { "name": "The One Glass Rule", "body": "backdrop-filter is allowed in exactly one place: GlassButton.vue. Adding glass to a card, a nav, or a panel breaks the rule.", "section": "elevation" }
    ],
    "dos": [
      "Do keep the tide visible on every page. It is the brand.",
      "Do use Ember Brown in light mode and Warm Gold in dark mode, and only those two, for accents.",
      "Do hold body text columns to md:w-3/4 lg:w-1/2 (≈66ch) on prose-heavy pages.",
      "Do declare font-optical-sizing: auto wherever Playfair Display appears.",
      "Do use view-transition-name for elements that persist across routes (title, nav, active underline).",
      "Do prefer flat, bordered, list-shaped layouts over cards. Project list and About page are the templates.",
      "Do use clamp() for display sizes so headings scale fluidly rather than stepping at breakpoints."
    ],
    "donts": [
      "Don't drift into the standard dev-portfolio aesthetic: no monospace headers, no blinking terminal cursor, no neon-on-black, no $ whoami.",
      "Don't drift into the FAANG-manager LinkedIn look: no stock photos, no badge logos, no \"passionate leader\" copy, no work-not-shown.",
      "Don't ship glassy SaaS landing patterns: no hero metrics, no identical card grids, no gradient text.",
      "Don't add a blog section unless there is something genuinely worth reading.",
      "Don't use #000 or #fff anywhere. Tint to cream.",
      "Don't use the tide's blue-teal range outside Waves.vue. Not as a button, not as a focus ring, not as a tag.",
      "Don't use Ember Brown in dark mode or Warm Gold in light mode. The two warms are mode-bound.",
      "Don't add backdrop-filter to anything other than GlassButton.",
      "Don't wrap content in cards by reflex. The site is list-shaped, not tile-shaped.",
      "Don't use side-stripe borders, gradient text, or modals as the first reach for an interaction.",
      "Don't use em dashes in body copy. Commas, colons, semicolons, periods, parentheses.",
      "Don't add a second display headline to a page. One title per route."
    ]
  }
}

---

## File: DESIGN.md

---
name: AmirRaminfar.me
colors:
  cream: "#f5f3ee"
  ink-deep: "#1a1a1a"
  ink-cream: "#f0ece4"
  body-light: "#374151"
  body-dark: "#d1d5db"
  surface-night: "#111827"
  ember-brown: "#b8513d"
  ember-brown-deep: "#943f2f"
  warm-gold: "#d4a87c"
  warm-gold-bright: "#e8c49e"
  tide-deep: "#071e32"
  tide-shore: "#5dade2"
typography:
  display:
    fontFamily: "Playfair Display, Georgia, serif"
    fontSize: "clamp(2.75rem, 6vw, 3.75rem)"
    fontWeight: 500
    lineHeight: 1.1
    letterSpacing: "-0.02em"
    fontFeature: "'opsz' auto"
  body:
    fontFamily: "Roboto, system-ui, sans-serif"
    fontSize: "1.125rem"
    fontWeight: 400
    lineHeight: 1.7
    letterSpacing: "0.01em"
  label:
    fontFamily: "Roboto, system-ui, sans-serif"
    fontSize: "0.875rem"
    fontWeight: 500
    letterSpacing: "0.05em"
rounded:
  sm: "8px"
  md: "12px"
  lg: "16px"
spacing:
  xs: "8px"
  sm: "16px"
  md: "24px"
  lg: "40px"
  xl: "64px"
components:
  button-glass:
    backgroundColor: "rgba(255,255,255,0.45)"
    textColor: "{colors.body-light}"
    rounded: "{rounded.md}"
    padding: "10px 20px"
  button-glass-hover:
    backgroundColor: "rgba(255,255,255,0.65)"
    textColor: "#1a3f47"
  button-glass-dark:
    backgroundColor: "rgba(255,255,255,0.08)"
    textColor: "{colors.body-dark}"
    rounded: "{rounded.md}"
    padding: "10px 20px"
  nav-link:
    textColor: "#6b7280"
    typography: "{typography.label}"
    padding: "8px 8px"
  nav-link-active:
    textColor: "{colors.ink-deep}"
    typography: "{typography.label}"
  link-inline:
    textColor: "{colors.ember-brown}"
  link-inline-dark:
    textColor: "{colors.warm-gold}"
  project-thumb:
    rounded: "{rounded.lg}"
    width: "240px"
---

# Design System: AmirRaminfar.me

## 1. Overview

**Creative North Star: "The Coastal Atelier"**

The site is an engineer's workshop with the ocean visible through the window. Warm cream paper, hand-set serif headlines, and a working tide that reacts to where you point. Every signature element is built, not bought: a six-layer animated D3 wave system, ambient raindrops, view-transitioned title morphs between pages. The interface is the proof of taste. Adjectives are not needed.

Density is calm. One column of generous text on cream paper, with a single anchored signature: the tide along the bottom. The page makes room for the work, not the other way around. This is not a SaaS landing, not a dev terminal, not a LinkedIn profile in HTML; it is closer to a small magazine cover that animates when nobody is reading it.

The system explicitly rejects the dev-portfolio category reflex: no neon-on-black, no monospace headers, no "$ whoami", no hero metrics, no identical card grids. When dark mode appears, it stays warm-tinted toward gold, never blue-black.

**Key Characteristics:**
- Warm cream surface (`#f5f3ee`) with optical-sized serif display type
- A living, mouse-reactive tide animation as the persistent background signature
- Two warm accents only: ember brown in light mode, warm gold in dark mode
- Generous vertical air, narrow text column (≤66ch), no card-grid filler
- View transitions on the title and nav between routes

## 2. Colors: The Cream and Tide Palette

A short palette by intent. Two warms carry every accent; one neutral pair carries every surface; the tide owns the background and is never used as type or chrome.

### Primary
- **Ember Brown** (`#b8513d`): the light-mode accent. Inline links, the active-route underline, anything the eye should follow first. Hover deepens to **Ember Brown Deep** (`#943f2f`).
- **Warm Gold** (`#d4a87c`): the dark-mode accent and link-decoration tint in both modes. Inline links in dark, the active-route underline in dark, link-underline tint at 40% opacity in both. Hover lifts to **Warm Gold Bright** (`#e8c49e`).

### Neutral
- **Cream** (`#f5f3ee`): the light-mode page surface. Paper, not white. The site is unmistakably warm before a single accent touches it.
- **Ink Deep** (`#1a1a1a`): light-mode heading color. Near-black, never `#000`, slightly warm.
- **Body Light** (`#374151`): light-mode body text. Reads quietly under the headings.
- **Surface Night** (`#111827`): dark-mode page surface. Warm-leaning charcoal, not blue-black.
- **Ink Cream** (`#f0ece4`): dark-mode heading color. The cream returns as type.
- **Body Dark** (`#d1d5db`): dark-mode body text.

### Tertiary: The Tide
- **Tide Deep** (`#071e32`) → **Tide Shore** (`#5dade2`): six layered wave colors stepping from deep navy to shore teal. These live exclusively in `Waves.vue`. **They are never type, never chrome, never borders.** The tide is a place, not a palette.

### Named Rules

**The Two Warms Rule.** Light mode uses Ember Brown for accent; dark mode uses Warm Gold. They are not interchangeable across modes. A brown link in dark mode looks like a bug; a gold link in light mode looks like a sticker.

**The Tide Stays in the Tide.** The blue-teal range is the property of `Waves.vue`. Nothing else on the page is allowed to borrow from it: not a button, not a focus ring, not a tag. The tide reads as scene because nothing else is wearing its colors.

**The No-Pure-Black Rule.** `#000` and `#fff` are forbidden. Surface and ink are always tinted toward cream. If a value drifts toward true black or true white, it is wrong by definition.

## 3. Typography

**Display Font:** Playfair Display (serif, with optical sizing across 400–900, italic available)
**Body Font:** Roboto (sans, weights 100–900)

**Character:** A near-magazine pairing. Playfair carries the page identity; Roboto stays out of the way. Display headings tighten letter-spacing (`-0.02em`); body opens slightly (`+0.01em`). The result reads editorial without dressing up.

### Hierarchy
- **Display** (Playfair, weight 500, `clamp(2.75rem, 6vw, 3.75rem)`, line-height 1.1, `letter-spacing: -0.02em`): the page title, one per route. Always carries `view-transition-name: title` so it morphs across navigations.
- **Body** (Roboto, weight 400, `1.125rem`, line-height 1.7, `letter-spacing: 0.01em`): paragraph text. Cap line length at ~66ch by holding the column at `md:w-3/4 lg:w-1/2` for prose-heavy pages.
- **Label** (Roboto, weight 500, `0.875rem`, `letter-spacing: 0.05em`, `text-transform: uppercase`): nav links and any small navigational chrome. The only place small caps appear.
- **Inline link** (inherits body): underlined with a 40% gold decoration tint, offset 4. Color follows mode (Ember Brown / Warm Gold).

### Named Rules

**The One Title Rule.** Each route has exactly one display heading and it owns `view-transition-name: title`. Subheads use body weight at body size, not a stepped-down display. The display voice belongs to the page, once.

**The Optical Sizing Rule.** Playfair must declare `font-optical-sizing: auto`. The font has a real `opsz` axis and the difference at hero size is visible; turning it off makes the headline look like it shipped in 2014.

## 4. Elevation

The site is essentially flat at rest. There are no card stacks, no layered panels, no drop shadows on text or surfaces. Depth is conveyed by the live tide behind everything (parallax, motion, blur layers in the wave SVG itself), and by the `backdrop-filter: blur(12px)` glass on the social buttons. Project thumbnails carry a single 1px border and a `shadow-sm`, nothing more.

### Shadow Vocabulary
- **Glass-rest** (`0 1px 3px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.6)`): the social-button rest state. A whisper of lift over the cream. Held constant across hover and active.
- **Thumb-rest** (`shadow-sm`, ~`0 1px 2px rgba(0,0,0,0.05)`): project list image. That's all.

### Named Rules

**The Flat-By-Default Rule.** Surfaces are flat. The only earned shadows in this system live on the glass buttons (functional: they need to read as liftable above the tide) and on project thumbnails (functional: they need to read as image-not-page). No decorative elevation anywhere else.

**The One Glass Rule.** `backdrop-filter` is allowed in exactly one place: `GlassButton.vue`. Adding glass to a card, a nav, or a panel breaks the rule. If a new surface wants to feel glassy, the answer is no.

## 5. Components

### Buttons (Glass)
- **Shape:** generously rounded (`12px`, `rounded-xl`).
- **Light:** translucent white at 45% opacity, 1px white-50 border, blurred 12px, body-light text. Padding `10px 20px`. Hover swaps to 65% opacity instantly; active settles to 50%.
- **Dark:** translucent white at 8% opacity, white-12 border, body-dark text. Hover swaps to 15% opacity instantly.
- **Use:** external destinations only (GitHub, LinkedIn, Stack Overflow). The glass treatment signals "leaves the site". Internal navigation never uses it.

### Navigation
- **Style:** uppercase Roboto medium, `0.875rem`, tracking `0.05em`. Default text is gray-500/400; active route is ink-deep/ink-cream.
- **Active:** a 2px full-width underline in Ember Brown (light) or Warm Gold (dark), placed via `::before` and carrying `view-transition-name: selected` so it slides between routes.
- **No hover background, no pill, no chip.** A nav link is text and an underline. That is the whole component.

### Inline Links (Body)
- **Style:** colored (Ember Brown light / Warm Gold dark), underlined with a 40%-opacity gold decoration, `underline-offset: 4`. Hover deepens the text color one step; the decoration does not change.
- **Never** style as a button or pill inside body text. The two-warm system reads as link the moment a color appears under the body weight.

### Project Item
- **Layout:** image left (`240px`), text right, on `md+`; stacked on mobile. Items are spaced by `2.5rem` of vertical air, never inside a card.
- **Image:** rounded-`16px`, 1px gray-200/700 border, `shadow-sm`. Video thumbnails autoplay muted in the same frame.
- **Text:** project name in body weight + medium, description in body color, inline links allowed.
- **No card.** The list is intentionally borderless. Each row reads as an entry on a page, not a tile in a grid.

### Signature: The Tide (Waves.vue)
A six-layer SVG wave field rendered with `d3-shape` curves and animated by `d3-timer`. Layers step from `Tide Deep` (back, blurred 10px, opacity 0.35) to `Tide Shore` (front, sharp, opacity 0.6). Mouse position drives a hue-rotation across all layers; the tide phase advances continuously. Per-page `useWaveMultiplier` scales amplitude (Home `9`, About `4`, Projects `2.5`).

The tide is the brand. It must always be present. It must never be replaced with a static image, gradient, or video.

### Signature: View Transitions
The page title (`view-transition-name: title`), the nav (`view-transition-name: nav`), and the active-route underline (`view-transition-name: selected`) morph across routes via the View Transitions API. New components added to a page-level surface should consider whether they belong in this morph set; most do not.

## 6. Do's and Don'ts

### Do:
- **Do** keep the tide visible on every page. It is the brand.
- **Do** use Ember Brown in light mode and Warm Gold in dark mode, and only those two, for accents.
- **Do** hold body text columns to `md:w-3/4 lg:w-1/2` (≈66ch) on prose-heavy pages.
- **Do** declare `font-optical-sizing: auto` wherever Playfair Display appears.
- **Do** use `view-transition-name` for elements that persist across routes (title, nav, active underline).
- **Do** prefer flat, bordered, list-shaped layouts over cards. Project list and About page are the templates.
- **Do** use `clamp()` for display sizes so headings scale fluidly rather than stepping at breakpoints.

### Don't:
- **Don't** drift into the standard dev-portfolio aesthetic: no monospace headers, no blinking terminal cursor, no neon-on-black, no `$ whoami`. (Anti-reference, PRODUCT.md.)
- **Don't** drift into the FAANG-manager LinkedIn look: no stock photos, no badge logos, no "passionate leader" copy, no work-not-shown. (Anti-reference, PRODUCT.md.)
- **Don't** ship glassy SaaS landing patterns: no hero metrics, no identical card grids, no gradient text. (Anti-reference, PRODUCT.md.)
- **Don't** add a blog section unless there is something genuinely worth reading. (Anti-reference, PRODUCT.md.)
- **Don't** use `#000` or `#fff` anywhere. Tint to cream.
- **Don't** use the tide's blue-teal range outside `Waves.vue`. Not as a button, not as a focus ring, not as a tag.
- **Don't** use Ember Brown in dark mode or Warm Gold in light mode. The two warms are mode-bound.
- **Don't** add `backdrop-filter` to anything other than `GlassButton`.
- **Don't** wrap content in cards by reflex. The site is list-shaped, not tile-shaped.
- **Don't** use side-stripe borders, gradient text, or modals as the first reach for an interaction.
- **Don't** use em dashes in body copy. Commas, colons, semicolons, periods, parentheses.
- **Don't** add a second display headline to a page. One title per route.

---

## File: package.json

{
  "name": "amirraminfar.me",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "packageManager": "bun@1.4.2",
  "scripts": {
    "dev": "bun --bun nuxt dev",
    "build": "bun --bun nuxt generate",
    "preview": "bun --bun nuxt preview",
    "format": "bun --bun prettier --write .",
    "postinstall": "bun --bun nuxt prepare"
  },
  "dependencies": {
    "d3-selection": "^3.0.0",
    "d3-shape": "^3.2.0",
    "d3-timer": "^3.0.1",
    "nuxt": "^4.5.2",
    "vue": "^3.5.43"
  },
  "devDependencies": {
    "@iconify-json/ph": "^1.2.2",
    "@nuxtjs/google-fonts": "^3.2.0",
    "@unocss/nuxt": "^66.10.5",
    "@unocss/reset": "^66.10.5",
    "prettier": "^3.9.9",
    "unocss": "^66.10.5"
  }
}

---

## File: PRODUCT.md

# Product

## Register

brand

## Users

Engineers, hiring managers, and collaborators who arrive from open-source projects (Dozzle, dtop), GitHub, LinkedIn, or word of mouth. They land here to evaluate one of two things: what side projects Amir is building, or who Amir is as an engineer-leader. They are technical, design-literate, and quick to bounce if a personal site feels generic.

## Product Purpose

A personal site for Amir Raminfar, engineering manager at Netflix and prolific open-source builder (Dozzle, dtop, phantomjs-node, Clashleaders, Gruper). It exists to make the work and the person feel coherent: side projects on one side, identity and trajectory on the other, both rendered in a way that itself proves taste and craft. Success is when a visitor leaves with a clear, warm impression of Amir as someone who ships, mentors, and builds with care, not a generic "engineering manager" resume.

## Brand Personality

Warm, innovative, modern. Voice is first-person and direct, not corporate. Tone leans confident but unpretentious, the kind of writing an engineer-leader uses when they have nothing to prove. The interface should feel like it was hand-built by the person it represents, with small, considered moments of cleverness (the wave animation, view transitions, raindrops) that reward attention without demanding it.

## Anti-references

- Generic blog templates with reverse-chronological post lists and no point of view.
- Standard dev-portfolio aesthetic: mono font, blinking terminal cursor, neon-on-black, "$ whoami" headers.
- The FAANG-manager LinkedIn look: stock photos, badge logos, "passionate leader" copy, no work shown.
- Glassy SaaS landing pages with hero metrics, identical card grids, gradient text.
- "Random website with no interesting data" — pages that exist to fill a route rather than show something worth seeing.

## Design Principles

1. **Show, don't tell.** The site itself is the proof of craft. Custom animations, view transitions, and considered typography do the work that adjectives would do on a weaker site. Never write copy that claims taste the design hasn't already demonstrated.
2. **Warm-modern, not cold-tech.** Cream and warm accents (brown, gold) over the dev-portfolio reflex of dark mode + neon. When dark mode is used, it stays warm-tinted, never blue-black. The category-reflex answer is wrong here on purpose.
3. **Engineer-craftsman, not corporate manager.** Lean into the open-source builder identity. Projects, code, and signature visual effects come first; titles and company logos are supporting cast.
4. **Substance over filler.** Every page earns its existence. Three real pages with real content beat ten skeleton routes. No "blog" section unless there is something genuinely worth reading.
5. **Quiet signature moments.** One or two distinctive flourishes per surface (the wave system, raindrops, view-transition title morph), not a parade of effects. Innovation is felt, not announced.

## Accessibility & Inclusion

No specific requirements beyond reasonable defaults. Maintain readable contrast in both light and dark modes; do not regress existing keyboard navigation.

---

## File: .prettierrc.json

{
  "printWidth": 160
}

---

## File: docs/superpowers/plans/2026-03-11-nuxt-migration.md

# Nuxt 3 Migration Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Migrate the Vue 3 + Vite SSG portfolio site to Nuxt 3 with static prerendering (`nuxt generate`) for GitHub Pages deployment.

**Architecture:** Nuxt 3 with file-based routing, `@unocss/nuxt` for styling, `nuxt-svgo` for SVG components, `@nuxtjs/google-fonts` for self-hosted fonts, and `experimental.viewTransition` for the View Transitions API. D3 wave animation runs client-only via `<ClientOnly>`. All output is prerendered static HTML.

**Tech Stack:** Nuxt 3, UnoCSS, D3.js, LightningCSS, nuxt-svgo, @nuxtjs/google-fonts

**Spec:** `docs/superpowers/specs/2026-03-11-nuxt-migration-design.md`

---

## Chunk 1: Scaffold and Dependencies

### Task 1: Install Nuxt and update package.json

**Files:**
- Modify: `package.json`

- [ ] **Step 1: Remove old dependencies and add Nuxt**

Run:
```bash
pnpm remove vite-ssg vue-router @vueuse/head @vitejs/plugin-vue @vue/compiler-sfc @vue/server-renderer vite-plugin-webfont-dl vite-svg-loader cross-env vite beasties serve-static unplugin-icons d3
pnpm add nuxt @unocss/nuxt nuxt-svgo @nuxtjs/google-fonts
pnpm add -D lightningcss
```

Note: `lightningcss` moves to devDependencies. `d3` umbrella removed (individual `d3-selection`, `d3-shape`, `d3-timer` are kept).

- [ ] **Step 2: Update scripts in package.json**

Change `scripts` in `package.json` to:
```json
{
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt generate",
    "preview": "nuxt preview"
  }
}
```

- [ ] **Step 3: Commit**

```bash
git add package.json pnpm-lock.yaml
git commit -m "chore: swap dependencies from Vite SSG to Nuxt 3"
```

### Task 2: Create nuxt.config.ts

**Files:**
- Create: `nuxt.config.ts`

- [ ] **Step 1: Write nuxt.config.ts**

Create `nuxt.config.ts` at project root:

```ts
export default defineNuxtConfig({
  ssr: true,

  nitro: {
    prerender: {
      routes: ["/", "/about", "/projects"],
    },
  },

  css: ["@unocss/reset/tailwind-compat.css", "~/assets/css/custom.css"],

  modules: ["@unocss/nuxt", "nuxt-svgo", "@nuxtjs/google-fonts"],

  app: {
    head: {
      htmlAttrs: {
        lang: "en",
        class:
          "text-gray-700 transition-colors bg-cream dark:bg-gray-900 dark:text-gray-300",
      },
      link: [{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }],
    },
  },

  experimental: {
    viewTransition: true,
  },

  googleFonts: {
    families: {
      "Playfair Display": { wght: "400..900", ital: "400..900" },
      Roboto: [100, 300, 400, 500, 700, 900],
    },
    download: true,
    display: "swap",
  },

  svgo: {
    defaultImport: "component",
    svgoConfig: {
      plugins: [
        {
          name: "preset-default",
          params: {
            overrides: {
              convertPathData: false,
            },
          },
        },
      ],
    },
  },

  vite: {
    css: {
      transformer: "lightningcss",
    },
    build: {
      cssMinify: "lightningcss",
    },
  },

  compatibilityDate: "2026-03-11",
});
```

- [ ] **Step 2: Commit**

```bash
git add nuxt.config.ts
git commit -m "chore: add nuxt.config.ts with all module configuration"
```

## Chunk 2: File Structure and Assets

### Task 3: Create Nuxt directory structure and move assets

**Files:**
- Create: `app.vue`
- Create: `layouts/` directory
- Create: `pages/` directory
- Create: `components/` directory
- Create: `composables/` directory
- Create: `assets/css/` directory
- Create: `assets/images/` directory
- Move: `favicon.svg` → `public/favicon.svg`
- Move: `src/custom.css` → `assets/css/custom.css`
- Move: `src/images/*.png` → `assets/images/`
- Move: `src/images/ar.svg` → `assets/images/ar.svg`

- [ ] **Step 1: Create directory structure**

```bash
mkdir -p layouts pages components composables assets/css assets/images
```

- [ ] **Step 2: Move assets to Nuxt locations**

```bash
cp src/custom.css assets/css/custom.css
cp src/images/ar.svg assets/images/ar.svg
cp src/images/dozzle.png assets/images/dozzle.png
cp src/images/dtop.png assets/images/dtop.png
cp src/images/phantom.png assets/images/phantom.png
cp src/images/clashleaders.png assets/images/clashleaders.png
cp favicon.svg public/favicon.svg
```

Note: Using `cp` instead of `mv` so the old site still works until cleanup. The `public/` directory already exists with `CNAME` and `resume/`.

- [ ] **Step 3: Create minimal app.vue**

Create `app.vue`:

```vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

- [ ] **Step 4: Commit**

```bash
git add app.vue layouts/ pages/ components/ composables/ assets/ public/favicon.svg
git commit -m "chore: create Nuxt directory structure and copy assets"
```

## Chunk 3: Composable and Components

### Task 4: Create wave multiplier composable

**Files:**
- Create: `composables/useWaves.ts`

- [ ] **Step 1: Write the composable**

Create `composables/useWaves.ts`:

```ts
import { ref } from "vue";

const multiplier = ref(2);

export function useWaveMultiplier() {
  return multiplier;
}
```

- [ ] **Step 2: Commit**

```bash
git add composables/useWaves.ts
git commit -m "feat: add useWaveMultiplier composable for shared wave state"
```

### Task 5: Port Waves component

**Files:**
- Create: `components/Waves.vue`
- Reference: `src/components/Waves.vue` (current source)

- [ ] **Step 1: Write Waves.vue using composable**

Create `components/Waves.vue` — copy from `src/components/Waves.vue` with these changes:
- Replace `export const multipier = ref(2);` with `import { useWaveMultiplier } from '~/composables/useWaves';`
- Add `const multipier = useWaveMultiplier();` inside the component's `data()` setup area
- In the `step()` method, reference `multipier.value` (which now comes from the composable)

The component stays Options API since it heavily uses `this.$el` and D3 DOM manipulation. Full file:

```vue
<template>
  <div></div>
</template>

<script>
import { select } from "d3-selection";
import { line, curveBasis } from "d3-shape";
import { timer } from "d3-timer";
import { useWaveMultiplier } from "~/composables/useWaves";

const d3 = Object.assign({}, { select, line, curveBasis, timer });
const shape = d3.line().curve(d3.curveBasis);
const multipier = useWaveMultiplier();

export default {
  data() {
    return {
      vis: null,
      points: 6,
      mousePosition: [500, 700],
      wavesCount: 4,
      waves: [],
      paths: [],
      pathHeights: [],
      seeds: [],
      w: null,
      h: null,
    };
  },
  mounted() {
    this.vis = d3.select(this.$el).append("svg").attr("pointer-events", "all");

    for (let i = 0; i < this.wavesCount; i++) {
      this.waves.push(
        this.vis
          .append("path")
          .attr("class", "wave")
          .style("opacity", (i + 1) / this.wavesCount / 2),
      );
      this.paths.push([]);
      this.seeds.push(Math.random());
    }

    this.init();
    d3.timer(this.step);

    window.addEventListener("mousemove", (e) => {
      this.mousePosition[0] = Math.min(e.clientX, 200) + 250;
      this.mousePosition[1] = Math.min(e.clientY, 300) + 600;
    });

    window.addEventListener("resize", this.init);
  },
  methods: {
    init() {
      this.w = window.innerWidth;
      this.h = window.innerHeight;
      this.vis.attr("width", this.w).attr("height", this.h);

      for (let i = 0; i < this.wavesCount; i++) {
        const path = this.paths[i];

        path[0] = [-200 * Math.random(), this.h];
        for (let j = 0; j < this.points; j++) {
          path[j + 1] = [(this.w / this.points) * j, path[j + 1] || this.h / 4];
        }
        path[this.points + 1] = [this.w + Math.random() * 200, this.h];
        this.paths[i] = path;
        this.pathHeights[i] = this.h / 2;
      }
    },
    step(elapsed) {
      for (let i = 0; i < this.wavesCount; i++) {
        this.pathHeights[i] += (this.h / multipier.value - (this.mousePosition[1] / 3 + this.mousePosition[0] / 3 + 200) - this.pathHeights[i]) / 10;
        this.update(elapsed, this.pathHeights[i], this.waves[i], this.paths[i], this.seeds[i]);
      }
    },
    update(elapsed, height, wave, path, seed) {
      for (let i = 1; i < this.points + 1; i++) {
        const sinSeed = ((seed / 2 + 0.2) * elapsed) / 6 + (i + (i % 10)) * 100 + seed * 500;
        path[i][1] = Math.sin(sinSeed / 100) * Math.sin(sinSeed / 200) * height + (this.h - 20 - seed * 10);
      }

      wave.attr("d", shape(path));
    },
  },
};
</script>
<style scoped>
div {
  z-index: 1;
  opacity: 0.8;
  position: fixed;
  bottom: -10px;
  view-transition-name: waves;

  :deep(.wave:nth-of-type(1)) {
    fill: #8b7355;
  }

  :deep(.wave:nth-of-type(2)) {
    fill: #b8a590;
  }

  :deep(.wave:nth-of-type(3)) {
    fill: #6b8f9e;
  }

  :deep(.wave:nth-of-type(4)) {
    fill: #2d4a56;
  }
}
</style>
```

- [ ] **Step 2: Commit**

```bash
git add components/Waves.vue
git commit -m "feat: port Waves component to use composable for multiplier"
```

## Chunk 4: Layout and Pages

### Task 6: Create default layout

**Files:**
- Create: `layouts/default.vue`
- Reference: `src/App.vue` (current source)

- [ ] **Step 1: Write layouts/default.vue**

Create `layouts/default.vue`:

```vue
<template>
  <ClientOnly>
    <Waves />
  </ClientOnly>
  <div class="relative z-10 flex flex-col min-h-screen">
    <nav class="flex justify-start mx-6 my-5 gap-x-5 text-base" style="view-transition-name: nav">
      <Logo class="color-gray-800 dark:color-white fill-current mr-auto mt-1" />
      <NuxtLink
        class="no-underline! text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-2 z-2 transition-colors font-medium tracking-wide uppercase text-sm"
        :to="item.path"
        v-for="item in menu"
        :key="item.path"
      >{{ item.title }}</NuxtLink>
    </nav>
    <main class="flex items-center flex-1 p-4 md:p-8">
      <slot />
    </main>
  </div>
</template>

<script setup>
import Logo from "~/assets/images/ar.svg";

const menu = [
  { title: "Home", path: "/" },
  { title: "About Me", path: "/about" },
  { title: "Projects", path: "/projects" },
];
</script>

<style scoped>
nav a {
  position: relative;

  &.router-link-active {
    --at-apply: "text-gray-900 dark:text-white";
  }

  &.router-link-active::before {
    content: " ";
    --at-apply: "absolute w-full h-[2px] bottom-0 left-0 right-0 bg-gray-800 dark:bg-white";
    view-transition-name: selected;
  }
}
</style>
```

Note: Changed `menu` from object to array for cleaner `v-for`. Added `:key`. NuxtLink uses `.router-link-active` class automatically.

- [ ] **Step 2: Commit**

```bash
git add layouts/default.vue
git commit -m "feat: add default layout with nav, waves, and slot"
```

### Task 7: Port Home page

**Files:**
- Create: `pages/index.vue`
- Reference: `src/pages/Home.vue` (current source)

- [ ] **Step 1: Write pages/index.vue**

Create `pages/index.vue`:

```vue
<template>
  <div class="w-auto md:w-3/4 lg:w-1/2">
    <h1 class="mb-6 text-5xl md:text-6xl leading-tight" style="view-transition-name: title">Hi there, my name is Amir.</h1>
    <p class="text-lg text-gray-600 dark:text-gray-400 leading-relaxed">
      I am a software engineer, mentor and parent in San Francisco area. Currently, I am an engineering manager at Netflix. Previously, I have worked at
      <a href="https://about.google" target="_blank" rel="noopener">Google</a>,
      <a target="_blank" rel="noopener" href="https://www.oracle.com/industries/utilities/opower-energy-efficiency/">Opower</a>, Capital One and a few other
      start ups. See my <NuxtLink to="/projects">projects</NuxtLink> for more information.
    </p>
    <div class="flex flex-wrap mt-10 gap-3">
      <a class="btn" href="https://github.com/amir20">
        <div class="i-ph-github-logo-bold"></div>
        <span>GitHub</span>
      </a>
      <a class="btn" href="https://www.linkedin.com/in/amirraminfar">
        <div class="i-ph-linkedin-logo-bold"></div>
        <span>LinkedIn</span>
      </a>
      <a class="btn" href="https://stackoverflow.com/users/419075/amir-raminfar">
        <div class="i-ph-stack-overflow-logo-bold"></div>
        <span>Stack Overflow</span>
      </a>
    </div>
  </div>
</template>

<script setup>
import { useWaveMultiplier } from "~/composables/useWaves";

useHead({
  title: "This is Amir | AmirRaminfar.me",
});

onMounted(() => {
  useWaveMultiplier().value = 9;
});
</script>

<style scoped>
.btn {
  --at-apply: text-gray-700 dark:text-gray-200 py-2.5 px-5 rounded-lg inline-flex items-center no-underline border border-gray-300 dark:border-gray-600 "hover:bg-gray-800 hover:text-white hover:border-gray-800 dark:hover:bg-white dark:hover:text-gray-900 dark:hover:border-white" transition-all duration-200 gap-2 text-sm font-medium tracking-wide;
}
</style>
```

- [ ] **Step 2: Commit**

```bash
git add pages/index.vue
git commit -m "feat: port Home page to Nuxt"
```

### Task 8: Port About page

**Files:**
- Create: `pages/about.vue`
- Reference: `src/pages/About.vue` (current source)

- [ ] **Step 1: Write pages/about.vue**

Create `pages/about.vue`:

```vue
<template>
  <div class="w-auto md:w-3/4 lg:w-1/2">
    <h1 class="text-5xl md:text-6xl mb-2 leading-tight" style="view-transition-name: title">Hello there.</h1>
    <div class="prose text-lg max-w-full text-gray-600 dark:text-gray-400 leading-relaxed">
      <p>
        I am a software engineer and manager with 18 years of experience, having worked at startups, large corporations, and as a co-founder of my own startup.
        My projects have ranged from web development to infrastructure, and I have managed engineering teams and served as a technical lead. I am passionate
        about building products and teams, constantly seeking new challenges and opportunities for growth.
      </p>

      <p>
        Born in Iran and raised in Seattle, I started programming in high school and went on to study computer science at George Washington University. My
        career began at AOL, where I worked on movies, aol.com, music, and AIM. In 2011, I joined Opower to lead the development of the Home Energy Reports
        product. Later, I co-founded Tilofy in 2014, creating a news recommendation engine for real-time content. I joined Google in 2019, where I worked on
        internal CRM systems and led the growth team for Assistant Auto. Currently, I am a manager at Netflix, leading the Discovery Platform team.
      </p>

      <p>
        I live in the East Bay with my wife and two kids. In my free time, I enjoy weightlifting, playing games, and working on open source projects, which keep
        me engaged and connected to the broader tech community.
      </p>
    </div>
  </div>
</template>

<script setup>
import { useWaveMultiplier } from "~/composables/useWaves";

useHead({
  title: "About | AmirRaminfar.me",
});

onMounted(() => {
  useWaveMultiplier().value = 2;
});
</script>
```

- [ ] **Step 2: Commit**

```bash
git add pages/about.vue
git commit -m "feat: port About page to Nuxt"
```

### Task 9: Port Projects page

**Files:**
- Create: `pages/projects.vue`
- Reference: `src/pages/Projects.vue` (current source)

- [ ] **Step 1: Write pages/projects.vue**

Create `pages/projects.vue`:

```vue
<template>
  <div class="w-auto md:w-4/5 lg:w-2/3">
    <h1 class="mb-10 text-5xl md:text-6xl leading-tight" style="view-transition-name: title">Projects</h1>
    <div>
      <ul class="space-y-10 text-lg">
        <li class="md:flex" v-for="project in projects" :key="project.title">
          <div class="md:flex-shrink-0">
            <a class="mt-1 font-medium leading-tight" :href="project.link">
              <img :src="project.image" :alt="project.imageAlt" width="320" height="165" class="object-cover w-full rounded-lg shadow-sm md:w-60 border border-gray-200 dark:border-gray-700" />
            </a>
          </div>
          <div class="mt-8 md:mt-0 md:ml-8">
            <a class="mt-1 text-lg font-medium leading-tight" :href="project.link">{{ project.title }}</a>
            <p class="mt-2 text-lg text-gray-600 dark:text-gray-400 leading-relaxed" v-html="project.description"></p>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { useWaveMultiplier } from "~/composables/useWaves";
import dozzle from "~/assets/images/dozzle.png";
import dtop from "~/assets/images/dtop.png";
import phantom from "~/assets/images/phantom.png";
import clashleaders from "~/assets/images/clashleaders.png";

useHead({
  title: "Projects | AmirRaminfar.me",
});

onMounted(() => {
  useWaveMultiplier().value = 1.5;
});

const projects = [
  {
    title: "Dozzle for Docker",
    link: "https://dozzle.dev/",
    image: dozzle,
    imageAlt: "Dozzle screenshot",
    description: `Dozzle is a web application that provides an interface to monitor your
        <a href="https://www.docker.com/">Docker</a> containers and logs. It supports multiple hosts, Docker Swarm and Kubernetes.`,
  },
  {
    title: "dtop",
    link: "https://dtop.dev/",
    image: dtop,
    imageAlt: "dtop screenshot",
    description: `dtop is a TUI application for monitoring Docker containers across multiple hosts with real-time CPU, memory, and network metrics. Built with Rust which uses minimal dependencies and is highly performant.`,
  },
  {
    title: "PhantomJS integration for NodeJS",
    link: "https://github.com/amir20/phantomjs-node",
    image: phantom,
    imageAlt: "Phantom screenshot",
    description: `Phantom is a module for integrating <a href="https://phantomjs.org/">PhantomJs</a> with Node. This project is no longer maintained because
        PhantomJs has been deprecated.`,
  },
  {
    title: "Clashleaders.com",
    link: "https://www.clashleaders.com/",
    image: clashleaders,
    imageAlt: "Clashleaders screenshot",
    description: `Clashleaders was a website for the <a href="https://supercell.com/en/games/clashofclans/">Clash of Clans</a> game, created to help players track their progress and compete. However, it became too time-consuming to maintain, so I decided to shut it down and focus on other projects.`,
  },
];
</script>
```

- [ ] **Step 2: Commit**

```bash
git add pages/projects.vue
git commit -m "feat: port Projects page to Nuxt"
```

## Chunk 5: Verify and Cleanup

### Task 10: Verify static generation works

- [ ] **Step 1: Run nuxt generate**

```bash
pnpm build
```

Expected: Build completes successfully, generates `.output/public/` with `index.html`, `about/index.html`, `projects/index.html`, plus CNAME and resume files.

- [ ] **Step 2: Check generated output**

```bash
ls .output/public/
ls .output/public/about/
ls .output/public/projects/
cat .output/public/CNAME
```

Expected: All HTML files present. CNAME contains `amirraminfar.com`. Resume directory preserved.

- [ ] **Step 3: Run dev server and visually verify**

```bash
pnpm dev
```

Manually check:
- Home page loads with waves animation, social buttons, correct heading
- About page loads with bio text
- Projects page loads with project images and descriptions
- Navigation works between pages with view transitions
- Dark mode works
- Waves respond to mouse movement

### Task 11: Clean up old files

**Files:**
- Remove: `src/` (entire directory)
- Remove: `vite.config.ts`
- Remove: `jsconfig.json`
- Remove: `index.html`
- Remove: root-level `favicon.svg` (now in `public/`)
- Modify: `.gitignore`
- Modify: `netlify.toml`

- [ ] **Step 1: Remove old source files**

```bash
rm -rf src/
rm vite.config.ts
rm jsconfig.json
rm index.html
rm favicon.svg
```

- [ ] **Step 2: Update .gitignore**

Replace contents of `.gitignore` with:

```
node_modules
.nuxt/
.output/
.nitro/
bun.lock
```

- [ ] **Step 3: Update netlify.toml**

Replace contents of `netlify.toml` with:

```toml
[build]
command = "pnpm build"
publish = ".output/public"
```

- [ ] **Step 4: Verify build still works after cleanup**

```bash
pnpm build
```

Expected: Build succeeds with no errors.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "chore: remove old Vite SSG files and update config for Nuxt"
```

### Task 12: Final verification

- [ ] **Step 1: Run dev server one final time**

```bash
pnpm dev
```

Verify all pages, navigation, waves, dark mode, and view transitions work.

- [ ] **Step 2: Run generate one final time**

```bash
pnpm build
```

Verify clean build with all routes prerendered.

- [ ] **Step 3: Preview generated site**

```bash
pnpm preview
```

Verify the static output serves correctly.

---

## File: docs/superpowers/specs/2026-03-11-nuxt-migration-design.md

# Nuxt 3 Migration Design

## Goal

Migrate the personal portfolio site from Vue 3 + Vite SSG to Nuxt 3 with static prerendering (`nuxt generate`). The output is pure static HTML deployed to GitHub Pages. No server runtime.

## Constraints

- GitHub Pages hosting: no Node server, prerendered HTML only
- 1:1 feature parity with current site — no new features
- Preserve all current styling (UnoCSS, custom CSS, dark mode, view transitions)
- Preserve D3 animated waves (client-only)
- Self-hosted fonts (match current `vite-plugin-webfont-dl` behavior)

## File Structure

```
├── app.vue                    # Root component (minimal, just <NuxtLayout> + <NuxtPage>)
├── layouts/
│   └── default.vue            # Nav + Waves + <slot> (from current App.vue)
├── pages/
│   ├── index.vue              # Home (from Home.vue)
│   ├── about.vue              # About (from About.vue)
│   └── projects.vue           # Projects (from Projects.vue)
├── components/
│   └── Waves.vue              # D3 wave background (auto-imported)
├── composables/
│   └── useWaves.ts            # Shared wave multiplier state
├── assets/
│   ├── css/
│   │   └── custom.css         # Global typography and link styles
│   └── images/
│       ├── ar.svg
│       ├── dozzle.png
│       ├── dtop.png
│       ├── phantom.png
│       └── clashleaders.png
├── public/
│   ├── favicon.svg            # Static favicon (moved from project root)
│   ├── CNAME                  # GitHub Pages custom domain (amirraminfar.com) — preserved
│   └── resume/                # Resume PDF and LaTeX source — preserved
│       ├── amir_raminfar_resume.pdf
│       └── amir_raminfar_resume.tex
├── nuxt.config.ts             # Nuxt configuration
├── uno.config.ts              # UnoCSS config (unchanged)
└── package.json
```

## Component Migration

### app.vue

Minimal root:

```vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

### layouts/default.vue

Derived from current `App.vue`:

```vue
<template>
  <ClientOnly>
    <Waves />
  </ClientOnly>
  <div class="relative z-10 flex flex-col min-h-screen">
    <nav class="flex justify-start mx-6 my-5 gap-x-5 text-base" style="view-transition-name: nav">
      <Logo class="color-gray-800 dark:color-white fill-current mr-auto mt-1" />
      <NuxtLink
        class="no-underline! text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-2 z-2 transition-colors font-medium tracking-wide uppercase text-sm"
        :to="path"
        v-for="{ title, path } in menu"
      >{{ title }}</NuxtLink>
    </nav>
    <main class="flex items-center flex-1 p-4 md:p-8">
      <slot />
    </main>
  </div>
</template>

<script setup>
import Logo from '~/assets/images/ar.svg'

const menu = {
  home: { title: 'Home', path: '/' },
  about: { title: 'About Me', path: '/about' },
  projects: { title: 'Projects', path: '/projects' },
}
</script>

<style scoped>
nav a {
  position: relative;

  &.router-link-active {
    --at-apply: "text-gray-900 dark:text-white";
  }

  &.router-link-active::before {
    content: " ";
    --at-apply: "absolute w-full h-[2px] bottom-0 left-0 right-0 bg-gray-800 dark:bg-white";
    view-transition-name: selected;
  }
}
</style>
```

Key changes from `App.vue`:
- `<router-view>` becomes `<slot />`
- `<router-link>` becomes `<NuxtLink>`
- `defineAsyncComponent` for Waves replaced by `<ClientOnly>` wrapper
- `aria-current="page"` selector becomes `.router-link-active` (Nuxt's default)
- Scale transition CSS classes removed (replaced by Nuxt view transitions)

### pages/index.vue

From `Home.vue`. Refactored to `<script setup>` with Nuxt auto-imports:

```vue
<script setup>
import { useWaveMultiplier } from '~/composables/useWaves'

useHead({
  title: 'This is Amir | AmirRaminfar.me',
})

onMounted(() => {
  useWaveMultiplier().value = 9
})
</script>
```

- `useHead()` auto-imported from Nuxt (no import needed)
- `onMounted` auto-imported from Vue
- `<router-link>` becomes `<NuxtLink>`

### pages/about.vue

From `About.vue`. Same `<script setup>` refactor pattern.

### pages/projects.vue

From `Projects.vue`:
- Image imports change from `import.meta.glob("/src/images/*.png")` to direct imports:

```ts
import dozzle from '~/assets/images/dozzle.png'
import dtop from '~/assets/images/dtop.png'
import phantom from '~/assets/images/phantom.png'
import clashleaders from '~/assets/images/clashleaders.png'
```

### composables/useWaves.ts

Extract the shared `multipier` ref from `Waves.vue` into a composable to avoid module-level state sharing issues during SSR prerendering:

```ts
const multiplier = ref(2)

export function useWaveMultiplier() {
  return multiplier
}
```

`Waves.vue` and all pages import from here instead of exporting/importing a ref from the component file.

### components/Waves.vue

Mostly unchanged. Key changes:
- Remove the exported `multipier` ref; import from `~/composables/useWaves` instead
- Must always be inside `<ClientOnly>` since it uses `window`, `document`, D3 DOM APIs, and `this.$el`

### SVG Logo (ar.svg)

Use `nuxt-svgo` module for inline SVG component behavior (same as current `vite-svg-loader`). Preserve the SVGO config that disables `convertPathData` to avoid SVG rendering changes.

## Configuration

### nuxt.config.ts

```ts
export default defineNuxtConfig({
  ssr: true,

  nitro: {
    prerender: {
      routes: ['/', '/about', '/projects'],
    },
  },

  css: [
    '@unocss/reset/tailwind-compat.css',
    '~/assets/css/custom.css',
  ],

  modules: [
    '@unocss/nuxt',
    'nuxt-svgo',
    '@nuxtjs/google-fonts',
  ],

  app: {
    head: {
      htmlAttrs: {
        lang: 'en',
        class: 'text-gray-700 transition-colors bg-cream dark:bg-gray-900 dark:text-gray-300',
      },
      link: [
        { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
      ],
    },
  },

  // Native View Transitions API (replaces manual router.beforeResolve hook)
  experimental: {
    viewTransition: true,
  },

  // Self-host Google Fonts at build time (matches vite-plugin-webfont-dl behavior)
  googleFonts: {
    families: {
      'Playfair Display': { wght: '400..900', ital: '400..900' },
      'Roboto': [100, 300, 400, 500, 700, 900],
    },
    download: true,
    display: 'swap',
  },

  svgo: {
    defaultImport: 'component',
    svgoConfig: {
      plugins: [
        {
          name: 'preset-default',
          params: {
            overrides: {
              convertPathData: false,
            },
          },
        },
      ],
    },
  },

  // Preserve LightningCSS behavior from current Vite config
  vite: {
    css: {
      transformer: 'lightningcss',
    },
    build: {
      cssMinify: 'lightningcss',
    },
  },

  compatibilityDate: '2026-03-11',
})
```

### UnoCSS

`uno.config.ts` stays unchanged. The `@unocss/nuxt` module picks it up automatically.

### View Transitions

Use Nuxt's `experimental: { viewTransition: true }` which provides the same `document.startViewTransition()` wrapping that the current `router.beforeResolve` hook does. The `style="view-transition-name: ..."` attributes on elements stay as-is.

## Packages

### Remove
- `vite-ssg`
- `vue-router` (bundled with Nuxt)
- `@vueuse/head` (Nuxt has `useHead` built-in)
- `@vitejs/plugin-vue` (bundled with Nuxt)
- `@vue/compiler-sfc` (bundled with Nuxt)
- `@vue/server-renderer` (bundled with Nuxt)
- `vite-plugin-webfont-dl` (replaced by `@nuxtjs/google-fonts`)
- `vite-svg-loader` (replaced by `nuxt-svgo`)
- `cross-env` (not needed)
- `vite` (bundled with Nuxt)
- `beasties` (Nuxt handles critical CSS)
- `serve-static` (not needed)
- `lightningcss` (keep as devDep for Vite config, but remove from dependencies)
- `unplugin-icons` (unused — site uses UnoCSS preset-icons via class names)
- `d3` (unused — only the individual `d3-selection`, `d3-shape`, `d3-timer` packages are imported)

### Add
- `nuxt`
- `@unocss/nuxt`
- `nuxt-svgo`
- `@nuxtjs/google-fonts`

### Keep
- `d3-selection`, `d3-shape`, `d3-timer`
- `unocss`, `@unocss/reset`, `@unocss/transformer-directives`
- `@iconify-json/ph`
- `vue` (peer dep)
- `lightningcss` (as devDep for Vite CSS config)

## Cleanup

Files to remove after migration:
- `src/` (entire directory — all content moved to Nuxt structure)
- `vite.config.ts` (replaced by `nuxt.config.ts`)
- `jsconfig.json` (Nuxt auto-generates `.nuxt/tsconfig.json`)
- `index.html` (Nuxt generates HTML)

Files to update:
- `.gitignore`: replace `.vite-ssg-temp/` with `.nuxt/`, `.output/`, `.nitro/`
- `netlify.toml`: update build command to `nuxt generate` and publish dir to `.output/public`

Files to preserve in `public/`:
- `CNAME` (custom domain for GitHub Pages)
- `resume/` directory (PDF and LaTeX source)
- `favicon.svg` (move from project root to `public/`)

## Deployment

`nuxt generate` outputs to `.output/public/`. GitHub Pages / Netlify should be configured to serve from this directory.

### Scripts

```json
{
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt generate",
    "preview": "nuxt preview"
  }
}
```

## Migration Steps (High Level)

1. Scaffold Nuxt config and install dependencies
2. Create Nuxt directory structure (`layouts/`, `pages/`, `components/`, `composables/`, `assets/`)
3. Move static assets: `favicon.svg` to `public/`, images to `assets/images/`, CSS to `assets/css/`
4. Create `composables/useWaves.ts` for shared wave state
5. Port `App.vue` to `app.vue` + `layouts/default.vue`
6. Port pages to `<script setup>` with Nuxt auto-imports (Home -> index, About -> about, Projects -> projects)
7. Port `Waves.vue` to use composable instead of exported ref
8. Configure `nuxt-svgo` for SVG logo
9. Verify `nuxt generate` produces correct static output for all 3 routes
10. Clean up: remove `src/`, `vite.config.ts`, `jsconfig.json`, `index.html`
11. Update `.gitignore` and `netlify.toml`

## Out of Scope

- Blog / content system (future work)
- New pages or features
- Visual design changes
- CI/CD pipeline changes (beyond build command and publish dir)

---

## File: docs/superpowers/specs/2026-03-15-raindrop-effect-design.md

# Raindrop Effect Design

## Summary

Add a canvas-based animated raindrop overlay that simulates light rain sliding down a window. Layers on top of existing noise effect. Drops interact with underlying wave colors via blend modes.

## Details

### Composable: `composables/useRaindrops.ts`

- Creates a full-viewport `<canvas>` element, fixed position, `pointer-events: none`
- Z-index between content and noise (z-index: 9998)
- Canvas uses `mix-blend-mode: overlay` to pick up wave colors

### Drop Model

- 5-10 drops active at a time
- Each drop: x, y, radius (2-5px), speed (0.5-2px/frame), opacity (0.3-0.7), horizontal drift, trail positions
- Spawn at random x near top, slide down with slight drift
- Respawn at top when exiting bottom

### Trail

- Each drop stores last 15-20 positions
- Trail rendered as progressively smaller, more transparent circles
- Fades quickly for subtlety

### Rendering

- `requestAnimationFrame` loop
- Canvas cleared and redrawn each frame
- Drops are white semi-transparent with bright highlight spot (refraction look)
- Radial gradient per drop: bright center fading to transparent edge

### Integration

- Called from `layouts/default.vue` alongside `useNoise()`
- Resizes canvas on window resize
- Works in both dark and light mode (white highlights + blend mode)

### Performance

- Max 10 drops + trails = ~200 draw calls per frame — negligible for canvas
- Single composable, no DOM thrashing

---

## File: .github/renovate.json

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base", "schedule:weekly", "group:allNonMajor"],
  "rangeStrategy": "bump"
}