diff --git a/web/app/globals.css b/web/app/globals.css
index 0b3b28e..41eea20 100644
--- a/web/app/globals.css
+++ b/web/app/globals.css
@@ -241,3 +241,32 @@ body {
/* Dossier folder card: motion now lives in components/hq/deck.tsx
(framer-motion springs ported from the Framer "Document Folder"). */
+
+/* My Passport: each visa stamps down with a slight overshoot as the book
+ opens (components/hq/passport.tsx). */
+@keyframes pp-stamp-in {
+ 0% {
+ transform: scale(1.7);
+ opacity: 0;
+ }
+ 55% {
+ opacity: 1;
+ }
+ 72% {
+ transform: scale(0.95);
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.pp-stamp {
+ animation: pp-stamp-in 0.5s cubic-bezier(0.2, 0.85, 0.25, 1) both;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .pp-stamp {
+ animation: none;
+ }
+}
diff --git a/web/components/hq/my-client.tsx b/web/components/hq/my-client.tsx
index 0d12004..ebff83e 100644
--- a/web/components/hq/my-client.tsx
+++ b/web/components/hq/my-client.tsx
@@ -4,6 +4,7 @@ import { UserButton, useAuth, useUser } from "@clerk/nextjs";
import Link from "next/link";
import type { Hackathon } from "@/lib/types-hq";
import { PageShell } from "./page-shell";
+import { Passport } from "./passport";
import { Tracker } from "./tracker";
export function MyClient({
@@ -20,6 +21,7 @@ export function MyClient({
) : (
<>
+
>
)}
@@ -44,6 +46,7 @@ function GatedHub({ hackathons }: { hackathons: Hackathon[] }) {
return (
<>
+
>
);
diff --git a/web/components/hq/passport.tsx b/web/components/hq/passport.tsx
new file mode 100644
index 0000000..3f6daf0
--- /dev/null
+++ b/web/components/hq/passport.tsx
@@ -0,0 +1,831 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+
+/* ---------------------------------------------------------------------------
+ My Passport — Members · Pillar 03
+
+ A React port of the "My Passport.dc.html" Claude Design component. A
+ collectible hacker passport that flips open in 3D to reveal ink-stamped
+ visas for every hackathon you've hacked. The original was authored in
+ Claude Design's DCLogic format; this keeps the artwork pixel-faithful
+ (leather cover, foil crest, guilloché pages, turbulence-warped stamps)
+ while adapting it to the app: project font tokens, reduced-motion support,
+ and a responsively scaled stage that lives inside a page shell rather than
+ a fixed full-screen overlay.
+--------------------------------------------------------------------------- */
+
+// Natural size of the passport spread (two 340px pages). The stage scales this
+// down to fit narrow viewports; see the ResizeObserver below.
+const BOOK_W = 680;
+const BOOK_H = 470;
+
+type Stamp = {
+ id: string;
+ top: string; // arc text along the top
+ sub: string; // arc text along the bottom
+ year: string;
+ mono: string; // big monogram in the middle
+ label: string; // HACKED / VISA / ADMITTED
+ color: string;
+ pos: { left: number; top: number; size: number };
+ rotate: number;
+ delay: number; // stamp-in animation delay, ms
+ ringOpacity?: number;
+ inkOpacity?: number;
+ topSize?: number;
+ topLS?: number;
+ subSize?: number;
+ subLS?: number;
+ monoSize?: number;
+};
+
+// Inside-left page (revealed under the opening cover).
+const STAMPS_LEFT: Stamp[] = [
+ {
+ id: "htn",
+ top: "HACK THE NORTH",
+ sub: "TORONTO · ON",
+ year: "2024",
+ mono: "HTN",
+ label: "HACKED",
+ color: "#ed5b29",
+ pos: { left: -6, top: 8, size: 196 },
+ rotate: -8,
+ delay: 0,
+ topSize: 13,
+ topLS: 1.2,
+ },
+ {
+ id: "pennapps",
+ top: "PENNAPPS",
+ sub: "PHILADELPHIA · PA",
+ year: "2024",
+ mono: "PA",
+ label: "VISA",
+ color: "#7a4bd0",
+ pos: { left: 150, top: 30, size: 190 },
+ rotate: 9,
+ delay: 160,
+ ringOpacity: 0.9,
+ inkOpacity: 0.88,
+ subSize: 10,
+ subLS: 1.8,
+ },
+ {
+ id: "mhacks",
+ top: "MHACKS",
+ sub: "ANN ARBOR · MI",
+ year: "2024",
+ mono: "MH",
+ label: "ADMITTED",
+ color: "#17b26a",
+ pos: { left: 22, top: 205, size: 190 },
+ rotate: 6,
+ delay: 320,
+ },
+ {
+ id: "h6ix",
+ top: "HACK THE 6IX",
+ sub: "TORONTO · ON",
+ year: "2025",
+ mono: "H6",
+ label: "HACKED",
+ color: "#ff7a45",
+ pos: { left: 140, top: 238, size: 190 },
+ rotate: -13,
+ delay: 480,
+ ringOpacity: 0.9,
+ topSize: 14,
+ topLS: 1.4,
+ },
+];
+
+// Right page (the base spread).
+const STAMPS_RIGHT: Stamp[] = [
+ {
+ id: "lahacks",
+ top: "LA HACKS",
+ sub: "LOS ANGELES · CA",
+ year: "2025",
+ mono: "LA",
+ label: "HACKED",
+ color: "#3b6bf0",
+ pos: { left: 12, top: 14, size: 196 },
+ rotate: -10,
+ delay: 80,
+ },
+ {
+ id: "bitcamp",
+ top: "BITCAMP",
+ sub: "COLLEGE PARK · MD",
+ year: "2025",
+ mono: "BC",
+ label: "VISA",
+ color: "#c98a2b",
+ pos: { left: 150, top: 44, size: 190 },
+ rotate: 8,
+ delay: 240,
+ },
+ {
+ id: "cuhacking",
+ top: "CUHACKING",
+ sub: "OTTAWA · ON",
+ year: "2025",
+ mono: "cuH",
+ label: "ADMITTED",
+ color: "#17b26a",
+ pos: { left: -4, top: 232, size: 190 },
+ rotate: 12,
+ delay: 400,
+ topSize: 14,
+ topLS: 1.6,
+ monoSize: 30,
+ },
+ {
+ id: "hackumass",
+ top: "HACKUMASS",
+ sub: "AMHERST · MA",
+ year: "2023",
+ mono: "UM",
+ label: "HACKED",
+ color: "#b0552f",
+ pos: { left: 150, top: 250, size: 190 },
+ rotate: -6,
+ delay: 560,
+ ringOpacity: 0.85,
+ inkOpacity: 0.82,
+ topSize: 14,
+ topLS: 1.6,
+ },
+];
+
+const MONO = "var(--font-mono)";
+const DISPLAY = "var(--font-display)";
+
+/* Shared turbulence filters that give every stamp its rough, hand-inked edge.
+ Rendered once, referenced by url(#…) from all stamp SVGs. */
+function InkFilters() {
+ return (
+
+ );
+}
+
+function StampMark({ stamp, index }: { stamp: Stamp; index: number }) {
+ const {
+ top,
+ sub,
+ year,
+ mono,
+ label,
+ color,
+ ringOpacity = 0.92,
+ inkOpacity = 0.9,
+ topSize = 15,
+ topLS = 2.2,
+ subSize = 11,
+ subLS = 2.4,
+ monoSize = 34,
+ } = stamp;
+ const tA = `pp-arc-a-${index}`;
+ const tB = `pp-arc-b-${index}`;
+
+ return (
+
+ );
+}
+
+function StampLayer({
+ stamps,
+ indexOffset,
+ visible,
+}: {
+ stamps: Stamp[];
+ indexOffset: number;
+ visible: boolean;
+}) {
+ if (!visible) return null;
+ return (
+
+ {stamps.map((s, i) => (
+
+ ))}
+
+ );
+}
+
+/* The foil crest on the cover: a trench-coated "incognito hacker" seal. */
+function CoverCrest() {
+ return (
+
+ );
+}
+
+type Phase = "closed" | "flash" | "open";
+
+export function Passport({
+ stampCount = "08",
+ cities = "07",
+}: {
+ stampCount?: string;
+ cities?: string;
+}) {
+ const [phase, setPhase] = useState("closed");
+ const [opened, setOpened] = useState(false);
+ const [scale, setScale] = useState(1);
+ const [reduced, setReduced] = useState(false);
+ const timers = useRef[]>([]);
+ const stageRef = useRef(null);
+
+ // Respect the OS "reduce motion" setting — skip the flip/wobble choreography.
+ useEffect(() => {
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
+ const apply = () => setReduced(mq.matches);
+ apply();
+ mq.addEventListener("change", apply);
+ return () => mq.removeEventListener("change", apply);
+ }, []);
+
+ // Scale the fixed-size spread down to fit the available width.
+ useEffect(() => {
+ const el = stageRef.current;
+ if (!el) return;
+ const ro = new ResizeObserver(() => {
+ setScale(Math.min(1, el.clientWidth / BOOK_W));
+ });
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, []);
+
+ const clearTimers = () => {
+ timers.current.forEach(clearTimeout);
+ timers.current = [];
+ };
+ useEffect(() => () => clearTimers(), []);
+
+ const open = () => {
+ if (phase !== "closed") return;
+ clearTimers();
+ if (reduced) {
+ setPhase("open");
+ setOpened(true);
+ return;
+ }
+ setPhase("flash");
+ timers.current.push(setTimeout(() => setPhase("open"), 420));
+ timers.current.push(setTimeout(() => setOpened(true), 420 + 720));
+ };
+
+ const close = () => {
+ clearTimers();
+ setOpened(false);
+ setPhase("closed");
+ };
+
+ const closed = phase === "closed";
+ const bookTransform =
+ phase === "open"
+ ? "translateX(0px) translateZ(0.01px) rotateZ(0deg) scale(1)"
+ : phase === "flash"
+ ? "translateX(-170px) translateZ(0.01px) rotateZ(0deg) scale(1)"
+ : "translateX(-170px) translateZ(0.01px) rotateZ(-4deg) scale(0.94)";
+ const coverRotate = phase === "open" ? "-168deg" : "0deg";
+ const bookTransition = reduced
+ ? "none"
+ : "transform 820ms cubic-bezier(.22,1,.32,1)";
+ const coverTransition = reduced
+ ? "none"
+ : "transform 1100ms cubic-bezier(.5,.05,.2,1)";
+
+ return (
+
+
+
+
+ {/* Header chrome */}
+
+
Members · Pillar 03
+
+ My Passport
+
+
+ {stampCount} stamps · {cities} cities
+
+
+
+ {/* Stage */}
+
+
+
+
+ {/* Contact shadow */}
+
+
+ {/* RIGHT PAGE (base) */}
+
+
+ {/* COVER (rotates open) */}
+
+ {/* FRONT = cover art */}
+
+
+
+
+
+
+
+
+ Global Hackathon Union
+
+
+
+
+
+
+ HACKER
+
+
+ PASSPORT
+
+
+
+
+
+
+
+ TYPE H · HQ · CODE 2026
+
+
+
+
+
+ {/* BACK = inside-left page */}
+
+
+
+ {/* Spine shadow */}
+
+
+
+
+
+
+ {/* Open / close control */}
+
+
+
+
+
+ );
+}