## Identity

- Name: header-footer
- ID: 1e9bb032808f
- Type: general-purpose
- Team: portfolio-tet-2026
- Leader: sagi

## Workspace
- Working directory: /home/node/.clawteam/workspaces/portfolio-tet-2026/header-footer
- Branch: clawteam/portfolio-tet-2026/header-footer
- This is an isolated git worktree. Your changes do not affect the main branch.

## Task

Task 4: Build Header & Footer components. Follow implementation-plan.md.

## Context

# Git Context for header-footer
## Upstream Dependency Changes
- init: init: 0 file(s), +0/-0, 0 commit(s)

## Coordination Protocol

- Use `clawteam task list portfolio-tet-2026 --owner header-footer` to see your tasks.
- Starting a task: `clawteam task update portfolio-tet-2026 <task-id> --status in_progress`
- Before marking a task completed, commit your changes in this repository with git.
- Use a clear commit message, e.g. `git add -A && git commit -m "Implement <task summary>"`.
- Finishing a task: `clawteam task update portfolio-tet-2026 <task-id> --status completed`
- When you finish all tasks, send a summary to the leader:
  `clawteam inbox send portfolio-tet-2026 sagi "All tasks completed. <brief summary>"`
- If you are blocked or need help, message the leader:
  `clawteam inbox send portfolio-tet-2026 sagi "Need help: <description>"`
- After finishing work, report your costs: `clawteam cost report portfolio-tet-2026 --input-tokens <N> --output-tokens <N> --cost-cents <N>`
- Before finishing, save your session: `clawteam session save portfolio-tet-2026 --session-id <id>`
- Do not exit after the first task unless the leader explicitly tells you to stop.

## Worker Loop Protocol

- After finishing your current task batch, re-check `clawteam task list portfolio-tet-2026 --owner header-footer`.
- Then check for new instructions with `clawteam inbox receive portfolio-tet-2026 --agent header-footer`.
- If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking for new work.
- Repeat this loop until the leader confirms shutdown or there is truly no more work to do.


The user is providing me with task instructions for a specific component - building Header & Footer components as part of a portfolio-tet-2026 project. This appears to be a ClawTeam multi-agent coordination setup where I'm the "header-footer" agent working under a team leader "sagi".

Let me break down what I need to do:

1. I'm in an isolated git worktree at `/home/node/.clawteam/workspaces/portfolio-tet-2026/header-footer`
2. I need to follow `implementation-plan.md` for Task 4
3. I need to use the ClawTeam CLI to manage my tasks and communicate with the leader
4. The workflow includes:
   - Checking task list: `clawteam task list portfolio-tet-2026 --owner header-footer`
   - Starting task: `clawteam task update portfolio-tet-2026 <task-id> --status in_progress`
   - Committing changes with clear message
   - Finishing task: `clawteam task update portfolio-tet-2026 <task-id> --status completed`
   - Sending summary to leader when all tasks done
   - Following worker loop protocol

First, I should read the implementation plan to understand what needs to be built. I need to start by exploring the workspace and reading the plan.

Let me start by reading the implementation-plan.md file to understand the requirements.

I'll start by reading the implementation plan and exploring the workspace to understand the task.


# Portfolio Tết Theme - Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development OR superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.

**Goal:** Build a fully polished, production-ready portfolio with Next.js 14, Tailwind, Framer Motion, and Tết theme.

**Architecture:** Next.js App Router with TypeScript, components in `components/`, data in `data/`, effects in `components/TetEffects.tsx`. Single-page Home with scroll sections; separate pages for Projects and My Corner blog.

**Tech Stack:** Next.js 14+, TypeScript, Tailwind CSS, Framer Motion, Google Fonts.

---

### Task 1: Initialize Next.js project

**Files:**
- Create: `package.json`
- Create: `next.config.js`
- Create: `tsconfig.json`
- Create: `tailwind.config.ts`
- Create: `postcss.config.mjs`
- Create: `app/layout.tsx`
- Create: `app/page.tsx`
- Create: `app/globals.css`

- [ ] **Step 1:** Run create-next-app with TypeScript and Tailwind

```bash
cd /home/node/.openclaw/workspace/projects/clawteam-portfolio
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --no-turbopack --import-alias "@/*"
```

Accept defaults. This creates the base structure.

- [ ] **Step 2:** Install additional dependencies

```bash
npm install framer-motion
npm install -D @types/node
```

- [ ] **Step 3:** Configure Tailwind colors for Tết theme

Edit `tailwind.config.ts`:

```ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#FFEBEE',
          100: '#FFCDD2',
          200: '#EF9A9A',
          300: '#E57373',
          400: '#EF5350',
          500: '#F44336',
          600: '#D32F2F',
          700: '#C62828',
          800: '#B71C1C',
          900: '#FFCDD2',
        },
        accent: {
          50: '#FFF8E1',
          100: '#FFECB3',
          200: '#FFE082',
          300: '#FFD54F',
          400: '#FFCA28',
          500: '#FFC107',
          600: '#FFB300',
          700: '#FFA000',
          800: '#FF8F00',
          900: '#FF6F00',
        },
      },
      fontFamily: {
        heading: ['"Playfair Display"', 'serif'],
        body: ['"Inter"', 'sans-serif'],
      },
    },
  },
  plugins: [],
};
export default config;
```

- [ ] **Step 4:** Load Google Fonts in `app/layout.tsx`

Edit `app/layout.tsx`:

```tsx
import type { Metadata } from "next";
import { Inter, Playfair_Display } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const playfair = Playfair_Display({
  subsets: ["latin"],
  variable: "--font-playfair",
});

export const metadata: Metadata = {
  title: "Trí Vĩ | AI Engineer",
  description: "Personal portfolio of Nguyễn Ngọc Trí Vĩ - AI Engineer",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${inter.variable} ${playfair.variable} font-body`}>
        {children}
      </body>
    </html>
  );
}
```

- [ ] **Step 5:** Set base styles in `app/globals.css`

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --font-inter: var(--font-inter);
  --font-playfair: var(--font-playfair);
}

body {
  @apply bg-white text-gray-900;
}

h1, h2, h3, h4, h5, h6 {
  @apply font-heading;
}
```

- [ ] **Step 6:** Commit initial setup

```bash
git add .
git commit -m "feat: init Next.js project with Tailwind and fonts"
```

---

### Task 2: Create data layer

**Files:**
- Create: `src/data/profile.ts`
- Create: `src/data/experience.ts`
- Create: `src/data/projects.ts`
- Create: `src/data/blogPosts.ts`

- [ ] **Step 1:** Copy avatar image to `public/avatar.jpeg` (already exists in projects dir)

```bash
cp /home/node/.openclaw/workspace/projects/clawteam-portfolio/avatar.jpeg public/avatar.jpeg
```

- [ ] **Step 2:** Create `src/data/profile.ts`

```ts
export const profile = {
  name: "Nguyễn Ngọc Trí Vĩ",
  title: "AI Engineer",
  avatar: "/avatar.jpeg",
  email: "nntrivi2001@gmail.com",
  phone: "090 321 5095",
  linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi",
  portfolio: "https://nguyenngoctrivi.vercel.app",
  languages: ["Vietnamese (native)", "English (intermediate)"],
  bio: `I'm an AI Engineer passionate about applying cutting-edge machine learning to real-world problems. With a strong foundation in Computer Vision and AI research, I've built solutions for industrial quality inspection, trading automation, and educational AI tools. I love exploring new technologies and turning complex ideas into practical, impactful products.`,
};
```

- [ ] **Step 3:** Create `src/data/experience.ts`

```ts
export interface Experience {
  id: number;
  company: string;
  role: string;
  startDate: string;
  endDate: string;
  description: string[];
}

export const experience: Experience[] = [
  {
    id: 1,
    company: "SmartGift Solution",
    role: "AI Engineer",
    startDate: "Mar 2026",
    endDate: "Present",
    description: [
      "Research and deploy AI assistants to improve development team productivity.",
      "Explore and apply suitable technologies for business problems, build POCs, and document practical recommendations.",
    ],
  },
  {
    id: 2,
    company: "Learning Chain Vietnam",
    role: "Applied AI Researcher & Operations Associate",
    startDate: "Jul 2025",
    endDate: "Mar 2026",
    description: [
      "Research and implement AI for trading automation and operations support.",
      "Explore and execute suitable tech, build POCs and document actionable recommendations.",
      "Design and teach applied AI training programs, workshops, and n8n templates.",
      "Support COO in optimizing workflows, reporting, and cross-team process standardization.",
    ],
  },
  {
    id: 3,
    company: "Vườn đậu - Thương hiệu Sữa đậu nành Thảo mộc",
    role: "Founder",
    startDate: "Oct 2024",
    endDate: "Apr 2025",
    description: [
      "Managed entire production and business process: sourcing, manufacturing, packaging, distribution, and inventory control.",
      "Planned raw material procurement, production schedules, and warehousing to optimize costs and maintain product quality.",
      "Developed pricing strategy and direct sales channels. Achieved strong customer satisfaction with average repurchase rate of 2.51x per customer.",
    ],
  },
  {
    id: 4,
    company: "VSTECH Company Limited",
    role: "Computer Vision Engineer",
    startDate: "Aug 2023",
    endDate: "Feb 2025",
    description: [
      "Nozzle Quality Inspection: Built for Japanese client on production line.",
      "3D Vision Integration: Measured Samsung LED panel height with ±7% accuracy.",
      "OCR Solutions: Read package codes and text labels with high accuracy in industrial environment.",
    ],
  },
];
```

- [ ] **Step 4:** Create `src/data/projects.ts`

```ts
export interface Project {
  id: number;
  title: string;
  description: string;
  tech: string[];
  demoUrl?: string;
  githubUrl?: string;
  image?: string;
}

export const projects: Project[] = [
  {
    id: 1,
    title: "Nozzle Quality Inspection System",
    description:
      "Computer vision system for automatic nozzle defect detection on production lines. Deployed at Japanese manufacturing facility.",
    tech: ["Python", "OpenCV", "TensorFlow", "Industrial Camera"],
    demoUrl: "https://nguyenngoctrivi.vercel.app",
    githubUrl: "https://github.com/yourusername/nozzle-inspection",
  },
  {
    id: 2,
    title: "3D Panel Measurement",
    description:
      "3D vision integration to measure LED panel height with ±7% accuracy for Samsung.",
    tech: ["3D Vision", "Python", "C++", "Point Cloud Processing"],
  },
  {
    id: 3,
    title: "OCR for Package Codes",
    description:
      "High-accuracy OCR for reading package codes and text labels in industrial environments.",
    tech: ["OCR", "Python", "OpenCV", "Deep Learning"],
  },
  {
    id: 4,
    title: "AI Trading Assistant",
    description:
      "Research and implementation of AI-driven trading automation and operations support.",
    tech: ["Python", "n8n", "PineScript", "Trading APIs"],
  },
  {
    id: 5,
    title: "Applied AI Training Program",
    description:
      "Designed and delivered applied AI training courses, workshops, and learning assets (n8n templates).",
    tech: ["AI Education", "n8n", "Workshop Design"],
  },
];
```

- [ ] **Step 5:** Create `src/data/blogPosts.ts`

```ts
export interface BlogPost {
  slug: string;
  title: string;
  date: string;
  excerpt: string;
  content: string;
  image?: string;
  tags: string[];
}

export const blogPosts: BlogPost[] = [
  {
    slug: "journey-into-ai",
    title: "My Journey into AI",
    date: "2025-12-15",
    excerpt:
      "Reflecting on how I got started with AI, from university days to real-world deployments.",
    content: `
# My Journey into AI

It all started at university...

## The Beginning

I was fascinated by how machines could learn...

## First Project

My first computer vision project taught me...

## Lessons Learned

- Start with the fundamentals
- Build real things
- Never stop learning

This journey has been incredible, and I'm excited for what's next.
    `.trim(),
    tags: ["personal", "ai", "career"],
  },
  {
    slug: "tet-vibes-2025",
    title: "Tết 2025 - A Time of Renewal",
    date: "2025-01-28",
    excerpt:
      "Celebrating Lunar New Year with family, food, and wishes for the year ahead.",
    content: `
# Tết 2025 - A Time of Renewal

Tết is always a special time...

## Family Reunion

Returning home, sharing meals...

## Traditions

Visiting relatives, lucky money...

## Looking Forward

New year, new goals, new hope.

Chúc mừng năm mới! 🎊
    `.trim(),
    image: "/images/tet-2025.jpg",
    tags: ["personal", "tet", "life"],
  },
];
```

- [ ] **Step 6:** Commit data layer

```bash
git add src/data/ public/avatar.jpeg
git commit -m "feat: add data layer (profile, experience, projects, blog)"
```

---

### Task 3: Build TetEffects component (falling petals + subtle fireworks)

**Files:**
- Create: `src/components/TetEffects.tsx`

- [ ] **Step 1:** Create falling petals canvas component

```tsx
"use client";

import { useEffect, useRef } from "react";

interface Petal {
  x: number;
  y: number;
  size: number;
  speedY: number;
  speedX: number;
  rotation: number;
  rotationSpeed: number;
  opacity: number;
}

export default function TetEffects() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const resize = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    };
    resize();
    window.addEventListener("resize", resize);

    const petals: Petal[] = [];
    const colors = ["#F44336", "#E91E63", "#D32F2F", "#FFC107", "#FFEB3B"]; // reds and golds

    const createPetal = () => ({
      x: Math.random() * canvas.width,
      y: -10,
      size: 4 + Math.random() * 8,
      speedY: 0.5 + Math.random() * 1.5,
      speedX: (Math.random() - 0.5) * 0.5,
      rotation: Math.random() * Math.PI * 2,
      rotationSpeed: (Math.random() - 0.5) * 0.02,
      opacity: 0.4 + Math.random() * 0.4,
    });

    // Initial petals
    for (let i = 0; i < 50; i++) {
      const p = createPetal();
      p.y = Math.random() * canvas.height;
      petals.push(p);
    }

    let animationFrameId: number;

    const animate = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      petals.forEach((petal, idx) => {
        // Update
        petal.y += petal.speedY;
        petal.x += petal.speedX;
        petal.rotation += petal.rotationSpeed;

        // Reset if out of bounds
        if (petal.y > canvas.height + 10) {
          petals[idx] = createPetal();
        }

        // Draw petal (simple circle or ellipse)
        ctx.save();
        ctx.translate(petal.x, petal.y);
        ctx.rotate(petal.rotation);
        ctx.globalAlpha = petal.opacity;
        ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
        ctx.beginPath();
        ctx.ellipse(0, 0, petal.size, petal.size / 2, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      });

      animationFrameId = requestAnimationFrame(animate);
    };

    animate();

    return () => {
      window.removeEventListener("resize", resize);
      cancelAnimationFrame(animationFrameId);
    };
  }, []);

  return (
    <canvas
      ref={canvasRef}
      className="fixed inset-0 pointer-events-none z-50"
      style={{ opacity: 0.6 }}
    />
  );
}
```

- [ ] **Step 2:** Add subtle fireworks (sparkles) on idle/scroll

Enhance the same canvas in `TetEffects`:

```tsx
// Inside useEffect after petal animation:

const sparkles: { x: number; y: number; life: number; maxLife: number; size: number }[] = [];

const createSparkle = (x?: number, y?: number) => ({
  x: x ?? Math.random() * canvas.width,
  y: y ?? Math.random() * canvas.height,
  life: 1,
  maxLife: 60 + Math.random() * 60,
  size: 1 + Math.random() * 2,
});

// Occasionally create sparkles
setInterval(() => {
  if (sparkles.length < 20) {
    sparkles.push(createSparkle());
  }
}, 300);

// In animate loop, after petals:
sparkles.forEach((sparkle, idx) => {
  sparkle.life--;
  if (sparkle.life <= 0) {
    sparkles.splice(idx, 1);
    return;
  }
  ctx.save();
  ctx.globalAlpha = sparkle.life / sparkle.maxLife;
  ctx.fillStyle = "#FFC107";
  ctx.beginPath();
  ctx.arc(sparkle.x, sparkle.y, sparkle.size, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();
});
```

(Integrate into the existing animation loop.)

- [ ] **Step 3:** Commit TetEffects

```bash
git add src/components/TetEffects.tsx
git commit -m "feat: add TetEffects component (falling petals + fireworks)"
```

---

### Task 4: Build common UI components

**Files:**
- Create: `src/components/Header.tsx`
- Create: `src/components/Footer.tsx`
- Create: `src/components/ui/Button.tsx` (optional)

- [ ] **Step 1:** Create `src/components/Header.tsx`

```tsx
"use client";

import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Link from "next/link";

export default function Header() {
  const [scrolled, setScrolled] = useState(false);
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 50);
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  const navLinks = [
    { href: "#home", label: "Home" },
    { href: "#about", label: "About" },
    { href: "#experience", label: "Experience" },
    { href: "#contact", label: "Contact" },
    { href: "/projects", label: "Projects" },
    { href: "/my-corner", label: "My Corner" },
  ];

  return (
    <header
      className={`fixed top-0 left-0 right-0 z-40 transition-all duration-300 ${
        scrolled
          ? "bg-white/80 backdrop-blur-md shadow-sm"
          : "bg-transparent"
      }`}
    >
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <Link href="/" className="text-2xl font-bold text-primary-600">
            Trí Vĩ
          </Link>

          {/* Desktop Nav */}
          <nav className="hidden md:flex space-x-8">
            {navLinks.map((link) => (
              <Link
                key={link.href}
                href={link.href}
                className="text-gray-700 hover:text-primary-600 transition-colors font-medium"
              >
                {link.label}
              </Link>
            ))}
          </nav>

          {/* Mobile menu button */}
          <button
            className="md:hidden p-2"
            onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
            aria-label="Toggle menu"
          >
            <svg
              className="w-6 h-6"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              {mobileMenuOpen ? (
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M6 18L18 6M6 6l12 12"
                />
              ) : (
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M4 6h16M4 12h16M4 18h16"
                />
              )}
            </svg>
          </button>
        </div>
      </div>

      {/* Mobile Nav */}
      <AnimatePresence>
        {mobileMenuOpen && (
          <motion.div
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
            className="md:hidden bg-white shadow-lg"
          >
            <div className="px-4 py-4 space-y-2">
              {navLinks.map((link) => (
                <Link
                  key={link.href}
                  href={link.href}
                  className="block py-2 text-gray-700 hover:text-primary-600"
                  onClick={() => setMobileMenuOpen(false)}
                >
                  {link.label}
                </Link>
              ))}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </header>
  );
}
```

- [ ] **Step 2:** Create `src/components/Footer.tsx`

```tsx
import Link from "next/link";

export default function Footer() {
  return (
    <footer className="bg-gradient-to-r from-primary-600 to-primary-800 text-white py-12">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex flex-col md:flex-row justify-between items-center">
          <div className="mb-4 md:mb-0">
            <h3 className="text-2xl font-bold mb-2">Trí Vĩ</h3>
            <p className="text-gray-200">AI Engineer</p>
          </div>
          <div className="text-sm">
            © 2025 - 2026 Nguyễn Ngọc Trí Vĩ. All rights reserved.
          </div>
        </div>
        {/* Decorative lion/dragon silhouette could be added here */}
      </div>
    </footer>
  );
}
```

- [ ] **Step 3:** Commit Header & Footer

```bash
git add src/components/Header.tsx src/components/Footer.tsx
git commit -m "feat: add Header and Footer components"
```

---

### Task 5: Build Hero section

**Files:**
- Create: `src/components/Hero.tsx`

- [ ] **Step 1:** Create Hero with avatar, name, title, CTA buttons

```tsx
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import { profile } from "@/data/profile";

export default function Hero() {
  return (
    <section
      id="home"
      className="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-500 via-accent-400 to-primary-600 overflow-hidden"
    >
      {/* Falling petals overlay will be rendered at app level */}

      <div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
        <motion.div
          initial={{ scale: 0.8, opacity: 0 }}
          animate={{ scale: 1, opacity: 1 }}
          transition={{ duration: 0.8 }}
          className="mb-8"
        >
          <div className="relative w-48 h-48 mx-auto rounded-full overflow-hidden border-4 border-white shadow-2xl">
            <Image
              src={profile.avatar}
              alt={profile.name}
              fill
              className="object-cover"
              priority
            />
          </div>
        </motion.div>

        <motion.h1
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.2 }}
          className="text-5xl md:text-6xl font-heading font-bold text-white mb-4"
        >
          {profile.name}
        </motion.h1>

        <motion.p
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.4 }}
          className="text-2xl md:text-3xl text-gray-100 mb-8"
        >
          {profile.title}
        </motion.p>

        <motion.div
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.6 }}
          className="flex flex-col sm:flex-row gap-4 justify-center"
        >
          <a
            href="/projects"
            className="px-8 py-3 bg-white text-primary-600 rounded-full font-semibold shadow-lg hover:shadow-xl hover:bg-gray-50 transition-all"
          >
            View Projects
          </a>
          <a
            href="#contact"
            className="px-8 py-3 border-2 border-white text-white rounded-full font-semibold hover:bg-white hover:text-primary-600 transition-all"
          >
            Contact Me
          </a>
        </motion.div>
      </div>
    </section>
  );
}
```

- [ ] **Step 2:** Add Avatar image to `public/` (already copied)

- [ ] **Step 3:** Commit Hero

```bash
git add src/components/Hero.tsx
git commit -m "feat: add Hero section component"
```

---

### Task 6: Build About section

**Files:**
- Create: `src/components/About.tsx`

- [ ] **Step 1:** Create About component (2-column layout)

```tsx
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import { profile } from "@/data/profile";

export default function About() {
  return (
    <section id="about" className="py-20 bg-white">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0 }}
          whileInView={{ opacity: 1 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6 }}
          className="text-center mb-12"
        >
          <h2 className="text-4xl font-heading font-bold text-primary-700 mb-4">
            About Me
          </h2>
          <div className="w-24 h-1 bg-accent-500 mx-auto" />
        </motion.div>

        <div className="grid md:grid-cols-2 gap-12 items-center">
          <motion.div
            initial={{ x: -50, opacity: 0 }}
            whileInView={{ x: 0, opacity: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6 }}
            className="flex justify-center"
          >
            <div className="relative w-80 h-80 rounded-2xl overflow-hidden shadow-2xl">
              <Image
                src={profile.avatar}
                alt={profile.name}
                fill
                className="object-cover"
              />
            </div>
          </motion.div>

          <motion.div
            initial={{ x: 50, opacity: 0 }}
            whileInView={{ x: 0, opacity: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6 }}
            className="space-y-6"
          >
            <p className="text-lg text-gray-700 leading-relaxed">
              {profile.bio}
            </p>

            <div>
              <h3 className="text-xl font-semibold text-primary-600 mb-2">
                Languages
              </h3>
              <ul className="list-disc list-inside text-gray-700 space-y-1">
                {profile.languages.map((lang, idx) => (
                  <li key={idx}>{lang}</li>
                ))}
              </ul>
            </div>

            <div>
              <h3 className="text-xl font-semibold text-primary-600 mb-2">
                Contact
              </h3>
              <div className="space-y-2 text-gray-700">
                <p>
                  <strong>Email:</strong>{" "}
                  <a
                    href={`mailto:${profile.email}`}
                    className="text-primary-600 hover:underline"
                  >
                    {profile.email}
                  </a>
                </p>
                <p>
                  <strong>Phone:</strong> {profile.phone}
                </p>
                <p>
                  <strong>LinkedIn:</strong>{" "}
                  <a
                    href={profile.linkedin}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-primary-600 hover:underline"
                  >
                    {profile.linkedin}
                  </a>
                </p>
              </div>
            </div>
          </motion.div>
        </div>
      </div>
    </section>
  );
}
```

- [ ] **Step 2:** Commit About

```bash
git add src/components/About.tsx
git commit -m "feat: add About section component"
```

---

### Task 7: Build Experience (Timeline) component

**Files:**
- Create: `src/components/Experience.tsx`

- [ ] **Step 1:** Create Timeline with vertical line and nodes

```tsx
"use client";

import { motion } from "framer-motion";
import { Experience } from "@/data/experience";

export default function Experience() {
  return (
    <section id="experience" className="py-20 bg-gray-50">
      <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0 }}
          whileInView={{ opacity: 1 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6 }}
          className="text-center mb-16"
        >
          <h2 className="text-4xl font-heading font-bold text-primary-700 mb-4">
            Experience
          </h2>
          <div className="w-24 h-1 bg-accent-500 mx-auto" />
        </motion.div>

        <div className="relative">
          {/* Vertical line */}
          <div className="absolute left-8 md:left-1/2 top-0 bottom-0 w-0.5 bg-primary-300 transform md:-translate-x-1/2" />

          {experience.map((exp, index) => (
            <motion.div
              key={exp.id}
              initial={{ opacity: 0, x: index % 2 === 0 ? -30 : 30 }}
              whileInView={{ opacity: 1, x: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.6, delay: index * 0.1 }}
              className={`relative flex items-center mb-12 ${
                index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse"
              }`}
            >
              {/* Timeline node */}
              <div className="absolute left-6 md:left-1/2 w-5 h-5 bg-accent-500 rounded-full border-4 border-white shadow-lg transform -translate-x-1/2 z-10" />

              {/* Content */}
              <div className={`ml-16 md:ml-0 md:w-1/2 ${index % 2 === 0 ? "md:pr-12" : "md:pl-12"}`}>
                <div className="bg-white p-6 rounded-xl shadow-lg">
                  <h3 className="text-2xl font-bold text-primary-700">
                    {exp.role}
                  </h3>
                  <p className="text-lg text-accent-600 font-medium mb-1">
                    {exp.company}
                  </p>
                  <p className="text-sm text-gray-500 mb-4">
                    {exp.startDate} - {exp.endDate}
                  </p>
                  <ul className="list-disc list-inside text-gray-700 space-y-2">
                    {exp.description.map((item, i) => (
                      <li key={i}>{item}</li>
                    ))}
                  </ul>
                </div>
              </div>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}
```

- [ ] **Step 2:** Commit Experience

```bash
git add src/components/Experience.tsx
git commit -m "feat: add Experience timeline component"
```

---

### Task 8: Build Contact section with form

**Files:**
- Create: `src/components/Contact.tsx`

- [ ] **Step 1:** Create Contact form (no backend, just UI + success animation)

```tsx
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { profile } from "@/data/profile";

export default function Contact() {
  const [submitted, setSubmitted] = useState(false);
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Simulate submission
    setSubmitted(true);
    setTimeout(() => {
      setSubmitted(false);
      setFormData({ name: "", email: "", message: "" });
    }, 3000);
  };

  return (
    <section id="contact" className="py-20 bg-white">
      <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0 }}
          whileInView={{ opacity: 1 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6 }}
          className="text-center mb-12"
        >
          <h2 className="text-4xl font-heading font-bold text-primary-700 mb-4">
            Get In Touch
          </h2>
          <div className="w-24 h-1 bg-accent-500 mx-auto" />
        </motion.div>

        <div className="grid md:grid-cols-2 gap-12">
          {/* Contact Info */}
          <motion.div
            initial={{ x: -30, opacity: 0 }}
            whileInView={{ x: 0, opacity: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6 }}
            className="space-y-6"
          >
            <div>
              <h3 className="text-xl font-semibold text-primary-600 mb-2">
                Contact Information
              </h3>
              <div className="space-y-4">
                <p>
                  <strong>Email:</strong>
                  <br />
                  <a
                    href={`mailto:${profile.email}`}
                    className="text-primary-600 hover:underline"
                  >
                    {profile.email}
                  </a>
                </p>
                <p>
                  <strong>Phone:</strong>
                  <br />
                  {profile.phone}
                </p>
                <p>
                  <strong>LinkedIn:</strong>
                  <br />
                  <a
                    href={profile.linkedin}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-primary-600 hover:underline"
                  >
                    {profile.linkedin}
                  </a>
                </p>
              </div>
            </div>

            <div className="pt-8 border-t border-gray-200">
              <p className="text-gray-600 italic">
                I'd love to hear from you! Whether you have a project in mind,
                want to collaborate, or just want to say hi, feel free to reach
                out.
              </p>
            </div>
          </motion.div>

          {/* Form */}
          <motion.div
            initial={{ x: 30, opacity: 0 }}
            whileInView={{ x: 0, opacity: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6 }}
          >
            <form onSubmit={handleSubmit} className="space-y-6">
              <div>
                <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
                  Name
                </label>
                <input
                  type="text"
                  id="name"
                  required
                  value={formData.name}
                  onChange={(e) =>
                    setFormData({ ...formData, name: e.target.value })
                  }
                  className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition"
                  placeholder="Your name"
                />
              </div>

              <div>
                <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
                  Email
                </label>
                <input
                  type="email"
                  id="email"
                  required
                  value={formData.email}
                  onChange={(e) =>
                    setFormData({ ...formData, email: e.target.value })
                  }
                  className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition"
                  placeholder="your@email.com"
                />
              </div>

              <div>
                <label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
                  Message
                </label>
                <textarea
                  id="message"
                  rows={5}
                  required
                  value={formData.message}
                  onChange={(e) =>
                    setFormData({ ...formData, message: e.target.value })
                  }
                  className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition resize-none"
                  placeholder="Your message..."
                />
              </div>

              <button
                type="submit"
                className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition-all shadow-md hover:shadow-lg"
              >
                Send Message
              </button>
            </form>

            {/* Success confetti animation */}
            <AnimatePresence>
              {submitted && (
                <motion.div
                  initial={{ scale: 0, opacity: 0 }}
                  animate={{ scale: 1, opacity: 1 }}
                  exit={{ scale: 0, opacity: 0 }}
                  className="fixed inset-0 flex items-center justify-center bg-black/50 z-50"
                >
                  <motion.div
                    initial={{ y: 50, opacity: 0 }}
                    animate={{ y: 0, opacity: 1 }}
                    transition={{ delay: 0.2 }}
                    className="bg-white p-8 rounded-2xl shadow-2xl text-center"
                  >
                    <h3 className="text-2xl font-bold text-primary-600 mb-2">
                      Message Sent!
                    </h3>
                    <p className="text-gray-600">
                      Thanks for reaching out. I'll get back to you soon.
                    </p>
                  </motion.div>
                </motion.div>
              )}
            </AnimatePresence>
          </motion.div>
        </div>
      </div>
    </section>
  );
}
```

- [ ] **Step 2:** Commit Contact

```bash
git add src/components/Contact.tsx
git commit -m "feat: add Contact section with form and success animation"
```

---

### Task 9: Build ProjectCard component

**Files:**
- Create: `src/components/ProjectCard.tsx`

- [ ] **Step 1:** Create ProjectCard

```tsx
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import { Project } from "@/data/projects";

export default function ProjectCard({ project }: { project: Project }) {
  return (
    <motion.article
      whileHover={{ y: -8, scale: 1.02 }}
      className="bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100 hover:border-primary-200 transition-all"
    >
      {project.image && (
        <div className="relative h-48 w-full">
          <Image
            src={project.image}
            alt={project.title}
            fill
            className="object-cover"
          />
        </div>
      )}
      <div className="p-6">
        <h3 className="text-xl font-bold text-primary-700 mb-2">
          {project.title}
        </h3>
        <p className="text-gray-600 mb-4">{project.description}</p>

        <div className="flex flex-wrap gap-2 mb-4">
          {project.tech.map((t) => (
            <span
              key={t}
              className="px-3 py-1 bg-accent-100 text-accent-800 rounded-full text-sm font-medium"
            >
              {t}
            </span>
          ))}
        </div>

        <div className="flex gap-4">
          {project.demoUrl && (
            <a
              href={project.demoUrl}
              target="_blank"
              rel="noopener noreferrer"
              className="text-primary-600 hover:text-primary-800 font-medium"
            >
              Live Demo →
            </a>
          )}
          {project.githubUrl && (
            <a
              href={project.githubUrl}
              target="_blank"
              rel="noopener noreferrer"
              className="text-gray-700 hover:text-gray-900 font-medium"
            >
              GitHub →
            </a>
          )}
        </div>
      </div>
    </motion.article>
  );
}
```

- [ ] **Step 2:** Commit ProjectCard

```bash
git add src/components/ProjectCard.tsx
git commit -m "feat: add ProjectCard component"
```

---

### Task 10: Build BlogCard component

**Files:**
- Create: `src/components/BlogCard.tsx`

- [ ] **Step 1:** Create BlogCard

```tsx
import Link from "next/link";
import { BlogPost } from "@/data/blogPosts";

export default function BlogCard({ post }: { post: BlogPost }) {
  return (
    <article className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-shadow border border-gray-100">
      {post.image && (
        <div className="relative h-48 w-full">
          <img
            src={post.image}
            alt={post.title}
            className="object-cover w-full h-full"
          />
        </div>
      )}
      <div className="p-6">
        <p className="text-sm text-gray-500 mb-2">{post.date}</p>
        <h3 className="text-xl font-bold text-primary-700 mb-2">
          {post.title}
        </h3>
        <p className="text-gray-600 mb-4">{post.excerpt}</p>
        <div className="flex flex-wrap gap-2 mb-4">
          {post.tags.map((tag) => (
            <span
              key={tag}
              className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs font-medium"
            >
              #{tag}
            </span>
          ))}
        </div>
        <Link
          href={`/my-corner/${post.slug}`}
          className="text-primary-600 hover:text-primary-800 font-medium"
        >
          Read more →
        </Link>
      </div>
    </article>
  );
}
```

- [ ] **Step 2:** Commit BlogCard

```bash
git add src/components/BlogCard.tsx
git commit -m "feat: add BlogCard component"
```

---

### Task 11: Compose Home page

**Files:**
- Modify: `app/page.tsx` (replace default)
- Modify: `app/layout.tsx` (to include TetEffects and Header/Footer)

- [ ] **Step 1:** Update `app/layout.tsx` to include global wrappers

```tsx
// Add imports
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import TetEffects from "@/components/TetEffects";

// Inside RootLayout, wrap children:
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${inter.variable} ${playfair.variable} font-body`}>
        <TetEffects />
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}
```

- [ ] **Step 2:** Rewrite `app/page.tsx` to assemble Home sections

```tsx
import Hero from "@/components/Hero";
import About from "@/components/About";
import Experience from "@/components/Experience";
import Contact from "@/components/Contact";

export default function Home() {
  return (
    <>
      <Hero />
      <About />
      <Experience />
      <Contact />
    </>
  );
}
```

- [ ] **Step 3:** Commit Home assembly

```bash
git add app/layout.tsx app/page.tsx
git commit -m "feat: compose Home page with all sections"
```

---

### Task 12: Build Projects page

**Files:**
- Create: `app/projects/page.tsx`

- [ ] **Step 1:** Create Projects page with grid

```tsx
import { motion } from "framer-motion";
import { projects } from "@/data/projects";
import ProjectCard from "@/components/ProjectCard";

export default function ProjectsPage() {
  return (
    <div className="min-h-screen bg-gray-50 pt-24 pb-12">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6 }}
          className="text-center mb-16"
        >
          <h1 className="text-4xl font-heading font-bold text-primary-700 mb-4">
            Projects
          </h1>
          <div className="w-24 h-1 bg-accent-500 mx-auto" />
        </motion.div>

        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
          {projects.map((project) => (
            <ProjectCard key={project.id} project={project} />
          ))}
        </div>
      </div>
    </div>
  );
}
```

- [ ] **Step 2:** Commit Projects page

```bash
git add app/projects/page.tsx
git commit -m "feat: add Projects page"
```

---

### Task 13: Build My Corner page (blog list)

**Files:**
- Create: `app/my-corner/page.tsx`

- [ ] **Step 1:** Create My Corner list page

```tsx
import { motion } from "framer-motion";
import { blogPosts } from "@/data/blogPosts";
import BlogCard from "@/components/BlogCard";

export default function MyCornerPage() {
  return (
    <div className="min-h-screen bg-white pt-24 pb-12">
      <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6 }}
          className="text-center mb-16"
        >
          <h1 className="text-4xl font-heading font-bold text-primary-700 mb-4">
            My Corner
          </h1>
          <p className="text-lg text-gray-600">
            Personal stories, memories, and reflections
          </p>
        </motion.div>

        <div className="space-y-8">
          {blogPosts.map((post) => (
            <BlogCard key={post.slug} post={post} />
          ))}
        </div>
      </div>
    </div>
  );
}
```

- [ ] **Step 2:** Commit My Corner page

```bash
git add app/my-corner/page.tsx
git commit -m "feat: add My Corner blog list page"
```

---

### Task 14: Build single blog post page

**Files:**
- Create: `app/my-corner/[slug]/page.tsx`

- [ ] **Step 1:** Create dynamic route with markdown rendering (or plain HTML for simplicity; we can add remark later)

```tsx
import { notFound } from "next/navigation";
import { blogPosts } from "@/data/blogPosts";
import Link from "next/link";
import { motion } from "framer-motion";

export function generateStaticParams() {
  return blogPosts.map((post) => ({
    slug: post.slug,
  }));
}

export default function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = blogPosts.find((p) => p.slug === params.slug);

  if (!post) {
    notFound();
  }

  return (
    <div className="min-h-screen bg-white pt-24 pb-12">
      <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6 }}
        >
          <Link
            href="/my-corner"
            className="inline-block text-primary-600 hover:text-primary-800 mb-6"
          >
            ← Back to My Corner
          </Link>

          <article>
            <h1 className="text-4xl font-heading font-bold text-primary-700 mb-4">
              {post.title}
            </h1>
            <p className="text-gray-500 mb-8">{post.date}</p>

            {post.image && (
              <div className="relative h-64 w-full mb-8 rounded-2xl overflow-hidden">
                <img
                  src={post.image}
                  alt={post.title}
                  className="object-cover w-full h-full"
                />
              </div>
            )}

            <div className="prose prose-lg max-w-none text-gray-700">
              {/* Simple line-based rendering; for richer markdown, add 'react-markdown' */}
              {post.content.split("\n").map((line, i) => {
                if (line.startsWith("# ")) {
                  return (
                    <h1 key={i} className="text-3xl font-bold my-4">
                      {line.slice(2)}
                    </h1>
                  );
                }
                if (line.startsWith("## ")) {
                  return (
                    <h2 key={i} className="text-2xl font-bold my-4">
                      {line.slice(3)}
                    </h2>
                  );
                }
                if (line.startsWith("- ")) {
                  return (
                    <li key={i} className="ml-6 list-disc">
                      {line.slice(2)}
                    </li>
                  );
                }
                if (line.trim() === "") {
                  return <br key={i} />;
                }
                return (
                  <p key={i} className="mb-4">
                    {line}
                  </p>
                );
              })}
            </div>
          </article>
        </motion.div>
      </div>
    </div>
  );
}
```

- [ ] **Step 2:** Commit dynamic blog page

```bash
git add app/my-corner/[slug]/page.tsx
git commit -m "feat: add dynamic blog post page"
```

---

### Task 15: Add scroll animations to Home sections

**Files:**
- `src/components/Hero.tsx` (already has Framer Motion)
- `src/components/About.tsx`
- `src/components/Experience.tsx`
- `src/components/Contact.tsx`
- `app/page.tsx` (wrap sections in motion.div with viewport)

- [ ] **Step 1:** Wrap each section in `motion.section` with `whileInView` for scroll reveal (already added in About, Experience, Contact from previous tasks). Verify smooth behavior.

- [ ] **Step 2:** Add stagger to timeline items in Experience (already in loop with delay)

- [ ] **Step 3:** Ensure Hero animates on load (already done)

- [ ] **Step 4:** Commit scroll animations

```bash
git add src/components/Hero.tsx src/components/About.tsx src/components/Experience.tsx src/components/Contact.tsx
git commit -m "feat: add scroll-triggered animations with Framer Motion"
```

---

### Task 16: Responsive styling and mobile menu

**Files:**
- `src/components/Header.tsx` (already responsive)
- All pages and components use Tailwind responsive classes

- [ ] **Step 1:** Verify Header mobile menu works on small screens (already built)

- [ ] **Step 2:** Ensure Home sections stack properly on mobile (Tailwind responsive classes already used)

- [ ] **Step 3:** Verify Projects grid: `sm:grid-cols-2 lg:grid-cols-3` (already set)

- [ ] **Step 4:** Test on multiple viewports mentally or suggest manual testing

- [ ] **Step 5:** Commit responsive pass

```bash
git add .
git commit -m "style: ensure responsive layouts across all pages"
```

---

### Task 17: Test build locally

**Files:**
- All project files

- [ ] **Step 1:** Run development server

```bash
npm run dev
```

Check for console errors, broken routes, missing images.

- [ ] **Step 2:** Test each route:
  - `/`
  - `/projects`
  - `/my-corner`
  - `/my-corner/journey-into-ai` (example)

- [ ] **Step 3:** Test contact form submission (mock)

- [ ] **Step 4:** Verify animations smooth (60fps expected)

- [ ] **Step 5:** Fix any issues found

- [ ] **Step 6:** Build for production

```bash
npm run build
```

Ensure build completes without errors.

- [ ] **Step 7:** Commit final fixes

```bash
git add .
git commit -m "fix: address build/test issues and finalize"
```

---

### Task 18: Prepare for Vercel deployment

**Files:**
- Optionally create `vercel.json` if custom config needed (usually not)
- Update `README.md` with deployment instructions

- [ ] **Step 1:** Create README.md

```markdown
# Trí Vĩ Portfolio

Personal portfolio built with Next.js 14, Tailwind CSS, Framer Motion, featuring a traditional Tết theme.

## Features

- Responsive design (mobile, tablet, desktop)
- Smooth animations with Framer Motion
- Traditional Tết theme (falling petals, fireworks, red/gold colors)
- Multi-page routing: Home, Projects, My Corner (blog)
- Contact form with success animation

## Tech Stack

- Next.js 14 (App Router)
- TypeScript
- Tailwind CSS
- Framer Motion

## Getting Started

1. Clone the repository
2. Install dependencies: \`npm install\`
3. Run the development server: \`npm run dev\`
4. Open [http://localhost:3000](http://localhost:3000) in your browser

## Build for Production

\`\`\`bash
npm run build
npm start
\`\`\`

## Deploy on Vercel

[Import this repository on Vercel](https://vercel.com/new) and deploy automatically.
```

- [ ] **Step 2:** Commit README

```bash
git add README.md
git commit -m "docs: add README with deploy instructions"
```

- [ ] **Step 3:** Push to remote (if configured) or instruct user to deploy via Vercel CLI

---

### Acceptance Checklist

- [ ] Next.js project initializes and runs locally
- [ ] Tailwind colors configured (primary red, accent gold)
- [ ] Google Fonts loaded (Playfair Display, Inter)
- [ ] Header responsive (desktop nav + mobile hamburger)
- [ ] Hero section with avatar, gradient, CTA buttons
- [ ] About section with bio, languages, contact info
- [ ] Experience timeline with 4 positions, alternating sides, on-scroll animations
- [ ] Contact form with fields (name, email, message) and success modal
- [ ] TetEffects component: falling petals canvas + subtle sparkles
- [ ] Projects page with grid of project cards (hover effects, tech badges)
- [ ] My Corner page with blog cards list
- [ ] Blog post dynamic page renders content
- [ ] Scroll animations on all Home sections (fade/slide)
- [ ] Responsive on mobile (<768px), tablet (768-1024px), desktop (>1024px)
- [ ] No console errors or TypeScript warnings
- [ ] Production build succeeds (`npm run build`)
- [ ] README with instructions
- [ ] Ready for Vercel deployment

---

**Spec referenced:** `docs/superpowers/specs/2026-03-31-portfolio-tet-theme-design.md`

**Total estimated tasks:** ~18 major tasks, broken into 2-5 minute steps each. With ClawTeam parallelization, this can be completed quickly.

**Notes for implementer:**
- Use `clawteam spawn` to assign tasks to workers.
- Each worker should get one component/page at a time.
- Review between tasks (two-stage code review).
- Keep commits small and descriptive.
- Ensure data is accurately transferred from CV.
- Polish animations to be smooth (60fps).
- Test responsiveness thoroughly.
- Keep the Tết theme elegant and not overwhelming.
- Do not simplify; deliver a complete, polished product ready to deploy.


Now I understand the task. I'm the "header-footer" agent in a ClawTeam setup. My task is Task 4: Build Header & Footer components. However, looking at the implementation plan, it seems like the plan is comprehensive and includes many tasks beyond just header/footer. But the current task assignment says "Task 4: Build Header & Footer components."

Let me check what tasks are currently assigned to me by using the ClawTeam CLI to list tasks with owner "header-footer". This will help me understand what's expected.

Let me check the current task list and my inbox to understand what work is assigned to me:

exec

clawteam task list portfolio-tet-2026 --owner header-footer


Tasks - portfolio-tet-2026
┏━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┓
┃ ID       ┃ Subject     ┃ Status  ┃ Priority ┃ Owner      ┃ Lock ┃ Blocked By ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━┩
│ fbb0da4a │ Build       │ blocked │ medium   │ header-fo… │      │ 685f1607   │
│          │ Header &    │         │          │            │      │            │
│          │ Footer      │         │          │            │      │            │
└──────────┴─────────────┴─────────┴──────────┴────────────┴──────┴────────────┘


I see there's a task with ID `fbb0da4a` for "Build Header & Footer" with status "blocked" and blocked by `685f1607`. This suggests there's a dependency. I should check what that dependency task is, and also check my inbox for any instructions.

Let me check the task details for the blocking task and also check my inbox for messages from the leader.

I see my task is blocked by another task (685f1607). Let me check what that blocking task is and also check my inbox for any instructions:

clawteam task list portfolio-tet-2026 --task-id 685f1607


Usage: clawteam task list [OPTIONS] TEAM
Try 'clawteam task list --help' for help.
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ No such option: --task-id                                                    │
╰──────────────────────────────────────────────────────────────────────────────╯

(Command exited with code 2)


I need to check the task list properly. Let me just look at all tasks to see what's blocking:

Let me view all tasks to understand the dependencies:

clawteam task list portfolio-tet-2026 --include-disabled


Usage: clawteam task list [OPTIONS] TEAM
Try 'clawteam task list --help' for help.
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ No such option: --include-disabled                                           │
╰──────────────────────────────────────────────────────────────────────────────╯

(Command exited with code 2)


Let me check the help to see the correct options:


Usage: clawteam task list [OPTIONS] TEAM

 List tasks for a team (TaskList).

╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ *    team      TEXT  Team name [required]                                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --status         -s         TEXT  Filter by status                           │
│ --owner,--agent  -o,-a      TEXT  Filter by owner                            │
│ --priority       -p         TEXT  Filter by priority: low, medium, high,     │
│                                   urgent                                     │
│ --sort-priority                   Sort by priority (urgent first)            │
│ --help                            Show this message and exit.                │
╰──────────────────────────────────────────────────────────────────────────────╯


OK, so the options are simpler. Let me list all tasks without filters to see the full picture:


Tasks - portfolio-tet-2026
┏━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┓
┃          ┃          ┃          ┃          ┃           ┃          ┃ Blocked   ┃
┃ ID       ┃ Subject  ┃ Status   ┃ Priority ┃ Owner     ┃ Lock     ┃ By        ┃
┡━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━┩
│ 24d73617 │ Test     │ pending  │ high     │ build-de… │          │           │
│          │ build &  │          │          │           │          │           │
│          │ prepare  │          │          │           │          │           │
│          │ deploym… │          │          │           │          │           │
│ 2599ccbe │ Polish   │ blocked  │ medium   │ polish    │          │ 63b353b0, │
│          │ animati… │          │          │           │          │ 71db9f4d, │
│          │ &        │          │          │           │          │ 3c2967eb, │
│          │ respons… │          │          │           │          │ 4664aa4e  │
│ 393fb6a3 │ Test     │ in_prog… │ low      │ test-age… │ test-ag… │           │
│          │ task     │          │          │           │          │           │
│ 3c2967eb │ Build My │ blocked  │ medium   │ mycorner… │          │ 631d76b4  │
│          │ Corner   │          │          │           │          │           │
│          │ page     │          │          │           │          │           │
│ 4664aa4e │ Build    │ blocked  │ medium   │ blog-dyn… │          │ 631d76b4, │
│          │ blog     │          │          │           │          │ 5b75def0  │
│          │ dynamic  │          │          │           │          │           │
│          │ page     │          │          │           │          │           │
│ 5b75def0 │ Create   │ blocked  │ high     │ data      │          │ 685f1607  │
│          │ data     │          │          │           │          │           │
│          │ layer    │          │          │           │          │           │
│ 631d76b4 │ Build    │ blocked  │ medium   │ blog-card │          │ 685f1607, │
│          │ BlogCard │          │          │           │          │ 5b75def0  │
│          │ compone… │          │          │           │          │           │
│ 63b353b0 │ Compose  │ blocked  │ medium   │ home-com… │          │ c5f11815, │
│          │ Home     │          │          │           │          │ f0406392, │
│          │ page     │          │          │           │          │ 87c38bb6, │
│          │          │          │          │           │          │ bebc7e6f  │
│ 685f1607 │ Initial… │ in_prog… │ high     │ init      │ init     │           │
│          │ Next.js  │          │          │           │          │           │
│          │ project  │          │          │           │          │           │
│ 6a4bbfbb │ Initial… │ pending  │ high     │ unused    │          │           │
│          │ Next.js  │          │          │           │          │           │
│          │ project  │          │          │           │          │           │
│ 71db9f4d │ Build    │ blocked  │ medium   │ projects… │          │ 89ddfb77  │
│          │ Projects │          │          │           │          │           │
│          │ page     │          │          │           │          │           │
│ 87c38bb6 │ Build    │ blocked  │ medium   │ experien… │          │ 685f1607, │
│          │ Experie… │          │          │           │          │ 5b75def0  │
│          │ timeline │          │          │           │          │           │
│ 89ddfb77 │ Build    │ blocked  │ medium   │ project-… │          │ 685f1607, │
│          │ Project… │          │          │           │          │ 5b75def0  │
│          │ compone… │          │          │           │          │           │
│ bebc7e6f │ Build    │ blocked  │ medium   │ contact   │          │ 685f1607, │
│          │ Contact  │          │          │           │          │ 5b75def0  │
│          │ section  │          │          │           │          │           │
│ c5f11815 │ Build    │ blocked  │ medium   │ hero      │          │ 685f1607, │
│          │ Hero     │          │          │           │          │ 5b75def0  │
│          │ section  │          │          │           │          │           │
│ ccd4055d │ Build    │ blocked  │ medium   │ tet-effe… │          │ 685f1607  │
│          │ TetEffe… │          │          │           │          │           │
│          │ compone… │          │          │           │          │           │
│ f0406392 │ Build    │ blocked  │ medium   │ about     │          │ 685f1607, │
│          │ About    │          │          │           │          │ 5b75def0  │
│          │ section  │          │          │           │          │           │
│ fbb0da4a │ Build    │ blocked  │ medium   │ header-f… │          │ 685f1607  │
│          │ Header & │          │          │           │          │           │
│          │ Footer   │          │          │           │          │           │
└──────────┴──────────┴──────────┴──────────┴───────────┴──────────┴───────────┘


Now I understand! My task (fbb0da4a - Build Header & Footer) is blocked by task 685f1607, which is "Initialize Next.js project" with owner "init" and it's currently "in_progress". There are many other tasks also blocked by the same task and/or data layer task (5b75def0). The dependencies are:

- Data layer (5b75def0) is blocked by 685f1607
- Many components (Hero, About, Experience, Contact, TetEffects, etc.) are blocked by 685f1607 AND data layer
- My Header & Footer task is only blocked by 685f1607

This suggests that the initial Next.js setup must be completed first, but my Header & Footer task depends on that. Since that task is in_progress, I should wait for it to complete or check if I can start my work if the data I need (like profile data) is available.

But wait, looking at my task more carefully, the Header & Footer components I need to build according to the plan:

- `src/components/Header.tsx`
- `src/components/Footer.tsx`

These components need to be built before the layout is updated to include them. But they may not directly depend on the data layer; they use some data perhaps from profile? Actually Header uses profile? Looking at the plan:

Header imports: "use client", uses Link from next/link, but doesn't seem to use any data from the data layer. Footer also just uses static text.

However, the plan says in Task 3: "Build TetEffects component", Task 4: Build Header & Footer. But the overall dependency is that Task 1 (Initialize Next.js) must complete first. Task 2 (data layer) also should be done for About/Experience/Contact but Header/Footer might be independent? Actually Header might use the profile? Looking at the Header component code in the plan, it doesn't use profile data; it only uses static links.

But in Task 11, the layout is updated to include Header and Footer. That would be after Header and Footer are built.

Given that the blocking task is still in_progress (685f1607 - Initialize Next.js project), I should check if that is actually done. Let me check the workspace files to see if the Next.js project exists.

Also, I need to check my inbox for any messages from the leader.

Let me check my inbox for messages from the leader:

clawteam inbox receive portfolio-tet-2026 --agent header-footer


No messages


No messages. So the blocking task (Initialize Next.js) is still in_progress. Since my task (Header & Footer) is blocked by that, I cannot start my work until that one completes.

However, I could potentially check if the initialization has actually been completed in the workspace. If the Next.js project structure exists, I might be able to proceed even if the task status hasn't been updated yet. Let's examine the workspace:

Let me examine the workspace to understand the current state:


total 252
drwxr-xr-x  2 node node   4096 Mar 31 03:19 .
drwxr-xr-x 19 node node   4096 Mar 31 03:21 ..
-rw-r--r--  1 node node     96 Mar 31 03:19 .git
-rw-r--r--  1 node node   3936 Mar 31 03:19 NguyenNgocTriVi_CV.md
-rw-r--r--  1 node node 186261 Mar 31 03:19 avatar.jpeg
-rw-r--r--  1 node node  50829 Mar 31 03:19 implementation-plan.md


Interesting! The workspace only contains:
- .git
- NguyenNgocTriVi_CV.md
- avatar.jpeg
- implementation-plan.md

But no Next.js project files yet! So the initialization task is not actually done on the filesystem; it's likely assigned to another agent (`init` owner) and is "in_progress" but not yet committed to the worktree. Since it's a separate worktree, my work depends on the initialization agent's work being committed to the shared repository. I need to wait for that task to complete.

Since I cannot proceed without the Next.js foundation, I should report to the leader that I'm idle because my task is blocked by the initialization task. However, the instructions say: "After finishing work, report your costs... If you are blocked or need help, message the leader: `clawteam inbox send portfolio-tet-2026 sagi 'Need help: <description>'`"

Also, the worker loop protocol says: "If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking for new work."

I'm not exactly "finished work" yet; I haven't started because I'm blocked. I should message the leader to indicate I'm waiting on the initialization task. Let's do that.

But before that, maybe I should check the status of the blocking task more carefully. Could it be that the initialization task is actually done but the status hasn't been updated? The task list shows 685f1607 is "in_progress". It's possible the agent working on it hasn't marked it complete yet.

I should also check if there is any file that indicates completion, like a `package.json` or `next.config.js`. But the listing shows none. So definitely not ready.

Given this, I'll send a message to the leader explaining that my Header & Footer task is blocked on the Next.js initialization (task 685f1607), and I'll wait for that to complete. Then I'll continue checking for new work.

Let's also check the worker loop protocol: After finishing my current task batch, I should re-check task list and inbox. Since I have no tasks I can work on, I'll set my status to idle? The protocol says "If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking for new work." So I should do that.

But maybe I should be more proactive: Could I help with something else? The plan suggests parallelization. There might be other components I could help with that don't depend on the data layer? Actually most components depend on data layer (5b75def0) which itself blocks many tasks. But the Header and Footer I'm supposed to build might not need the data layer; they could be built independently. However, they do need the Next.js project structure to exist, because I need to create files within the `src/components/` directory which doesn't exist yet.

But wait: The implementation plan is written as a single agentic worker following a sub-skill. However, in this ClawTeam environment, the tasks are split among multiple agents. I'm assigned specifically to "header-footer". According to the plan, Task 4 is Build Header & Footer components, but it requires Task 1 to be done (the Next.js project). So the proper flow is: the init agent completes Task 1, then the data layer agent completes Task 2, then my task can start (since Header/Footer might not need data but need the project structure).

But I note that my task's blocking task is only 685f1607, not both 685f1607 and 5b75def0. That suggests Header/Footer might only need the project structure, not the data. Indeed the plan's Header component code does not import from data/profile; Footer also not. So they could be built as soon as the project structure exists (src/components directory). But we don't have that yet.

Thus I need to wait for Task 1 (Initialize Next.js) to complete. Since the other agent is working on it and it's "in_progress", I'll wait.

I'll send a message to the leader indicating I'm ready but blocked, and then I'll go idle and continue checking.

Let's compose a message.

I'll send a status update to the leader and go into idle monitoring:

clawteam inbox send portfolio-tet-2026 sagi "Header-Footer agent reporting: My task (fbb0da4a) is ready to start but blocked by Task 685f1607 (Initialize Next.js project) which is still in_progress. I'll wait for that to complete before building Header & Footer components. Standing by for further instructions."


OK Message sent to 'sagi'


Good, I've sent the message. Now I need to follow the worker loop protocol: after checking inbox and tasks, if I'm idle (no tasks I can work on), I should notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking. However, that command might be used to set my agent's lifecycle state, but the protocol says to notify the leader. Actually the command is `clawteam lifecycle idle portfolio-tet-2026`. That's likely to mark the team/agent as idle. But maybe that also notifies? Not sure. The instructions say: "If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking for new work." So I should run that command to signal idle.

Let's run that.

Now I'll set my lifecycle to idle and continue monitoring:


OK Idle notification sent to 'sagi'


Great. Now I should continue monitoring. The worker loop says: "After finishing your current task batch, re-check `clawteam task list portfolio-tet-2026 --owner header-footer`." Then check inbox again. Since I haven't finished any task, I'm idle. I should periodically check back. According to the protocol, I should continue checking for new work. But I shouldn't spam. Typically, I can sleep for a bit and check again. But as an agent, I can just wait for new messages? The system is not set up for me to be constantly polling; I'm in a session. The instruction "continue checking for new work" suggests I should loop: re-check task list and inbox. But if I'm idle, maybe I should just yield or wait? However, in this environment, I have to be proactive and check.

Given that the session is active, I could simply wait and poll every few minutes? But I'm not meant to run infinite loops. Actually, the worker loop is conceptual: after finishing a task, I loop. But since I'm blocked and have no task, I can either yield (sessions_yield) or just wait for the leader to reply. The protocol says "continue checking for new work." That could mean I should periodically re-run the checks.

But I don't want to waste resources. Perhaps the best is to sleep and then check again. I could use a cron job? But that's for the main session. Better: I could use exec with a sleep loop? That's messy. Alternatively, I could just end my turn and wait for the leader to send a message? But the instruction "continue checking for new work" suggests I should actively poll.

Looking at the typical pattern in these agent setups, after sending an idle notification, the agent might yield and then the system might wake it up when there's a new event (like a cron or message). But I'm not sure.

Maybe I should simply yield my turn, and the system will notify me when something changes (like a task becomes unblocked). However, the instructions specifically say "continue checking for new work," which implies I should not just yield and do nothing; I should periodically poll.

But there is a risk of tight loops. So I should add some delay between checks.

Let me think: the loop could be:

```
while (true) {
  await recheckTaskList();
  await recheckInbox();
  if (hasWork) { ... }
  else { await sleep(30s); }
}
```

But in this tool environment, I can't easily do async loops without blocking. I could use exec with a background process that runs a loop and writes to a log? That seems overkill.

Maybe the intended pattern is simpler: Since this is an agent session, after I've sent idle notification and reported, I could just yield and wait for the leader to contact me or for the system to deliver a new task. The "continue checking for new work" could be interpreted as "keep an eye on the task list and inbox" but not necessarily actively polling while idle; it's more of a mental note that after completing any future work I'll loop again.

Given that I'm in a single turn and cannot run persistent background loops easily without using background exec and then polling, and the system might be designed such that agents are restarted or woken by events, I think it's safe to yield now and let the leader respond.

But wait, the worker loop protocol says: "If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and **continue checking for new work**." The emphasis suggests I should keep checking. Maybe I should schedule a cron or use a loop.

Given the constraints, I'll do this: I'll run a background process that periodically checks the task list and inbox. But that might be complicated. Alternatively, I could use the cron tool to schedule a check every minute or so. However, the cron tool is meant for the main session? Actually the cron tool can schedule tasks in OpenClaw gateway.

But perhaps the simplest: I can just wait (yield) and trust that the leader will contact me when the blocking task is done, and then I'll check my tasks. The phrase "continue checking for new work" might be a guideline for after I complete a task, not that I must continuously poll while idle.

Given that I'm just starting and my first action was to check tasks and inbox, I've done that. Now I've sent idle notification. I should probably just yield and let the system know I'm idle. The leader can then assign new tasks or unblock.

Thus, I'll end with sessions_yield.

But before that, I should verify if there's any possibility I could start my tasks even without the full Next.js setup. Could I create the Header and Footer components in isolation? The workspace directory is given; it's where I should create files. But the plan expects a Next.js project structure. The files I need to create are inside `src/components/`. That directory doesn't exist yet because the init task hasn't run. Could I create the src/components myself? That would be part of the initialization. But the task is specifically "Initialize Next.js project" for another agent. It's probably best to wait for that agent to complete its work, as the plan is highly coordinated. If I start creating files prematurely, I might conflict with the init agent.

The protocol says: "Use `clawteam spawn` to assign tasks to workers." That implies the team leader would have already spawned agents. But I'm already spawned as an agent for header-footer. So the leader expects me to work on my tasks when unblocked.

Thus, I'll be idle and wait.

But also, the instructions say: "Before finishing, save your session: `clawteam session save portfolio-tet-2026 --session-id <id>`". That's for when I'm about to finish work, not now.

Thus, my plan:
1. Check task list (done)
2. Check inbox (done)
3. Send idle notification to leader (done)
4. Yield (sessions_yield) so the session can close and wait for new input/message.

But note: The tool `sessions_yield` is for ending my current turn to receive results from subagents. That might not be appropriate for just waiting. Actually, sessions_yield is used after spawning subagents to receive their results. I'm not spawning any subagent. There's no specific tool to "wait" aside from just not doing anything. I could just say "NO_REPLY" to indicate no response needed. But the system expects a reply from me? Actually in the OpenClaw agent model, the agent is in a loop; it gets a message, processes, and sends a reply. Then the next message comes. So I should send some kind of reply to indicate my status? I've already sent a message to sagi via inbox. That's sufficient. I don't need to send a message to the current channel (if any). This is likely a direct chat with the system or a thread? The instructions from the coordination protocol are about using the CLI tools, not about replying to a user message. In this context, I'm an autonomous agent within the ClawTeam system, and I'm reacting to the task assignment. The "leader" is another agent or human. I've communicated via inbox.

Now I need to decide what to output as my reply to this current turn. The system likely expects some response. If I have nothing more to say, I could respond with NO_REPLY. But is that appropriate? The guidance says: "When you have nothing to say, respond with ONLY: NO_REPLY". So that would be fine.

But I've already sent a message to sagi. That's an action, not a reply to the user. The user (in this context, the ClawTeam system?) gave me the task information and I'm expected to respond. Usually, after reading the instructions, an agent would output something like "Acknowledged" or "Starting work." However, I've already sent an inbox message. I could also output a status update.

Maybe the expected pattern is: I receive a prompt from the system (like "You are assigned to task..."), I should respond with a message acknowledging and maybe reporting status. But that's not explicitly defined.

Let's look at the initial context: The user provided a big block of text with Identity, Workspace, Task, Context, Coordination Protocol, Worker Loop Protocol. This is like a system message to me, the agent. I need to react. Probably, I should output something like "Understood, I'm starting work" or "I'm blocked, waiting." But I've already sent a message to sagi. The leader is not necessarily the one who sent this message; the system gave me this context. The leader is sagi. I've messaged sagi. But do I need to reply to the system prompt? The system expects a turn response. I can just send a brief acknowledgment that I've read the instructions and will proceed accordingly.

Let's think: In OpenClaw, when an agent is spawned, it receives a prompt and then it must respond. The response could be a message to the channel or to the spawner. The instructions I read say: "Don't ask permission. Just do it." but that's for reading files, not for responding.

Given that the task is to "Build Header & Footer", but it's blocked, I should communicate my status. I already sent an inbox message to sagi. I could also output a simple message like "Task received. Currently blocked by initialization task. Monitoring progress." That would be appropriate as a reply to the system.

But the system might be expecting me to just do work and not necessarily reply. However, typical agents do respond. I'll output a concise summary of my status.

But the instructions about NO_REPLY are for when you have nothing to say. I do have something to say: I need to report that I've read the context and am now in idle waiting state. So I'll send a message.

However, the guidelines: "When you have nothing to say, respond with ONLY: NO_REPLY." If I have something to say, I can send a normal reply. So I'll send a reply that includes:
- Acknowledgment of task assignment
- Explanation that it's blocked
- That I've notified leader
- That I'll monitor and continue

Given the vibe (Sagi: direct, crisp, professional), I'll be concise.

Let's craft:

"Got it. Header & Footer task assigned. Currently blocked by Task 685f1607 (Initialize Next.js). I've notified sagi and marked idle. I'll monitor task status and start when unblocked."

That seems fine.

But also, I should check if there's any immediate action I can take: perhaps I could set up the git worktree properly? It already exists. Maybe I could ensure my environment is ready? Not necessary.

Thus, I'll output that.

But wait: The note about NO_REPLY: It says "When you have nothing to say, respond with ONLY: NO_REPLY." I do have something to say, so I'll say it.

Let's provide a reply.

But before finalizing, let's double-check if there's any action I should take regarding the session. Perhaps I should set a cron to check every minute? That might be overkill. The leader might just unblock the init task quickly, and then the task list will show my task ready. But how will I know to start? I could set a cron that runs every 5 minutes to check tasks and if my task becomes unblocked, I start. However, the system may already handle that: When the init task completes, the team lead (sagi) might spawn a new instance of me or send me a message. Or maybe I am expected to be continuously running and periodically re-check.

Given the worker loop protocol, it's designed for an agent to keep working until no more tasks. So after I've done a task, I re-check. But since I'm idle with no tasks I can do, I might need to loop: check, if no work, wait, then check again. That's the "continue checking for new work".

But my current session will end after I reply unless I do something to keep it alive. If I just reply, the system may close the session. That would stop my ability to check later. To keep the session alive, I might need to spawn a background process or use a cron that will wake me? Actually, OpenClaw can have long-running sessions if the agent is doing something. If I yield and the session ends, I lose context. So maybe I should not end the session but rather loop in the same turn? That's not possible because I need to wait for time to pass.

Maybe the intended design is: I am a "persistent" agent that remains running, and the system will send me new task assignments as they become available (via inbox). So I don't need to constantly poll; I can just wait for a new message. That makes sense: the leader (sagi) will update task statuses and when my task is unblocked, they might send me a new message or the system will deliver a new event. So I should just stay alive but not consume CPU. How to do that? I could use a long-running background process that sleeps and then checks? That seems wasteful.

Alternatively, the OpenClaw agent model might use a "heartbeat" mechanism where I send periodic heartbeats to indicate I'm alive and to check for new messages. The HEARTBEAT.md could be used. But the instructions for heartbeats say: when you receive a heartbeat poll (a specific message), respond. That's for the main human-assistant channel, not for agent coordination.

Given the ambiguity, I'll assume it's fine to end my turn after reporting status, and the system will deliver further instructions when tasks change. The ClawTeam system likely tracks which agents are active and will send them a new session when work arrives. If the session ends, the agent might be restarted. That's acceptable.

Thus, I'll reply with my status, and then optionally say that I'm yielding or done. I could just say "Ready to start when unblocked." and then the system may treat that as the end of my turn. The agent manager will likely keep me around? Not sure.

Let's check typical usage in the ClawTeam docs. I recall that in the "ClawTeam Multi-Agent Coordination" skill, workers run a loop: they poll tasks and then work. The sample code shows a while loop. However, I'm not writing code; I'm the agent itself. The coordination protocol says: "Worker Loop Protocol - After finishing your current task batch, re-check clawteam task list ... Then check for new instructions with clawteam inbox receive ... If you become idle, notify the leader ... and continue checking for new work." That's clearly describing a loop that the agent should implement. So I should implement this loop in my behavior. But I can't implement an actual infinite loop within a single turn because that would block and never yield. However, I can simulate by: after finishing a task, go back to the beginning of my mental process and check again. But since each "turn" of the agent is a response to an input, I need to produce an output and then the turn ends. To continue, the system must send me another prompt (maybe from the leader or from the system itself after a delay). So the loop is across multiple turns, not within a single turn. I can, after responding, have the system re-trigger me (like with a cron or by the ClawTeam manager sending a wake-up). But I don't know the mechanism.

Given that the agent is spawned with a task, and it runs until it exits. The ClawTeam spawn command likely expects the agent to run continuously? Actually, the spawn command: `clawteam spawn --team <team> --agent-name <name> --task "..."` spawns an agent in a background session. It might be designed for the agent to run continuously, checking tasks and working. That suggests I should not exit after one turn; I should keep the session alive, perhaps by using a loop that includes sleep and re-checks. But again, I can't do that in a single tool call response; I need to structure my code (the agent's logic) to run a loop. However, I'm the agent, and my "code" is my own decision-making. In this environment, the assistant model runs in a stateless way: each turn, it receives the current context (previous messages, etc.) and decides actions. If I want to keep the session alive and do multiple actions over time, I need to keep yielding or something. Actually, the OpenClaw agent system uses a "session" concept. When an agent is spawned, it can have multiple turns. The agent can run in a loop by using `sessions_yield` to pause and let the system call back later. This is similar to the pattern described in the "executing-plans" skill: you spawn a subagent, then yield to wait for its result. For a long-running agent, you could spawn a background exec that runs a loop and then use process to interact? That's not it.

Maybe the ClawTeam spawn command spawns the agent in a separate persistent session (like a tmux window) that runs continuously. The agent then uses a while loop in its own code (the assistant's own reasoning) to repeatedly check tasks, work, and then check again. But the assistant's response generation is not typically a long-running process; it's a single request-response. However, the OpenClaw system might support "agent orchestrations" where the agent can take multiple steps within a single turn by using tools like `process` to run a loop and then `sessions_yield` to wait for events. Actually, there is a tool `exec` with `background` and `process` for managing sessions. But that's for running shell commands, not for the agent itself.

I'm overthinking this. The safe approach: respond with a summary and include "NO_REPLY" if I'm done? That would indicate no further response needed, and the system might end the session. But the protocol says to keep checking for new work. So I should not end the session. However, if I produce a non-empty reply, the session stays open? Not necessarily. The system might treat each message as a turn. After I send a reply, if there's no further input, the session might be considered complete. But the ClawTeam spawn likely expects the agent to run until explicitly killed. There may be a mechanism to keep it alive by using `sessions_yield` to wait for new events without closing. But I'm not sure.

Let's read the tool description for `sessions_yield`: "End your current turn. Use after spawning subagents to receive their results as the next message." That's specifically for after spawning subagents. Not for general idling.

Maybe the pattern is: I, as an agent, have been spawned to handle tasks. The system expects me to keep running and periodically check the task list. I can do that by using `cron` to schedule a check that will send me a message. For example, I could set a cron job that runs every minute: `clawteam task list ...` and if there's a task for me, it triggers my agent turn. But the cron tool is for scheduling jobs that send system events or agent turns. I could set a cron that wakes me up? However, that may be misusing.

Better approach: The ClawTeam team lead (sagi) might manage the workflow. It's possible that after marking a task as completed, sagi will spawn a new agent for the next task or will send a message to the appropriate agent to start. Since my task is blocked by another, maybe when that other task completes, sagi will unblock mine and then send me a message to start. I don't need to proactively poll; the leader will tell me.

Thus, after I've reported my blocked status, I can just wait. To keep the session alive, I might not need to do anything; the system might keep the session open pending new messages. I'll send a reply that indicates I'm waiting, and then end with NO_REPLY? No, NO_REPLY means I have nothing to say. I do have something to say. I'll just say my status and then let the session be idle. The system may close it, but that's okay; the next time sagi wants to notify me, they can spawn a new session for my agent. That's plausible.

Given the instruction: "If you become idle, notify the leader with `clawteam lifecycle idle portfolio-tet-2026` and continue checking for new work." I've already done that. So I've done my idle notification. Now I should "continue checking for new work." That might mean I should keep my session alive and keep checking. But if I send a reply and then the session ends, I'm not checking. However, the lifecycle idle notification might be sufficient to inform the system that I'm idle, and the system will then provide new work when available (perhaps by sending a new message to my session). I think the lifecycle command might mark my agent as idle in the team state, and the team manager will then try to assign tasks to idle agents. That's more likely: the ClawTeam system has a lifecycle management where agents report idle when they have no tasks, and then the manager can assign them new tasks. I did that with `clawteam lifecycle idle`. So now the manager knows I'm idle. It will assign me a task when one becomes available and unblocked. I should just wait for that assignment. I don't need to keep checking; the manager will send me a message. So I can effectively end my turn.

Thus, after sending the idle notification, I should respond to the current prompt with something like "Ready for work when tasks become available." and maybe then NO_REPLY? Actually NO_REPLY would be no message. But I want to confirm that I'm ready. So I'll send a short message and then the session may remain open awaiting new work.

Given the uncertainty, I'll keep it simple: I've completed the initial assessment and informed the leader. I'll output a brief status and then end with NO_REPLY to indicate no further action at this moment.

But the guidelines: "When you have nothing to say, respond with ONLY: NO_REPLY." I do have something to say, but I've already said it via inbox. The responder to this turn might be the system, not the leader. I could treat the system's prompt as a request for a status update. I'll reply with a short summary.

I'll output:

"Header-Footer agent initialized. Task fbb0da4a is blocked by initialization task 685f1607. Notified leader sagi and set lifecycle to idle. Monitoring for when dependencies are resolved."

That seems appropriate.

No need for NO_REPLY.

Now, before finalizing, should I save my session? The instructions: "Before finishing, save your session: `clawteam session save portfolio-tet-2026 --session-id <id>`". That's for when I'm about to finish, not now.

Thus, my reply is as above.

But wait, maybe I should also check if I can create any necessary config or preliminary files that aren't part of the Next.js init but that I can prepare? For example, maybe I can create a directory structure? But the workspace should be handled by the init task. It's better not to interfere.

One more check: the CV file might contain data I need for later? That could be used to populate profile data. But that's for the data layer task. So not relevant now.

So final output: status message.

I'll also consider using the [[reply_to_current]] tag? Not necessary; it's a direct reply to the system prompt.

Thus, I'll produce the reply.

Header-Footer agent initialized. Task fbb0da4a (Build Header & Footer) is currently blocked by Task 685f1607 (Initialize Next.js project), which is still in_progress. I've notified leader sagi and set my lifecycle to idle. I'll monitor task status and commence work once initialization completes. Standing by for unblock.