{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"paper-portfolio","type":"registry:page","title":"Paper Portfolio","description":"A seven-page torn-paper portfolio: hero collage, case studies, a printable capabilities page and a draggable sketchbook. Ships the six original PNGs — the hero collage and five logo marks — with the rest of the artwork drawn in SVG.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"app/paper-portfolio/layout.tsx","target":"app/paper-portfolio/layout.tsx","content":"import type { Metadata } from \"next\"\n\nimport { SiteFooter } from \"./components/site-footer\"\nimport { SiteHeader } from \"./components/site-header\"\nimport { profile } from \"./data/projects\"\n\nimport \"./paper-portfolio.css\"\n\nexport const metadata: Metadata = {\n  title: {\n    default: `${profile.name} ${profile.surname} — ${profile.role}`,\n    template: `%s — ${profile.name} ${profile.surname}`,\n  },\n  description: `Portfolio of ${profile.name} ${profile.surname} — AI engineer, developer and builder creating AI products, web experiences and experimental software.`,\n}\n\n/**\n * The template shell.\n *\n * This is a nested layout, so it renders no `<html>` or `<body>` — the app's root\n * layout owns those. Two wrappers instead: `.paperDesk` paints the surface the\n * sheet sits on, `.paperSite` is the sheet and the scope for every style in\n * `paper-portfolio.css`.\n */\nexport default function PaperPortfolioLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"paperDesk\">\n      <div className=\"paperSite\">\n        <div className=\"paperNoise\" aria-hidden=\"true\" />\n        <SiteHeader />\n        <main>{children}</main>\n        <SiteFooter />\n      </div>\n    </div>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/paper-portfolio.css","target":"app/paper-portfolio/paper-portfolio.css","content":"/*\n * paper-portfolio — a torn-paper scrapbook portfolio.\n *\n * Every selector is scoped under `.paperSite` on purpose. A template that ships\n * two hundred names like `.projectCard`, `.brand` and `.eyebrow` into a\n * consumer's global stylesheet is a collision waiting to happen; the wrapper\n * costs one div and makes the whole sheet inert outside it.\n *\n * The palette is the only thing most people will want to change. It is at the\n * top, and nothing below hard-codes a colour that is already a variable.\n */\n/*\n * The desk the sheet sits on. The original painted this colour on `body`; a\n * template has no business doing that, so it becomes one wrapper element.\n */\n.paperDesk {\n  background: #d8d1c5;\n  overflow-x: hidden;\n}\n\n.paperSite {\n  --paper: #f3eee3;\n  --paper-light: #faf6ed;\n  --paper-deep: #e9dfce;\n  --ink: #11100d;\n  --muted: #5b554d;\n  --line: #bbae96;\n  --yellow: #f1cf58;\n  --sticky: #ead17a;\n  --orange: #e8622b;\n  --shadow: rgba(57, 45, 29, 0.16);\n\n  /* Swap these two to re-voice the whole site. */\n  --font-hand: var(--font-paper-hand, \"Segoe Print\", \"Bradley Hand\", \"Comic Sans MS\", cursive);\n  --font-body: Arial, Helvetica, sans-serif;\n\n  position: relative;\n  width: min(1648px, 100%);\n  min-height: 100vh;\n  margin: 0 auto;\n  background: var(--paper);\n  color: var(--ink);\n  font-family: var(--font-hand);\n  box-shadow: 0 26px 80px rgba(0, 0, 0, 0.14);\n  overflow: hidden;\n  /* Contains the grain's multiply blend so it cannot reach the host page. */\n  isolation: isolate;\n}\n\n.paperSite *,\n.paperSite *::before,\n.paperSite *::after { box-sizing: border-box; }\n.paperSite a { color: inherit; text-decoration: none; }\n.paperSite button,\n.paperSite input,\n.paperSite textarea { font: inherit; color: inherit; }\n.paperSite button,\n.paperSite a { -webkit-tap-highlight-color: transparent; }\n.paperSite img { display: block; max-width: 100%; }\n.paperSite ::selection { background: var(--yellow); color: #111; }\n.paperSite main { min-height: 70vh; }\n/* The grain. Two dot grids and a wash, multiplied over everything — it is what\n * keeps large flat areas from reading as flat colour. Absolute, not fixed, so it\n * scrolls with the page instead of shimmering against it. */\n.paperSite .paperNoise {\n  position: absolute;\n  inset: 0;\n  pointer-events: none;\n  opacity: 0.48;\n  z-index: 100;\n  background-image:\n    radial-gradient(#342d2313 0.72px, transparent 0.85px),\n    radial-gradient(#fff8 1px, transparent 1.3px),\n    linear-gradient(90deg, #ffffff0e, #9b87660b 46%, #fff0);\n  background-size: 5px 5px, 9px 9px, 100% 100%;\n  mix-blend-mode: multiply;\n}\n\n/* ---------- header ---------- */\n.paperSite .siteHeader {\n  height: 86px;\n  padding: 17px 48px 10px;\n  display: grid;\n  grid-template-columns: 450px 1fr auto;\n  align-items: center;\n  position: relative;\n  z-index: 50;\n}\n.paperSite .brand {\n  width: max-content;\n  display: flex;\n  align-items: center;\n  gap: 11px;\n  font-size: 24px;\n  font-weight: 800;\n  transform: rotate(-1deg);\n  position: relative;\n}\n.paperSite .brandStar { font: 38px Georgia, serif; line-height: 1; }\n.paperSite .brandUnderline {\n  position: absolute;\n  width: 140px;\n  border-bottom: 2px solid var(--ink);\n  left: 54px;\n  bottom: -5px;\n  transform: rotate(-2deg);\n}\n.paperSite .mainNav { display: flex; justify-content: center; gap: 46px; font-size: 17px; font-weight: 650; }\n.paperSite .mainNav a { position: relative; padding: 8px 2px; }\n.paperSite .mainNav a::after {\n  content: \"\";\n  position: absolute;\n  height: 4px;\n  left: -8px;\n  right: -8px;\n  bottom: 0;\n  background: transparent;\n  border-radius: 50%;\n  transform: rotate(-2deg);\n}\n.paperSite .mainNav a:hover::after,\n.paperSite .mainNav a.active::after { background: var(--ink); }\n.paperSite .talkButton {\n  color: #fff;\n  background: #070707;\n  padding: 12px 16px;\n  transform: rotate(-1deg);\n  box-shadow: 5px 4px 0 rgba(0, 0, 0, 0.12);\n  transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n.paperSite .talkButton:hover { transform: rotate(1deg) translateY(-2px); box-shadow: 7px 7px 0 rgba(0, 0, 0, 0.12); }\n.paperSite .talkButton b { font-size: 24px; margin-left: 7px; }\n.paperSite .talkBubble { margin-right: 7px; }\n.paperSite .menuButton { display: none; }\n\n/* ---------- home ---------- */\n.paperSite .homeHero { position: relative; min-height: 545px; margin: 0 48px; }\n.paperSite .heroCopy { position: absolute; left: 30px; top: 25px; width: 515px; z-index: 8; }\n.paperSite .helloLine { font-size: 21px; transform: rotate(-3deg); width: max-content; }\n.paperSite .helloLine span { font-size: 35px; display: inline-block; transform: rotate(-20deg) translateY(4px); }\n.paperSite .heroCrown { position: absolute; left: 365px; top: -4px; font: 66px Georgia, serif; transform: rotate(-13deg); }\n.paperSite .heroCopy h1,\n.paperSite .pageIntro h1,\n.paperSite .caseHero h1,\n.paperSite .resumePage h1 {\n  margin: 2px 0 0;\n  font-size: 70px;\n  line-height: 0.94;\n  letter-spacing: -2.2px;\n  font-weight: 850;\n  transform: rotate(-1deg);\n}\n.paperSite .heroCopy h1 mark,\n.paperSite .pageIntro h1 mark,\n.paperSite .resumePage h1 mark {\n  background: linear-gradient(transparent 18%, var(--yellow) 18%, var(--yellow) 89%, transparent 89%);\n  color: inherit;\n  padding: 0 10px 4px 2px;\n}\n.paperSite .heroCopy h2 { margin: 10px 0 0 68px; font-size: 21px; font-weight: 500; }\n.paperSite .orangeUnderline { display: block; width: 240px; border-top: 4px solid var(--orange); margin: 4px 0 0 130px; transform: rotate(-3deg); }\n.paperSite .heroStatement { margin: 18px 0 10px; font-size: 42px; line-height: 1.08; letter-spacing: 0.5px; font-weight: 650; }\n.paperSite .heroStatement em {\n  font-style: normal;\n  background: linear-gradient(transparent 32%, #eb6b34 32%, #eb6b34 88%, transparent 88%);\n  padding: 0 6px;\n}\n.paperSite .heroIntro { max-width: 472px; font-size: 14.5px; line-height: 1.5; color: #29261f; }\n.paperSite .heroActions { display: flex; align-items: center; gap: 34px; margin-top: 16px; }\n.paperSite .inkButton {\n  display: inline-flex;\n  align-items: center;\n  gap: 12px;\n  border: 0;\n  background: #090909;\n  color: #fff;\n  padding: 13px 18px;\n  box-shadow: 4px 5px 0 #b9ad9a;\n  transform: rotate(-1deg);\n  cursor: pointer;\n  transition: transform 0.18s ease, box-shadow 0.18s ease;\n}\n.paperSite .inkButton:hover { transform: rotate(1deg) translateY(-3px); box-shadow: 7px 8px 0 #b9ad9a; }\n.paperSite .underLink { border-bottom: 2px solid var(--ink); padding-bottom: 5px; }\n\n/* The torn bottom edge is a clip-path, not an image: it scales, recolours with\n * the panel, and costs nothing to download. */\n.paperSite .identityNote {\n  position: absolute;\n  left: 555px;\n  top: 35px;\n  width: 228px;\n  height: 292px;\n  padding: 42px 26px 26px;\n  background: var(--paper-light);\n  border: 1px solid #c9bda9;\n  box-shadow: 0 8px 14px var(--shadow);\n  transform: rotate(-2deg);\n  z-index: 10;\n  clip-path: polygon(2% 2%, 98% 0, 97% 96%, 88% 98%, 78% 96%, 70% 99%, 60% 96%, 50% 99%, 42% 96%, 32% 99%, 22% 96%, 12% 99%, 2% 96%);\n}\n.paperSite .topTape {\n  position: absolute;\n  left: 50%;\n  top: -13px;\n  width: 80px;\n  height: 30px;\n  background: rgba(212, 190, 151, 0.63);\n  transform: translateX(-50%) rotate(4deg);\n}\n.paperSite .identityNote div { display: flex; gap: 13px; align-items: flex-start; margin-bottom: 18px; font-size: 13.5px; line-height: 1.45; }\n.paperSite .identityNote b { font-family: var(--font-body); font-size: 23px; line-height: 1; }\n.paperSite .identityNote a { text-decoration: underline; text-underline-offset: 4px; }\n\n.paperSite .heroMotion {\n  position: absolute;\n  left: 760px;\n  top: -4px;\n  width: 565px;\n  height: 520px;\n  transform: translate(var(--hero-x, 0), var(--hero-y, 0));\n  transition: transform 0.12s linear;\n  z-index: 4;\n}\n.paperSite .heroArtwork { position: relative; width: 100%; height: 100%; }\n.paperSite .heroArtwork svg { width: 100%; height: 100%; display: block; }\n.paperSite .artTape { position: absolute; z-index: 4; width: 80px; height: 28px; background: rgba(215, 194, 157, 0.67); }\n.paperSite .artTapeOne { left: 78px; top: 8px; transform: rotate(8deg); }\n.paperSite .artTapeTwo { right: 62px; top: 26px; transform: rotate(-7deg); }\n\n.paperSite .stickyNote { background: var(--sticky); box-shadow: 0 8px 12px var(--shadow); }\n.paperSite .whatIDo {\n  position: absolute;\n  right: 26px;\n  top: 58px;\n  width: 275px;\n  min-height: 266px;\n  padding: 30px 31px 24px;\n  transform: rotate(3deg);\n  clip-path: polygon(0 3%, 97% 0, 100% 96%, 3% 100%);\n  z-index: 12;\n}\n.paperSite .whatIDo h3 { margin: 0 0 15px; text-align: center; padding-bottom: 7px; border-bottom: 2px solid var(--ink); font-size: 20px; }\n.paperSite .whatIDo ul { list-style: none; padding: 0; margin: 0; line-height: 2; font-size: 16px; }\n.paperSite .whatIDo li::before { content: \"★\"; font-size: 11px; margin-right: 12px; }\n.paperSite .rocketDoodle { position: absolute; right: 14px; bottom: 8px; font-size: 34px; transform: rotate(-30deg); }\n.paperSite .aboutSnippet { position: absolute; right: 72px; top: 420px; width: 330px; z-index: 15; }\n.paperSite .curvedArrow { position: absolute; left: -66px; top: 6px; font-size: 54px; transform: rotate(33deg); }\n.paperSite .aboutSnippet h3 { margin: 0 0 12px; padding-bottom: 7px; border-bottom: 2px solid var(--ink); font-size: 17px; letter-spacing: 1px; }\n.paperSite .aboutSnippet p { margin: 0 0 17px; font-size: 13px; line-height: 1.55; }\n.paperSite .blackNote {\n  display: inline-block;\n  background: #080808;\n  color: #fff;\n  padding: 9px 15px;\n  font-size: 12.5px;\n  transform: rotate(2deg);\n  box-shadow: 4px 4px 0 rgba(54, 43, 30, 0.19);\n}\n.paperSite .blackNote span { color: var(--orange); font-size: 21px; margin-left: 8px; }\n\n.paperSite .featuredSection { padding: 4px 78px 0; }\n.paperSite .sectionTitleLine { display: flex; align-items: end; gap: 14px; margin-bottom: 12px; }\n.paperSite .sectionTitleLine h2 { margin: 0; font-size: 17px; letter-spacing: 0.5px; border-bottom: 2px solid var(--ink); padding-bottom: 6px; min-width: 176px; }\n.paperSite .sectionTitleLine > span { font-size: 25px; }\n.paperSite .sectionTitleLine > a { margin-left: auto; transform: rotate(1deg); border-bottom: 1px dashed var(--ink); }\n.paperSite .homeProjectGrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 17px; }\n.paperSite .projectCard {\n  min-height: 210px;\n  position: relative;\n  background: var(--paper-light);\n  border: 1px solid #c7baa5;\n  box-shadow: 0 5px 10px var(--shadow);\n  padding: 20px;\n  display: grid;\n  grid-template-columns: 84px 1fr 28px;\n  gap: 15px;\n  align-items: start;\n  transform: rotate(var(--tilt, 0deg));\n  clip-path: polygon(0 2%, 99% 0, 100% 95%, 95% 97%, 89% 95%, 84% 98%, 77% 95%, 70% 98%, 63% 95%, 56% 98%, 48% 95%, 40% 98%, 32% 95%, 24% 98%, 16% 95%, 8% 98%, 0 95%);\n  transition: transform 0.18s ease, box-shadow 0.18s ease;\n}\n.paperSite .projectCard:hover { transform: translateY(-7px) rotate(0deg); box-shadow: 0 15px 24px rgba(57, 45, 29, 0.22); }\n.paperSite .projectMark svg { width: 100%; height: 100%; display: block; }\n.paperSite .projectCard .projectMark { width: 82px; height: 82px; border-radius: 16px; overflow: hidden; background: #111; }\n.paperSite .projectCardCopy h3 { margin: 4px 0 5px; font-size: 21px; }\n.paperSite .projectCardCopy p { margin: 0; font-family: var(--font-body); line-height: 1.45; font-size: 13px; color: #4a463f; }\n.paperSite .projectMeta { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #6c645b; }\n.paperSite .projectArrow { align-self: end; justify-self: end; font-size: 28px; }\n.paperSite .projectCard.compact { min-height: 145px; grid-template-columns: 68px 1fr 26px; padding: 16px 13px; }\n.paperSite .projectCard.compact .projectMark { width: 66px; height: 66px; border-radius: 14px; }\n.paperSite .projectCard.compact .projectCardCopy h3 { font-size: 16px; margin: 2px 0 4px; }\n.paperSite .projectCard.compact .projectMeta { display: none; }\n.paperSite .projectCard.compact .projectCardCopy p { font-size: 12px; }\n\n.paperSite .homeBottomRow { margin: 25px 78px 42px; display: grid; grid-template-columns: 1.3fr 0.6fr 190px; gap: 36px; align-items: start; }\n.paperSite .toolsBlock h2,\n.paperSite .connectBlock h2 { font-size: 17px; margin: 0; padding-bottom: 7px; border-bottom: 2px solid var(--line); }\n.paperSite .toolChips { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }\n.paperSite .toolChips span { display: flex; align-items: center; gap: 8px; padding: 9px 11px; border: 1px solid #c8bda8; background: #f8f3e9; font-size: 13px; }\n.paperSite .toolChips b {\n  width: 30px;\n  height: 30px;\n  display: grid;\n  place-items: center;\n  background: #0b0b0b;\n  color: #fff;\n  border-radius: 8px;\n  font: 700 12px var(--font-body);\n}\n.paperSite .socialRow { display: flex; gap: 16px; margin-top: 18px; }\n.paperSite .socialRow a {\n  width: 46px;\n  height: 46px;\n  display: grid;\n  place-items: center;\n  border-radius: 50%;\n  background: #090909;\n  color: #fff;\n  font: 700 13px var(--font-body);\n  transition: transform 0.18s ease;\n}\n.paperSite .socialRow a:hover { transform: rotate(-8deg) translateY(-4px); }\n.paperSite .conversationNote {\n  padding: 18px 15px;\n  background: #eee2c8;\n  border: 1px solid #d3c5a5;\n  transform: rotate(-3deg);\n  font-size: 13px;\n  line-height: 1.35;\n  box-shadow: 0 6px 10px var(--shadow);\n  position: relative;\n}\n.paperSite .conversationNote::before {\n  content: \"\";\n  position: absolute;\n  right: 10px;\n  top: -12px;\n  width: 58px;\n  height: 23px;\n  background: rgba(214, 196, 156, 0.7);\n  transform: rotate(10deg);\n}\n/* ---------- shared interior page ---------- */\n.paperSite .pageIntro { position: relative; margin: 50px 78px 45px; max-width: 1100px; min-height: 205px; }\n.paperSite .eyebrow { display: inline-block; font-family: var(--font-body); font-size: 11px; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 13px; }\n.paperSite .pageIntro h1 { font-size: 74px; max-width: 930px; }\n.paperSite .pageIntro p { max-width: 700px; font-size: 17px; line-height: 1.6; margin: 24px 0 0; color: #413c35; }\n.paperSite .introDoodle { position: absolute; left: 870px; top: 65px; font-size: 75px; transform: rotate(14deg); }\n\n.paperSite .paperPanel { background: var(--paper-light); border: 1px solid #c9bca6; box-shadow: 8px 9px 0 #ded2bd; position: relative; }\n.paperSite .workEditorial { margin: 0 78px 60px; display: grid; grid-template-columns: 260px 1fr; gap: 34px; align-items: start; }\n.paperSite .workLeadNote { position: sticky; top: 24px; background: var(--sticky); padding: 36px 24px 28px; transform: rotate(-2deg); box-shadow: 0 8px 15px var(--shadow); }\n.paperSite .workLeadNote .topTape { top: -14px; }\n.paperSite .workLeadNote strong { font-size: 22px; display: block; margin-bottom: 10px; }\n.paperSite .workLeadNote p { font-family: var(--font-body); font-size: 14px; line-height: 1.6; }\n.paperSite .scribble { display: block; margin-top: 35px; transform: rotate(-4deg); }\n.paperSite .fullProjectGrid,\n.paperSite .projectLibrary { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }\n.paperSite .projectLibrary { margin: 0 78px 70px; }\n.paperSite .pageCTA {\n  margin: 30px 78px 70px;\n  padding: 35px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  border-top: 2px solid var(--line);\n  border-bottom: 2px solid var(--line);\n}\n.paperSite .pageCTA p { font-size: 28px; margin: 0; }\n\n/* ---------- about ---------- */\n.paperSite .aboutPageGrid { margin: 0 78px 70px; display: grid; grid-template-columns: 470px 1fr; gap: 75px; align-items: center; }\n.paperSite .aboutPortrait { padding: 18px; transform: rotate(-1deg); }\n.paperSite .aboutPortrait svg { width: 100%; height: auto; display: block; }\n.paperSite .handCaption { position: absolute; right: 15px; bottom: 5px; transform: rotate(-5deg); background: var(--paper-light); padding: 5px 8px; }\n.paperSite .aboutStory h2 { font-size: 42px; margin: 0 0 18px; }\n.paperSite .aboutStory > p { font-family: var(--font-body); font-size: 16px; line-height: 1.75; color: #403b34; }\n.paperSite .principleNotes { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 28px; }\n.paperSite .principleNotes span { border: 1px solid var(--line); padding: 14px; background: var(--paper-light); transform: rotate(-0.6deg); }\n.paperSite .principleNotes span:nth-child(even) { transform: rotate(0.7deg); }\n.paperSite .timelineSection { margin: 0 78px 70px; }\n.paperSite .timelineList { border-top: 1px solid var(--line); }\n.paperSite .timelineList article { display: grid; grid-template-columns: 70px 260px 1fr; gap: 22px; align-items: center; padding: 20px 5px; border-bottom: 1px solid var(--line); }\n.paperSite .timelineList article > span { font-family: var(--font-body); font-size: 11px; }\n.paperSite .timelineList h3 { margin: 0; font-size: 20px; }\n.paperSite .timelineList p { margin: 0; font-family: var(--font-body); font-size: 14px; color: #514b43; }\n/* ---------- case study ---------- */\n.paperSite .caseStudy { margin: 50px 78px 80px; }\n.paperSite .backLink { border-bottom: 1px solid var(--ink); }\n.paperSite .caseHero { margin-top: 38px; display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 70px; align-items: center; }\n.paperSite .caseHero h1 { font-size: 84px; margin: 12px 0; }\n.paperSite .caseHero > div:first-child > p { max-width: 650px; font-size: 22px; line-height: 1.55; }\n.paperSite .caseStatus { margin-top: 28px; font: 11px var(--font-body); letter-spacing: 1.6px; }\n.paperSite .caseStatus b { margin-left: 12px; font-family: inherit; background: var(--yellow); padding: 8px 10px; }\n.paperSite .caseArtwork { min-height: 390px; padding: 50px; display: grid; place-items: center; transform: rotate(1.2deg); }\n.paperSite .caseArtwork .projectMark { width: 230px; height: 230px; border-radius: 40px; overflow: hidden; background: #111; }\n.paperSite .caseDoodle { position: absolute; bottom: 18px; right: 22px; transform: rotate(-5deg); }\n.paperSite .caseBody { margin-top: 70px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 25px; }\n.paperSite .caseBody article {\n  min-height: 245px;\n  padding: 27px;\n  background: var(--paper-light);\n  border: 1px solid var(--line);\n  box-shadow: 6px 7px 0 #dfd3bf;\n  transform: rotate(-0.6deg);\n}\n.paperSite .caseBody article:nth-child(2) { transform: rotate(0.5deg); }\n.paperSite .caseBody article:nth-child(3) { transform: rotate(-0.2deg); }\n.paperSite .caseBody article > span { font: 11px var(--font-body); }\n.paperSite .caseBody h2 { font-size: 28px; margin: 13px 0; }\n.paperSite .caseBody p { font-family: var(--font-body); line-height: 1.65; color: #48433b; }\n.paperSite .caseTags { display: flex; flex-wrap: wrap; gap: 9px; }\n.paperSite .caseTags b { font: 600 12px var(--font-body); padding: 9px 10px; border: 1px solid var(--ink); }\n.paperSite .caseFooter { margin-top: 55px; display: flex; align-items: center; gap: 35px; }\n\n/* ---------- contact ---------- */\n.paperSite .contactPageGrid { margin: 0 78px 70px; display: grid; grid-template-columns: 1fr 390px; gap: 60px; align-items: start; }\n.paperSite .contactFormWrap { background: var(--paper-light); border: 1px solid var(--line); box-shadow: 9px 10px 0 #ddd0bb; padding: 32px; transform: rotate(-0.3deg); }\n.paperSite .contactForm { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }\n.paperSite .contactForm label { display: grid; gap: 9px; }\n.paperSite .contactForm label span { font-size: 14px; }\n.paperSite .contactForm input,\n.paperSite .contactForm textarea {\n  width: 100%;\n  border: 0;\n  border-bottom: 2px solid var(--ink);\n  background: transparent;\n  padding: 10px 4px;\n  outline: none;\n  resize: vertical;\n  font-family: var(--font-body);\n}\n.paperSite .contactForm input:focus,\n.paperSite .contactForm textarea:focus { border-bottom-color: var(--orange); }\n.paperSite .fullField { grid-column: 1 / -1; }\n.paperSite .paperButton { border: 0; border-bottom: 2px solid var(--ink); background: transparent; cursor: pointer; width: max-content; }\n.paperSite .formHint { margin: 20px 0 0; background: var(--yellow); width: max-content; max-width: 100%; padding: 8px 10px; }\n/* The hint is a live region, so it stays in the DOM and takes no room until it\n * actually has something to announce. */\n.paperSite .formHint:empty { display: none; }\n.paperSite .contactSidebar { display: grid; gap: 28px; }\n.paperSite .contactSticky { padding: 38px 25px 28px; transform: rotate(2deg); position: relative; }\n.paperSite .contactSticky h2 { margin: 0 0 18px; font-size: 28px; }\n.paperSite .contactSticky a { text-decoration: underline; text-underline-offset: 4px; }\n.paperSite .contactSticky p { margin: 28px 0 0; line-height: 1.6; }\n.paperSite .contactLinks { display: grid; padding: 20px; transform: rotate(-1deg); }\n.paperSite .contactLinks a { display: flex; justify-content: space-between; padding: 13px 0; border-bottom: 1px solid var(--line); }\n\n/* ---------- sketchbook ---------- */\n.paperSite .sketchBoard {\n  position: relative;\n  height: 650px;\n  margin: 0 78px 80px;\n  border: 2px solid var(--ink);\n  background:\n    repeating-linear-gradient(0deg, transparent 0 31px, rgba(54, 49, 41, 0.08) 32px),\n    repeating-linear-gradient(90deg, transparent 0 31px, rgba(54, 49, 41, 0.08) 32px),\n    var(--paper-light);\n  overflow: hidden;\n}\n.paperSite .draggableNote {\n  position: absolute;\n  width: 285px;\n  min-height: 180px;\n  padding: 31px 22px 22px;\n  box-shadow: 8px 8px 0 rgba(54, 43, 30, 0.12);\n  touch-action: none;\n  user-select: none;\n  cursor: grab;\n  transform: rotate(-2deg);\n}\n.paperSite .draggableNote:active { cursor: grabbing; z-index: 10; }\n.paperSite .draggableNote.yellow { background: var(--sticky); }\n.paperSite .draggableNote.cream { background: #f7f0e2; border: 1px solid var(--line); }\n.paperSite .draggableNote.orange { background: #e88955; }\n.paperSite .draggableNote:nth-of-type(even) { transform: rotate(2deg); }\n.paperSite .noteTape {\n  position: absolute;\n  left: 50%;\n  top: -11px;\n  width: 75px;\n  height: 26px;\n  transform: translateX(-50%) rotate(-3deg);\n  background: rgba(214, 196, 156, 0.74);\n}\n.paperSite .draggableNote small { position: absolute; right: 12px; top: 10px; font-size: 9px; font-family: var(--font-body); opacity: 0.55; }\n.paperSite .draggableNote h3 { margin: 7px 0 10px; font-size: 24px; }\n.paperSite .draggableNote p { margin: 0; font-family: var(--font-body); line-height: 1.5; }\n.paperSite .boardDoodle { position: absolute; font-size: 40px; pointer-events: none; }\n.paperSite .boardDoodleOne { right: 11%; top: 10%; }\n.paperSite .boardDoodleTwo { left: 48%; bottom: 13%; font-size: 70px; transform: rotate(-30deg); }\n.paperSite .boardDoodleThree { right: 7%; bottom: 6%; font-size: 20px; transform: rotate(4deg); }\n\n/* ---------- resume ---------- */\n.paperSite .resumePage { margin: 48px 78px 80px; background: var(--paper-light); border: 1px solid var(--line); box-shadow: 10px 11px 0 #d9ccb7; padding: 48px; }\n.paperSite .resumePage > header { display: grid; grid-template-columns: 1fr auto; gap: 50px; padding-bottom: 32px; border-bottom: 2px solid var(--ink); }\n.paperSite .resumePage h1 { font-size: 65px; }\n.paperSite .resumePage header p { font-size: 18px; }\n.paperSite .resumeContact { display: grid; gap: 9px; align-content: end; font-family: var(--font-body); font-size: 13px; text-align: right; }\n.paperSite .resumeContact a { text-decoration: underline; }\n.paperSite .resumeColumns { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; padding: 35px 0; }\n.paperSite .resumeColumns h2 { font-size: 22px; border-bottom: 1px solid var(--line); padding-bottom: 8px; margin: 0 0 15px; }\n.paperSite .resumeColumns p,\n.paperSite .resumeColumns li { font-family: var(--font-body); line-height: 1.65; color: #3f3b34; }\n.paperSite .resumeColumns ul { padding-left: 20px; margin: 0 0 28px; }\n.paperSite .resumeActions { display: flex; gap: 30px; align-items: center; border-top: 1px solid var(--line); padding-top: 25px; }\n.paperSite .printNote { font-size: 12px; color: var(--muted); }\n\n/* ---------- footer ---------- */\n.paperSite .siteFooter {\n  margin: 25px 48px 0;\n  border-top: 2px solid var(--ink);\n  padding: 30px 30px 38px;\n  display: flex;\n  justify-content: space-between;\n  align-items: end;\n}\n.paperSite .siteFooter > div:first-child { position: relative; padding-left: 40px; }\n.paperSite .footerStar { position: absolute; left: 0; top: -3px; font: 30px Georgia, serif; }\n.paperSite .siteFooter strong { font-size: 18px; }\n.paperSite .siteFooter p { margin: 5px 0 0; font: 12px var(--font-body); color: #5d574e; }\n.paperSite .footerLinks { display: flex; gap: 25px; font-size: 13px; }\n.paperSite .footerLinks a:hover { text-decoration: underline; text-underline-offset: 5px; }\n\n/* ---------- responsive ---------- */\n@media (max-width: 1250px) {\n  .paperSite .siteHeader { grid-template-columns: 320px 1fr auto; }\n  .paperSite .mainNav { gap: 24px; font-size: 15px; }\n  .paperSite .homeHero { min-height: 700px; }\n  .paperSite .heroMotion { left: auto; right: 110px; top: 0; width: 520px; }\n  .paperSite .identityNote { left: 470px; }\n  .paperSite .whatIDo { right: 10px; top: 355px; }\n  .paperSite .aboutSnippet { top: 520px; right: 340px; }\n  .paperSite .homeProjectGrid { grid-template-columns: 1fr 1fr; }\n  .paperSite .homeBottomRow { grid-template-columns: 1fr 0.55fr; }\n  .paperSite .conversationNote { grid-column: 2; }\n}\n@media (max-width: 900px) {\n  .paperSite { width: 100%; box-shadow: none; }\n  .paperSite .siteHeader { height: auto; min-height: 76px; padding: 14px 20px; grid-template-columns: 1fr auto; }\n  .paperSite .brand { font-size: 20px; z-index: 70; }\n  .paperSite .brandStar { font-size: 30px; }\n  .paperSite .brandUnderline { width: 115px; left: 45px; }\n  .paperSite .menuButton {\n    display: grid;\n    width: 44px;\n    height: 42px;\n    place-content: center;\n    gap: 7px;\n    border: 1px solid var(--ink);\n    background: var(--paper-light);\n    cursor: pointer;\n    z-index: 70;\n  }\n  .paperSite .menuButton span { width: 22px; border-top: 2px solid var(--ink); }\n  .paperSite .talkButton { display: none; }\n  .paperSite .mainNav {\n    display: none;\n    position: absolute;\n    left: 12px;\n    right: 12px;\n    top: 68px;\n    background: var(--paper-light);\n    border: 1px solid var(--ink);\n    box-shadow: 7px 8px 0 #111;\n    padding: 25px;\n    grid-template-columns: 1fr 1fr;\n    z-index: 60;\n  }\n  .paperSite .mainNav.open { display: grid; }\n  .paperSite .mainNav a { padding: 12px 4px; }\n\n  .paperSite .homeHero { margin: 0 20px; min-height: auto; display: flex; flex-direction: column; gap: 24px; }\n  .paperSite .heroCopy,\n  .paperSite .identityNote,\n  .paperSite .heroMotion,\n  .paperSite .whatIDo,\n  .paperSite .aboutSnippet { position: relative; left: auto; right: auto; top: auto; width: 100%; height: auto; }\n  .paperSite .heroCopy { order: 1; }\n  .paperSite .heroCopy h1 { font-size: clamp(52px, 13vw, 78px); }\n  .paperSite .heroCrown { right: 0; left: auto; }\n  .paperSite .heroCopy h2 { margin-left: 0; }\n  .paperSite .orangeUnderline { margin-left: 40px; }\n  .paperSite .heroStatement { font-size: clamp(31px, 8vw, 44px); }\n  .paperSite .heroMotion { order: 2; transform: none; }\n  .paperSite .heroArtwork { height: auto; }\n  .paperSite .heroArtwork svg { height: auto; }\n  .paperSite .identityNote { order: 3; min-height: 260px; clip-path: none; }\n  .paperSite .whatIDo { order: 4; max-width: 350px; align-self: center; }\n  .paperSite .aboutSnippet { order: 5; max-width: 430px; align-self: flex-end; padding-left: 35px; }\n\n  .paperSite .featuredSection { padding: 34px 20px 0; }\n  .paperSite .sectionTitleLine { flex-wrap: wrap; }\n  .paperSite .sectionTitleLine > a { width: 100%; margin: 2px 0 0; }\n  .paperSite .homeProjectGrid,\n  .paperSite .fullProjectGrid,\n  .paperSite .projectLibrary { grid-template-columns: 1fr; }\n  .paperSite .homeBottomRow { margin: 30px 20px 45px; grid-template-columns: 1fr; }\n  .paperSite .conversationNote { grid-column: auto; max-width: 230px; }\n\n  .paperSite .pageIntro { margin: 45px 20px 35px; min-height: auto; }\n  .paperSite .pageIntro h1 { font-size: clamp(50px, 13vw, 75px); }\n  .paperSite .pageIntro p { font-size: 15px; }\n  .paperSite .introDoodle { display: none; }\n  .paperSite .workEditorial { margin: 0 20px 50px; grid-template-columns: 1fr; }\n  .paperSite .workLeadNote { position: relative; top: 0; max-width: 420px; }\n  .paperSite .projectLibrary { margin: 0 20px 55px; }\n  .paperSite .pageCTA { margin: 20px; padding: 25px 0; flex-direction: column; align-items: flex-start; gap: 18px; }\n  .paperSite .pageCTA p { font-size: 23px; }\n  .paperSite .aboutPageGrid { margin: 0 20px 55px; grid-template-columns: 1fr; gap: 35px; }\n  .paperSite .aboutPortrait { max-width: 520px; }\n  .paperSite .timelineSection { margin: 0 20px 55px; }\n  .paperSite .timelineList article { grid-template-columns: 50px 1fr; }\n  .paperSite .timelineList article p { grid-column: 2; }\n  .paperSite .caseStudy { margin: 40px 20px 60px; }\n  .paperSite .caseHero { grid-template-columns: 1fr; gap: 30px; }\n  .paperSite .caseHero h1 { font-size: clamp(58px, 15vw, 84px); }\n  .paperSite .caseHero > div:first-child > p { font-size: 18px; }\n  .paperSite .caseArtwork { min-height: 320px; }\n  .paperSite .caseBody { grid-template-columns: 1fr; }\n  .paperSite .caseFooter { flex-wrap: wrap; }\n  .paperSite .contactPageGrid { margin: 0 20px 60px; grid-template-columns: 1fr; }\n  .paperSite .contactForm { grid-template-columns: 1fr; }\n  .paperSite .fullField { grid-column: auto; }\n  .paperSite .sketchBoard { margin: 0 20px 60px; height: 760px; }\n  .paperSite .draggableNote { width: min(270px, 70vw); }\n  .paperSite .resumePage { margin: 30px 20px 60px; padding: 26px; }\n  .paperSite .resumePage > header,\n  .paperSite .resumeColumns { grid-template-columns: 1fr; }\n  .paperSite .resumeContact { text-align: left; }\n  .paperSite .resumePage h1 { font-size: 54px; }\n  .paperSite .resumeActions { flex-wrap: wrap; }\n  .paperSite .siteFooter { margin: 15px 20px 0; padding: 25px 4px 35px; flex-direction: column; align-items: flex-start; gap: 25px; }\n  .paperSite .footerLinks { flex-wrap: wrap; gap: 15px 22px; }\n}\n@media (max-width: 520px) {\n  .paperSite .heroActions { align-items: flex-start; flex-direction: column; gap: 18px; }\n  .paperSite .projectCard,\n  .paperSite .projectCard.compact { grid-template-columns: 64px 1fr 22px; padding: 14px 12px; }\n  .paperSite .projectCard .projectMark,\n  .paperSite .projectCard.compact .projectMark { width: 62px; height: 62px; }\n  .paperSite .principleNotes { grid-template-columns: 1fr; }\n  .paperSite .sketchBoard { height: 900px; }\n}\n\n/*\n * Print exists for one page — /resume. Everything that is decoration on screen\n * is noise on paper, so the chrome is dropped rather than restyled.\n */\n@media print {\n  .paperSite { width: 100%; box-shadow: none; background: #fff; overflow: visible; }\n  .paperSite .siteHeader,\n  .paperSite .siteFooter,\n  .paperSite .paperNoise,\n  .paperSite .resumeActions,\n  .paperSite .printNote { display: none !important; }\n  .paperSite .resumePage { margin: 0; border: 0; box-shadow: none; padding: 25px; }\n}\n","type":"registry:style"},{"path":"app/paper-portfolio/page.tsx","target":"app/paper-portfolio/page.tsx","content":"import Link from \"next/link\"\n\nimport { TornCollage } from \"./components/doodles\"\nimport { HeroMotion } from \"./components/hero-motion\"\nimport { ProjectCard } from \"./components/project-card\"\nimport { featuredProjects, href, profile } from \"./data/projects\"\n\nconst TOOLS: Array<[string, string]> = [\n  [\"N\", \"Next.js\"],\n  [\"⚡\", \"FastAPI\"],\n  [\"Py\", \"Python\"],\n  [\"JS\", \"TypeScript\"],\n  [\"♟\", \"AI Agents\"],\n  [\"◇\", \"Design Systems\"],\n]\n\nexport default function PaperPortfolioHome() {\n  return (\n    <>\n      <section className=\"homeHero\">\n        <div className=\"heroCopy\">\n          <div className=\"helloLine\">\n            hey! I&apos;m <span aria-hidden=\"true\">↘</span>\n          </div>\n          <div className=\"heroCrown\" aria-hidden=\"true\">\n            ♔\n          </div>\n          <h1>\n            {profile.name}\n            <br />\n            <mark>{profile.surname}</mark>\n          </h1>\n          <h2>{profile.role}</h2>\n          <span className=\"orangeUnderline\" aria-hidden=\"true\" />\n          <p className=\"heroStatement\">\n            I build with code,\n            <br />\n            curiosity and <em>rebellion.</em>\n          </p>\n          <p className=\"heroIntro\">\n            I&apos;m {profile.name} {profile.surname}, an IIT student and developer from India\n            building AI tools, digital products, and handcrafted web experiences.\n          </p>\n          <div className=\"heroActions\">\n            <Link className=\"inkButton\" href={href(\"/work\")}>\n              View My Work <span aria-hidden=\"true\">→</span>\n            </Link>\n            <Link className=\"underLink\" href={href(\"/resume\")}>\n              View Resume <span aria-hidden=\"true\">↘</span>\n            </Link>\n          </div>\n        </div>\n\n        <aside className=\"identityNote\">\n          <span className=\"topTape\" aria-hidden=\"true\" />\n          <div>\n            <b aria-hidden=\"true\">⌖</b>\n            <span>{profile.location}</span>\n          </div>\n          <div>\n            <b aria-hidden=\"true\">◎</b>\n            <a href={profile.siteUrl} target=\"_blank\" rel=\"noreferrer\">\n              {profile.site}\n            </a>\n          </div>\n          <div>\n            <b aria-hidden=\"true\">◉</b>\n            <a href={profile.repoUrl} target=\"_blank\" rel=\"noreferrer\">\n              {profile.handle}\n            </a>\n          </div>\n          <div>\n            <b aria-hidden=\"true\">♜</b>\n            <span>{profile.studio}</span>\n          </div>\n        </aside>\n\n        <HeroMotion>\n          <div className=\"heroArtwork\">\n            <span className=\"artTape artTapeOne\" aria-hidden=\"true\" />\n            <span className=\"artTape artTapeTwo\" aria-hidden=\"true\" />\n            <TornCollage />\n          </div>\n        </HeroMotion>\n\n        <aside className=\"whatIDo stickyNote\">\n          <span className=\"topTape\" aria-hidden=\"true\" />\n          <h3>WHAT I DO</h3>\n          <ul>\n            <li>AI/ML Systems</li>\n            <li>Web Development</li>\n            <li>UI/UX Design</li>\n            <li>Automation</li>\n            <li>Product Building</li>\n          </ul>\n          <span className=\"rocketDoodle\" aria-hidden=\"true\">\n            ♧\n          </span>\n        </aside>\n\n        <aside className=\"aboutSnippet\">\n          <span className=\"curvedArrow\" aria-hidden=\"true\">\n            ↪\n          </span>\n          <h3>A BIT ABOUT ME</h3>\n          <p>\n            I love building things that live at the intersection of code, design and impact. From\n            LLM apps to playful experiments — I ship ideas that matter.\n          </p>\n          <Link className=\"blackNote\" href={href(\"/about\")}>\n            Always learning.\n            <br />\n            Always building. <span aria-hidden=\"true\">♡</span>\n          </Link>\n        </aside>\n      </section>\n\n      <section className=\"featuredSection\">\n        <div className=\"sectionTitleLine\">\n          <h2>FEATURED WORK</h2>\n          <span aria-hidden=\"true\">☆</span>\n          <Link href={href(\"/projects\")}>More coming soon! ↘</Link>\n        </div>\n        <div className=\"homeProjectGrid\">\n          {featuredProjects.map((project, index) => (\n            <ProjectCard project={project} compact key={project.slug} index={index} />\n          ))}\n        </div>\n      </section>\n\n      <section className=\"homeBottomRow\">\n        <div className=\"toolsBlock\">\n          <h2>TOOLS &amp; SUPERPOWERS</h2>\n          <div className=\"toolChips\">\n            {TOOLS.map(([glyph, tool]) => (\n              <span key={tool}>\n                <b aria-hidden=\"true\">{glyph}</b>\n                {tool}\n              </span>\n            ))}\n          </div>\n        </div>\n        <div className=\"connectBlock\">\n          <h2>LET&apos;S CONNECT</h2>\n          <div className=\"socialRow\">\n            <Link href={href(\"/contact\")} aria-label=\"Contact\">\n              <span aria-hidden=\"true\">✉</span>\n            </Link>\n            <a href={profile.repoUrl} target=\"_blank\" rel=\"noreferrer\" aria-label=\"GitHub\">\n              <span aria-hidden=\"true\">GH</span>\n            </a>\n            <a href={profile.siteUrl} target=\"_blank\" rel=\"noreferrer\" aria-label=\"Website\">\n              <span aria-hidden=\"true\">◎</span>\n            </a>\n          </div>\n        </div>\n        <Link className=\"conversationNote\" href={href(\"/contact\")}>\n          Great ideas start\n          <br />\n          with a conversation.\n          <br />\n          <b>Let&apos;s do something. →</b>\n        </Link>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/work/page.tsx","target":"app/paper-portfolio/work/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\n\nimport { PageIntro } from \"../components/page-intro\"\nimport { ProjectCard } from \"../components/project-card\"\nimport { href, projects } from \"../data/projects\"\n\nexport const metadata: Metadata = { title: \"Work\" }\n\nexport default function WorkPage() {\n  return (\n    <>\n      <PageIntro eyebrow=\"01 / selected work\" title=\"Things I have\" accent=\"built.\">\n        Projects, experiments and product systems where engineering and design meet.\n      </PageIntro>\n\n      <section className=\"workEditorial\">\n        <div className=\"workLeadNote\">\n          <span className=\"topTape\" aria-hidden=\"true\" />\n          <strong>How I choose work</strong>\n          <p>\n            Useful problem. Clear point of view. Enough technical difficulty to learn something.\n          </p>\n          <span className=\"scribble\" aria-hidden=\"true\">\n            → make it real\n          </span>\n        </div>\n        <div className=\"fullProjectGrid\">\n          {projects.map((project, index) => (\n            <ProjectCard project={project} key={project.slug} index={index} />\n          ))}\n        </div>\n      </section>\n\n      <section className=\"pageCTA\">\n        <p>Have a difficult thing worth building?</p>\n        <Link className=\"inkButton\" href={href(\"/contact\")}>\n          Start a conversation →\n        </Link>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/about/page.tsx","target":"app/paper-portfolio/about/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\n\nimport { StudioPortrait } from \"../components/doodles\"\nimport { PageIntro } from \"../components/page-intro\"\nimport { href, profile } from \"../data/projects\"\n\nexport const metadata: Metadata = { title: \"About\" }\n\nconst TIMELINE: Array<[string, string]> = [\n  [\"Assembly / QBASIC\", \"Learning how computers think at the lowest level.\"],\n  [\"C / C++\", \"Pointers, memory, compilers and the habit of understanding systems.\"],\n  [\"Web Development\", \"Turning logic into things people can see and use.\"],\n  [\"Automation / n8n\", \"Connecting systems and removing repetitive work.\"],\n  [\"AI / ML\", \"Models, agents, multimodal products and new interaction patterns.\"],\n  [profile.studio, \"Bringing experiments together under one place for building.\"],\n]\n\nconst PRINCIPLES = [\n  \"01 — understand before decorating\",\n  \"02 — make state visible\",\n  \"03 — prototype the hard part first\",\n  \"04 — details are product decisions\",\n]\n\nexport default function AboutPage() {\n  return (\n    <>\n      <PageIntro eyebrow=\"02 / about\" title=\"A builder before a\" accent=\"bio.\">\n        I care about understanding how things work, then shaping that knowledge into software people\n        can actually use.\n      </PageIntro>\n\n      <section className=\"aboutPageGrid\">\n        <article className=\"aboutPortrait paperPanel\">\n          <span className=\"topTape\" aria-hidden=\"true\" />\n          <StudioPortrait />\n          <p className=\"handCaption\">still learning → still building</p>\n        </article>\n        <article className=\"aboutStory\">\n          <h2>I like the messy middle.</h2>\n          <p>\n            The interesting part is usually between the idea and the polished demo: choosing an\n            architecture, deleting the wrong approach, fixing the details and making the product\n            feel intentional.\n          </p>\n          <p>\n            My work moves between AI systems, developer tools, interfaces, automation and\n            experimental products. I&apos;m especially interested in products where the interaction\n            model itself can be rethought.\n          </p>\n          <div className=\"principleNotes\">\n            {PRINCIPLES.map((line) => (\n              <span key={line}>{line}</span>\n            ))}\n          </div>\n        </article>\n      </section>\n\n      <section className=\"timelineSection\">\n        <div className=\"sectionTitleLine\">\n          <h2>MY BUILDING JOURNEY</h2>\n          <span aria-hidden=\"true\">↘</span>\n        </div>\n        <div className=\"timelineList\">\n          {TIMELINE.map(([title, copy], index) => (\n            <article key={title}>\n              <span aria-hidden=\"true\">{String(index + 1).padStart(2, \"0\")}</span>\n              <h3>{title}</h3>\n              <p>{copy}</p>\n            </article>\n          ))}\n        </div>\n      </section>\n\n      <section className=\"pageCTA\">\n        <p>The longer version is in my resume.</p>\n        <Link className=\"inkButton\" href={href(\"/resume\")}>\n          Open resume →\n        </Link>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/projects/page.tsx","target":"app/paper-portfolio/projects/page.tsx","content":"import type { Metadata } from \"next\"\n\nimport { PageIntro } from \"../components/page-intro\"\nimport { ProjectCard } from \"../components/project-card\"\nimport { projects } from \"../data/projects\"\n\nexport const metadata: Metadata = { title: \"Projects\" }\n\nexport default function ProjectsPage() {\n  return (\n    <>\n      <PageIntro eyebrow=\"03 / project library\" title=\"Products, agents &\" accent=\"experiments.\">\n        A growing shelf of shipped ideas, active builds and explorations.\n      </PageIntro>\n      <section className=\"projectLibrary\">\n        {projects.map((project, index) => (\n          <ProjectCard project={project} key={project.slug} index={index} />\n        ))}\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/projects/[slug]/page.tsx","target":"app/paper-portfolio/projects/[slug]/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\nimport { notFound } from \"next/navigation\"\n\nimport { ProjectMark } from \"../../components/doodles\"\nimport { getProject, href, projects } from \"../../data/projects\"\n\ntype Params = { slug: string }\n\nexport function generateStaticParams(): Params[] {\n  return projects.map((project) => ({ slug: project.slug }))\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<Params>\n}): Promise<Metadata> {\n  const { slug } = await params\n  const project = getProject(slug)\n  return project\n    ? { title: project.title, description: project.short }\n    : { title: \"Project not found\" }\n}\n\nexport default async function ProjectPage({ params }: { params: Promise<Params> }) {\n  const { slug } = await params\n  const project = getProject(slug)\n  if (!project) notFound()\n\n  return (\n    <article className=\"caseStudy\">\n      <Link className=\"backLink\" href={href(\"/projects\")}>\n        ← all projects\n      </Link>\n\n      <header className=\"caseHero\">\n        <div>\n          <span className=\"eyebrow\">\n            {project.category} / {project.year}\n          </span>\n          <h1>{project.title}</h1>\n          <p>{project.statement}</p>\n          <div className=\"caseStatus\">\n            STATUS <b>{project.status}</b>\n          </div>\n        </div>\n        <div className=\"caseArtwork paperPanel\">\n          <span className=\"topTape\" aria-hidden=\"true\" />\n          <ProjectMark seed={project.mark} />\n          <span className=\"caseDoodle\" aria-hidden=\"true\">\n            selected work ✦\n          </span>\n        </div>\n      </header>\n\n      <section className=\"caseBody\">\n        <article>\n          <span aria-hidden=\"true\">01</span>\n          <h2>The challenge</h2>\n          <p>{project.challenge}</p>\n        </article>\n        <article>\n          <span aria-hidden=\"true\">02</span>\n          <h2>The approach</h2>\n          <p>{project.solution}</p>\n        </article>\n        <article>\n          <span aria-hidden=\"true\">03</span>\n          <h2>Built with</h2>\n          <div className=\"caseTags\">\n            {project.stack.map((item) => (\n              <b key={item}>{item}</b>\n            ))}\n          </div>\n        </article>\n      </section>\n\n      <footer className=\"caseFooter\">\n        {project.external ? (\n          <a className=\"inkButton\" href={project.external} target=\"_blank\" rel=\"noreferrer\">\n            Open repository ↗\n          </a>\n        ) : (\n          <Link className=\"inkButton\" href={href(\"/contact\")}>\n            Ask about this build →\n          </Link>\n        )}\n        <Link className=\"underLink\" href={href(\"/projects\")}>\n          Next: project library ↗\n        </Link>\n      </footer>\n    </article>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/contact/page.tsx","target":"app/paper-portfolio/contact/page.tsx","content":"import type { Metadata } from \"next\"\n\nimport { ContactForm } from \"../components/contact-form\"\nimport { PageIntro } from \"../components/page-intro\"\nimport { profile } from \"../data/projects\"\n\nexport const metadata: Metadata = { title: \"Contact\" }\n\nexport default function ContactPage() {\n  return (\n    <>\n      <PageIntro eyebrow=\"04 / contact\" title=\"Have an idea?\" accent=\"Write on the page.\">\n        For projects, collaborations, product discussions or a difficult technical problem that\n        sounds interesting.\n      </PageIntro>\n\n      <section className=\"contactPageGrid\">\n        <ContactForm />\n        <aside className=\"contactSidebar\">\n          <div className=\"stickyNote contactSticky\">\n            <span className=\"topTape\" aria-hidden=\"true\" />\n            <h2>Find me online</h2>\n            <a href={profile.siteUrl} target=\"_blank\" rel=\"noreferrer\">\n              {profile.site} ↗\n            </a>\n            <p>{profile.location}</p>\n          </div>\n          <div className=\"contactLinks paperPanel\">\n            <a href={profile.repoUrl} target=\"_blank\" rel=\"noreferrer\">\n              GitHub <span aria-hidden=\"true\">↗</span>\n            </a>\n            <a href={profile.siteUrl} target=\"_blank\" rel=\"noreferrer\">\n              Website <span aria-hidden=\"true\">↗</span>\n            </a>\n          </div>\n        </aside>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/resume/page.tsx","target":"app/paper-portfolio/resume/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\n\nimport { PrintButton } from \"../components/print-button\"\nimport { href, profile } from \"../data/projects\"\n\nexport const metadata: Metadata = { title: \"Resume\" }\n\nconst SELECTED = [\n  [\"Bloom Chat\", \"multimodal AI workspace\"],\n  [\"ArtBloom\", \"AI creative studio\"],\n  [\"Pandu Pet\", \"animated desktop companion\"],\n  [\"Bloom Browser\", \"AI-native browser concept\"],\n  [\"Bloom Code\", \"terminal coding agent\"],\n]\n\nconst CAPABILITIES = [\n  \"AI / ML systems\",\n  \"LLM & agent orchestration\",\n  \"Next.js / React\",\n  \"Python / FastAPI\",\n  \"Automation\",\n  \"Product & interaction design\",\n]\n\nexport default function ResumePage() {\n  return (\n    <article className=\"resumePage\">\n      <header>\n        <div>\n          <span className=\"eyebrow\">RESUME / 2026</span>\n          <h1>\n            {profile.name}\n            <br />\n            <mark>{profile.surname}</mark>\n          </h1>\n          <p>{profile.role}</p>\n        </div>\n        <div className=\"resumeContact\">\n          <a href={profile.siteUrl}>{profile.site}</a>\n          <a href={profile.repoUrl}>github.com/{profile.handle}</a>\n          <span>{profile.location}</span>\n        </div>\n      </header>\n\n      <section className=\"resumeColumns\">\n        <div>\n          <h2>Profile</h2>\n          <p>\n            Developer focused on AI products, agent systems, web experiences, automation and\n            experimental software.\n          </p>\n          <h2>Selected projects</h2>\n          <ul>\n            {SELECTED.map(([title, summary]) => (\n              <li key={title}>\n                <b>{title}</b> — {summary}\n              </li>\n            ))}\n          </ul>\n        </div>\n        <div>\n          <h2>Capabilities</h2>\n          <ul>\n            {CAPABILITIES.map((item) => (\n              <li key={item}>{item}</li>\n            ))}\n          </ul>\n          <h2>Current direction</h2>\n          <p>Building under {profile.studio} and contributing to open-source software.</p>\n        </div>\n      </section>\n\n      <div className=\"resumeActions\">\n        <PrintButton />\n        <Link className=\"underLink\" href={href(\"/contact\")}>\n          Contact me →\n        </Link>\n      </div>\n      <p className=\"printNote\">Tip: press Ctrl/Cmd + P to save this page as a PDF.</p>\n    </article>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/sketchbook/page.tsx","target":"app/paper-portfolio/sketchbook/page.tsx","content":"import type { Metadata } from \"next\"\n\nimport { PageIntro } from \"../components/page-intro\"\nimport { SketchBoard } from \"../components/sketch-board\"\n\nexport const metadata: Metadata = { title: \"Sketchbook\" }\n\nexport default function SketchbookPage() {\n  return (\n    <>\n      <PageIntro eyebrow=\"05 / sketchbook\" title=\"Unfinished thoughts are\" accent=\"allowed here.\">\n        Notes about products, interaction, agents and whatever I am currently trying to understand.\n        The cards below are draggable — focus one and use the arrow keys.\n      </PageIntro>\n      <SketchBoard />\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/paper-portfolio/data/projects.ts","target":"app/paper-portfolio/data/projects.ts","content":"/**\n * Content and routing for the paper-portfolio template.\n *\n * Everything a consumer needs to change lives here: the identity in\n * {@link profile} and the case studies in {@link projects}. Both hold the real\n * content of the portfolio this template was cut from — Shreyash Mishra's site\n * — deliberately, so the preview and a fresh install read the same. Swap both\n * objects for your own before you deploy it as yours.\n */\n\n/**\n * Where the template is mounted. The install writes it to\n * `app/paper-portfolio/`, so every internal link is prefixed with this. Move the\n * folder and change this one string; nothing else hard-codes a path.\n *\n * Mounting it at the root (`\"\"`) works too, but then the template owns `/work`,\n * `/about`, `/contact` and friends — decide that deliberately.\n */\nexport const BASE = \"/paper-portfolio\"\n\n/** Prefix a template-internal path with {@link BASE}. */\nexport function href(path: string): string {\n  return path === \"/\" ? BASE || \"/\" : `${BASE}${path}`\n}\n\n/**\n * Identity shown in the header, footer, hero and resume.\n *\n * These are real: a real person, a real domain, a real inbox. If you install\n * this template, replace every field before you publish — otherwise you ship\n * someone else's name and portrait.\n */\nexport const profile = {\n  name: \"Shreyash\",\n  surname: \"Mishra\",\n  role: \"AI Engineer · Developer · Builder\",\n  location: \"India · working worldwide\",\n  studio: \"Bloom Kernel Labs\",\n  site: \"shreyashmishra.in\",\n  siteUrl: \"https://shreyashmishra.in\",\n  email: \"shreyash.aiml.dev@gmail.com\",\n  handle: \"Iitian001\",\n  repoUrl: \"https://github.com/Iitian001\",\n  /**\n   * Portrait collage for the hero and the about page. Same asset contract as\n   * {@link Project.image}: it installs to `public/paper-portfolio/` and is\n   * served from the base URL, not from {@link BASE}.\n   */\n  portrait: \"/paper-portfolio/hero-collage.png\",\n} as const\n\nexport type Project = {\n  slug: string\n  title: string\n  category: string\n  year: string\n  /**\n   * Screenshot shown on the card and in the case-study header.\n   *\n   * The file lives at `assets/<name>` inside this template, the install copies\n   * it to `public/paper-portfolio/<name>`, and Next serves anything in `public`\n   * from the base URL — so the path written here is `/paper-portfolio/<name>`.\n   * It is deliberately independent of {@link BASE}: moving the routes does not\n   * move the images.\n   */\n  image: string\n  /** Seed for the generated paper glyph on cards and case studies. */\n  mark: string\n  short: string\n  statement: string\n  challenge: string\n  solution: string\n  stack: string[]\n  status: string\n  /** Optional outbound link shown as \"Open repository\" on the case study. */\n  external?: string\n}\n\nexport const projects: Project[] = [\n  {\n    slug: \"bloom-chat\",\n    title: \"Bloom Chat\",\n    category: \"Multimodal AI Workspace\",\n    year: \"2026\",\n    image: \"/paper-portfolio/bloom-chat.png\",\n    mark: \"bloom-chat\",\n    short: \"A multimodal AI workspace for research, memory, creation and focused work.\",\n    statement:\n      \"I wanted an AI workspace to feel like a place you think in—not another empty prompt box.\",\n    challenge:\n      \"Bring chat, research, image creation and persistent memory into one product without burying the user in controls.\",\n    solution:\n      \"A modular workspace with a quiet conversational core, project context, memory surfaces and deliberate tool entry points.\",\n    stack: [\"Next.js\", \"React\", \"AI orchestration\", \"Memory systems\", \"Multimodal UX\"],\n    status: \"In development\",\n  },\n  {\n    slug: \"artbloom\",\n    title: \"ArtBloom\",\n    category: \"AI Creative Studio\",\n    year: \"2026\",\n    image: \"/paper-portfolio/artbloom.png\",\n    mark: \"artbloom\",\n    short:\n      \"An expressive AI image studio designed to make generation feel like a creative process.\",\n    statement:\n      \"The goal is to make AI image generation feel less like filling a form and more like entering a studio.\",\n    challenge:\n      \"Keep an advanced image workflow understandable while supporting iteration, editing and visual exploration.\",\n    solution:\n      \"A visual-first interface built around creation states, galleries, focused controls and an art-directed identity.\",\n    stack: [\"Next.js\", \"FastAPI\", \"FLUX\", \"Image pipelines\", \"Product design\"],\n    status: \"Active build\",\n    external: \"https://github.com/Iitian001/Art-bloom\",\n  },\n  {\n    slug: \"pandu-pet\",\n    title: \"Pandu Pet\",\n    category: \"Animated Desktop Companion\",\n    year: \"2026\",\n    image: \"/paper-portfolio/pandu.png\",\n    mark: \"pandu-pet\",\n    short:\n      \"A desktop companion that moves, reacts and grows into a small presence on your screen.\",\n    statement:\n      \"Pandu started with one question: can a desktop utility have enough character that people miss it when it is gone?\",\n    challenge:\n      \"Blend animation, lightweight desktop behavior and expressive states without making the companion distracting.\",\n    solution:\n      \"A state-driven pet system with reusable animation loops, interactions and room for future adaptive behavior.\",\n    stack: [\"Godot\", \"Python tooling\", \"Animation pipelines\", \"Desktop UX\"],\n    status: \"Prototype series\",\n    external: \"https://github.com/Iitian001/Pandu_pet\",\n  },\n  {\n    slug: \"bloom-browser\",\n    title: \"Bloom Browser\",\n    category: \"AI-native Browser\",\n    year: \"2026\",\n    image: \"/paper-portfolio/browser.png\",\n    mark: \"bloom-browser\",\n    short:\n      \"A privacy-minded browser concept that treats AI as part of the browsing workflow rather than a sidebar.\",\n    statement:\n      \"The browser is where most digital work already happens. Bloom asks what it becomes when intelligence is native to that space.\",\n    challenge:\n      \"Introduce agentic capabilities without turning browsing into a noisy dashboard or compromising user control.\",\n    solution:\n      \"A browser shell centered on intentional actions, contextual assistance and visible user control over AI behavior.\",\n    stack: [\"Browser architecture\", \"React\", \"Agent systems\", \"Privacy UX\", \"Product design\"],\n    status: \"In development\",\n    external: \"https://github.com/Iitian001/Bloom_browser\",\n  },\n  {\n    slug: \"bloom-code\",\n    title: \"Bloom Code\",\n    category: \"Terminal Coding Agent\",\n    year: \"2026\",\n    image: \"/paper-portfolio/star.png\",\n    mark: \"bloom-code\",\n    short:\n      \"A terminal-first coding agent exploring reliable multi-model execution and developer workflows.\",\n    statement:\n      \"Coding agents should feel fast and decisive, but also transparent enough that developers stay in control.\",\n    challenge:\n      \"Coordinate models, tools and repository context while keeping terminal interaction predictable.\",\n    solution:\n      \"A compact agent loop with explicit tool states, provider fallback concepts and benchmark-driven iteration.\",\n    stack: [\"Node.js\", \"LLM APIs\", \"Terminal UX\", \"Agent orchestration\"],\n    status: \"Research / build\",\n  },\n  {\n    slug: \"bloom-call\",\n    title: \"Bloom Call\",\n    category: \"Voice AI Assistant\",\n    year: \"2026\",\n    image: \"/paper-portfolio/star.png\",\n    mark: \"bloom-call\",\n    short: \"A voice assistant concept for handling incoming and outgoing conversational calls.\",\n    statement:\n      \"Voice systems need to feel immediate, interruptible and human-paced rather than like a phone tree with an LLM attached.\",\n    challenge:\n      \"Join speech, reasoning and call state while keeping latency and failures understandable.\",\n    solution:\n      \"A provider-flexible voice pipeline with separated speech, reasoning and call-control layers.\",\n    stack: [\"Voice AI\", \"LLM orchestration\", \"Realtime systems\", \"FastAPI\"],\n    status: \"Exploration\",\n  },\n]\n\n/** The four shown on the home page. */\nexport const featuredProjects: Project[] = projects.slice(0, 4)\n\nexport function getProject(slug: string): Project | undefined {\n  return projects.find((project) => project.slug === slug)\n}\n","type":"registry:lib"},{"path":"app/paper-portfolio/components/site-header.tsx","target":"app/paper-portfolio/components/site-header.tsx","content":"\"use client\"\n\nimport Link from \"next/link\"\nimport { usePathname } from \"next/navigation\"\nimport { useEffect, useState } from \"react\"\n\nimport { BASE, href, profile } from \"../data/projects\"\n\nconst links: Array<[string, string]> = [\n  [\"/\", \"Home\"],\n  [\"/work\", \"Work\"],\n  [\"/about\", \"About\"],\n  [\"/projects\", \"Projects\"],\n  [\"/contact\", \"Contact\"],\n  [\"/sketchbook\", \"Sketchbook\"],\n]\n\nexport function SiteHeader() {\n  const pathname = usePathname()\n  const [open, setOpen] = useState(false)\n\n  /*\n   * The mobile drawer is absolutely positioned inside the header, so a route\n   * change leaves it hanging open over the new page. Close it whenever the path\n   * changes rather than relying on every link remembering to.\n   */\n  useEffect(() => setOpen(false), [pathname])\n\n  /* Escape closes it too — it covers the page and traps nothing. */\n  useEffect(() => {\n    if (!open) return\n    const onKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") setOpen(false)\n    }\n    window.addEventListener(\"keydown\", onKey)\n    return () => window.removeEventListener(\"keydown\", onKey)\n  }, [open])\n\n  const home = BASE || \"/\"\n  const isActive = (path: string) =>\n    path === \"/\" ? pathname === home : pathname.startsWith(href(path))\n\n  return (\n    <header className=\"siteHeader\">\n      <Link className=\"brand\" href={home} aria-label={`${profile.name} ${profile.surname} home`}>\n        <span className=\"brandStar\" aria-hidden=\"true\">\n          ✱\n        </span>\n        <span>\n          {profile.name} {profile.surname}\n        </span>\n        <span className=\"brandUnderline\" aria-hidden=\"true\" />\n      </Link>\n\n      <button\n        type=\"button\"\n        className=\"menuButton\"\n        onClick={() => setOpen((value) => !value)}\n        aria-expanded={open}\n        aria-label=\"Toggle navigation\"\n      >\n        <span aria-hidden=\"true\" />\n        <span aria-hidden=\"true\" />\n      </button>\n\n      <nav className={open ? \"mainNav open\" : \"mainNav\"} aria-label=\"Primary\">\n        {links.map(([path, label]) => (\n          <Link\n            key={path}\n            href={href(path)}\n            className={isActive(path) ? \"active\" : undefined}\n            aria-current={isActive(path) ? \"page\" : undefined}\n          >\n            {label}\n          </Link>\n        ))}\n      </nav>\n\n      <Link className=\"talkButton\" href={href(\"/contact\")}>\n        <span className=\"talkBubble\" aria-hidden=\"true\">\n          ◌\n        </span>{\" \"}\n        Let&apos;s Talk! <b aria-hidden=\"true\">›</b>\n      </Link>\n    </header>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/site-footer.tsx","target":"app/paper-portfolio/components/site-footer.tsx","content":"import Link from \"next/link\"\n\nimport { href, profile } from \"../data/projects\"\n\nexport function SiteFooter() {\n  return (\n    <footer className=\"siteFooter\">\n      <div>\n        <span className=\"footerStar\" aria-hidden=\"true\">\n          ✱\n        </span>\n        <strong>\n          {profile.name} {profile.surname}\n        </strong>\n        <p>{profile.role}</p>\n      </div>\n      <div className=\"footerLinks\">\n        <Link href={href(\"/projects\")}>Projects</Link>\n        <Link href={href(\"/contact\")}>Contact</Link>\n        <a href={profile.repoUrl} target=\"_blank\" rel=\"noreferrer\">\n          GitHub ↗\n        </a>\n        <a href={profile.siteUrl} target=\"_blank\" rel=\"noreferrer\">\n          {profile.site} ↗\n        </a>\n      </div>\n    </footer>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/page-intro.tsx","target":"app/paper-portfolio/components/page-intro.tsx","content":"export type PageIntroProps = {\n  eyebrow: string\n  title: string\n  /** Highlighted tail of the heading. */\n  accent?: string\n  children: React.ReactNode\n}\n\nexport function PageIntro({ eyebrow, title, accent, children }: PageIntroProps) {\n  return (\n    <section className=\"pageIntro\">\n      <span className=\"eyebrow\">{eyebrow}</span>\n      <h1>\n        {title} {accent ? <mark>{accent}</mark> : null}\n      </h1>\n      <p>{children}</p>\n      <span className=\"introDoodle\" aria-hidden=\"true\">\n        ↘\n      </span>\n    </section>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/project-card.tsx","target":"app/paper-portfolio/components/project-card.tsx","content":"import Link from \"next/link\"\n\nimport { href, type Project } from \"../data/projects\"\n\nimport { ProjectMark } from \"./doodles\"\n\nexport type ProjectCardProps = {\n  project: Project\n  /** The tighter variant used in the home-page grid. */\n  compact?: boolean\n  /** Position in the list. Only used to alternate the paper tilt. */\n  index?: number\n}\n\nexport function ProjectCard({ project, compact = false, index = 0 }: ProjectCardProps) {\n  return (\n    <Link\n      href={href(`/projects/${project.slug}`)}\n      className={compact ? \"projectCard compact\" : \"projectCard\"}\n      /*\n       * A custom property, so the hover rule in CSS can flatten the tilt without\n       * needing to know which way this particular card leans.\n       */\n      style={{ \"--tilt\": `${index % 2 ? 0.6 : -0.5}deg` } as React.CSSProperties}\n    >\n      <ProjectMark seed={project.mark} />\n      <div className=\"projectCardCopy\">\n        <div className=\"projectMeta\">\n          {project.category} · {project.year}\n        </div>\n        <h3>{project.title}</h3>\n        <p>{project.short}</p>\n      </div>\n      <span className=\"projectArrow\" aria-hidden=\"true\">\n        →\n      </span>\n    </Link>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/hero-motion.tsx","target":"app/paper-portfolio/components/hero-motion.tsx","content":"\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\n/**\n * Parallaxes the hero artwork against the pointer.\n *\n * Two things worth keeping if you edit this: the listener is passive (it never\n * calls preventDefault, so it must not block scrolling), and it writes CSS custom\n * properties instead of `transform` directly — the transform stays in the\n * stylesheet where the transition lives.\n */\nexport function HeroMotion({ children }: { children: React.ReactNode }) {\n  const ref = useRef<HTMLDivElement>(null)\n\n  useEffect(() => {\n    const node = ref.current\n    if (!node) return\n\n    /*\n     * Skip the effect entirely for anyone who asked for less motion, and on the\n     * mobile layout where the artwork is in the flow rather than floating.\n     */\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const narrow = window.matchMedia(\"(max-width: 900px)\")\n\n    const move = (event: PointerEvent) => {\n      if (reduced.matches || narrow.matches) return\n      const x = (event.clientX / window.innerWidth - 0.5) * 8\n      const y = (event.clientY / window.innerHeight - 0.5) * 6\n      node.style.setProperty(\"--hero-x\", `${x}px`)\n      node.style.setProperty(\"--hero-y\", `${y}px`)\n    }\n\n    const reset = () => {\n      node.style.removeProperty(\"--hero-x\")\n      node.style.removeProperty(\"--hero-y\")\n    }\n\n    window.addEventListener(\"pointermove\", move, { passive: true })\n    reduced.addEventListener(\"change\", reset)\n    narrow.addEventListener(\"change\", reset)\n    return () => {\n      window.removeEventListener(\"pointermove\", move)\n      reduced.removeEventListener(\"change\", reset)\n      narrow.removeEventListener(\"change\", reset)\n    }\n  }, [])\n\n  return (\n    <div ref={ref} className=\"heroMotion\">\n      {children}\n    </div>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/sketch-board.tsx","target":"app/paper-portfolio/components/sketch-board.tsx","content":"\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\ntype Tone = \"yellow\" | \"cream\" | \"orange\"\n\ntype Note = {\n  id: number\n  title: string\n  body: string\n  /** Position as a percentage of the board, so it survives a resize. */\n  x: number\n  y: number\n  tone: Tone\n}\n\nconst INITIAL_NOTES: Note[] = [\n  {\n    id: 1,\n    title: \"Interface note\",\n    body: \"A control should appear when it has something to do — not sit there being permanently available.\",\n    x: 6,\n    y: 8,\n    tone: \"yellow\",\n  },\n  {\n    id: 2,\n    title: \"Tiny rule\",\n    body: \"If a screen looks impressive before it feels useful, keep editing.\",\n    x: 43,\n    y: 15,\n    tone: \"cream\",\n  },\n  {\n    id: 3,\n    title: \"State, plainly\",\n    body: \"Show what the system is doing: waiting → working → checking → done. Never just a spinner.\",\n    x: 66,\n    y: 46,\n    tone: \"orange\",\n  },\n  {\n    id: 4,\n    title: \"Build log\",\n    body: \"Handmade does not mean decorative. It means every decision had a reason behind it.\",\n    x: 18,\n    y: 54,\n    tone: \"cream\",\n  },\n]\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)\n\n/** How far one arrow-key press moves a note, in percent of the board. */\nconst NUDGE = 2\n\n/**\n * The draggable note board on /sketchbook.\n *\n * Positions are percentages so the layout survives a resize, but the *limits*\n * are measured from the real elements rather than hard-coded — a percentage\n * ceiling like \"78%\" lets a 285px note hang off a 1200px board and get clipped.\n * Arrow keys move a focused note, because a mouse-only feature is no feature for\n * anyone navigating by keyboard.\n */\nexport function SketchBoard() {\n  const boardRef = useRef<HTMLDivElement>(null)\n  const noteRefs = useRef(new Map<number, HTMLElement>())\n  const dragging = useRef<{ id: number; offsetX: number; offsetY: number } | null>(null)\n  const [notes, setNotes] = useState<Note[]>(INITIAL_NOTES)\n\n  /** The largest left/top percentage that still keeps the note fully on the board. */\n  const limitsFor = useCallback((id: number) => {\n    const board = boardRef.current\n    const note = noteRefs.current.get(id)\n    if (!board || !note) return { maxX: 100, maxY: 100 }\n    const { width, height } = board.getBoundingClientRect()\n    return {\n      maxX: width > 0 ? Math.max(0, ((width - note.offsetWidth) / width) * 100) : 0,\n      maxY: height > 0 ? Math.max(0, ((height - note.offsetHeight) / height) * 100) : 0,\n    }\n  }, [])\n\n  const settle = useCallback(() => {\n    setNotes((items) =>\n      items.map((item) => {\n        const { maxX, maxY } = limitsFor(item.id)\n        return { ...item, x: clamp(item.x, 0, maxX), y: clamp(item.y, 0, maxY) }\n      }),\n    )\n  }, [limitsFor])\n\n  /* The initial positions are tuned for a wide board; pull them in on narrow ones. */\n  useEffect(() => {\n    settle()\n    window.addEventListener(\"resize\", settle)\n    return () => window.removeEventListener(\"resize\", settle)\n  }, [settle])\n\n  function move(id: number, x: number, y: number) {\n    const { maxX, maxY } = limitsFor(id)\n    setNotes((items) =>\n      items.map((item) =>\n        item.id === id ? { ...item, x: clamp(x, 0, maxX), y: clamp(y, 0, maxY) } : item,\n      ),\n    )\n  }\n\n  function onPointerDown(event: React.PointerEvent<HTMLElement>, note: Note) {\n    const board = boardRef.current\n    if (!board || event.button !== 0) return\n    const rect = board.getBoundingClientRect()\n    dragging.current = {\n      id: note.id,\n      offsetX: event.clientX - (rect.left + (note.x / 100) * rect.width),\n      offsetY: event.clientY - (rect.top + (note.y / 100) * rect.height),\n    }\n    event.currentTarget.setPointerCapture(event.pointerId)\n  }\n\n  function onPointerMove(event: React.PointerEvent<HTMLDivElement>) {\n    const drag = dragging.current\n    const board = boardRef.current\n    if (!drag || !board) return\n    const rect = board.getBoundingClientRect()\n    if (rect.width === 0 || rect.height === 0) return\n    move(\n      drag.id,\n      ((event.clientX - rect.left - drag.offsetX) / rect.width) * 100,\n      ((event.clientY - rect.top - drag.offsetY) / rect.height) * 100,\n    )\n  }\n\n  function onKeyDown(event: React.KeyboardEvent<HTMLElement>, note: Note) {\n    const step: Record<string, [number, number]> = {\n      ArrowLeft: [-NUDGE, 0],\n      ArrowRight: [NUDGE, 0],\n      ArrowUp: [0, -NUDGE],\n      ArrowDown: [0, NUDGE],\n    }\n    const delta = step[event.key]\n    if (!delta) return\n    event.preventDefault()\n    move(note.id, note.x + delta[0], note.y + delta[1])\n  }\n\n  return (\n    <div\n      ref={boardRef}\n      className=\"sketchBoard\"\n      onPointerMove={onPointerMove}\n      onPointerUp={() => {\n        dragging.current = null\n      }}\n      onPointerCancel={() => {\n        dragging.current = null\n      }}\n    >\n      <span className=\"boardDoodle boardDoodleOne\" aria-hidden=\"true\">\n        ✦\n      </span>\n      <span className=\"boardDoodle boardDoodleTwo\" aria-hidden=\"true\">\n        ↗\n      </span>\n      <span className=\"boardDoodle boardDoodleThree\" aria-hidden=\"true\">\n        ( keep making )\n      </span>\n\n      {notes.map((note) => (\n        <article\n          key={note.id}\n          ref={(node) => {\n            if (node) noteRefs.current.set(note.id, node)\n            else noteRefs.current.delete(note.id)\n          }}\n          className={`draggableNote ${note.tone}`}\n          style={{ left: `${note.x}%`, top: `${note.y}%` }}\n          onPointerDown={(event) => onPointerDown(event, note)}\n          onKeyDown={(event) => onKeyDown(event, note)}\n          tabIndex={0}\n          role=\"group\"\n          aria-roledescription=\"Movable note\"\n          aria-label={`${note.title}. Use the arrow keys to move it.`}\n        >\n          <span className=\"noteTape\" aria-hidden=\"true\" />\n          <small aria-hidden=\"true\">drag me</small>\n          <h3>{note.title}</h3>\n          <p>{note.body}</p>\n        </article>\n      ))}\n    </div>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/contact-form.tsx","target":"app/paper-portfolio/components/contact-form.tsx","content":"\"use client\"\n\nimport { useState } from \"react\"\n\nimport { profile } from \"../data/projects\"\n\ntype Status = \"idle\" | \"shared\" | \"copied\" | \"mail\" | \"failed\"\n\nconst HINTS: Record<Exclude<Status, \"idle\">, string> = {\n  shared: \"Shared. Thanks — a reply usually comes within a day or two.\",\n  copied: `Copied to your clipboard. Paste it into an email to ${profile.email}.`,\n  mail: \"Your email app should be opening with the message ready to send.\",\n  failed: `Could not copy automatically. Email ${profile.email} and paste the message yourself.`,\n}\n\n/**\n * A contact form with no backend, on purpose.\n *\n * A static template cannot host a mail server, and pretending otherwise means a\n * form that silently drops messages. So this hands the message to something that\n * *can* send it: the native share sheet, the clipboard, or a `mailto:` link, in\n * that order. Wire it to an action or an API route if you want real submissions.\n */\nexport function ContactForm() {\n  const [status, setStatus] = useState<Status>(\"idle\")\n  const [copied, setCopied] = useState(false)\n\n  async function submit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    const data = new FormData(event.currentTarget)\n    const name = String(data.get(\"name\") || \"Website visitor\")\n    const email = String(data.get(\"email\") || \"\")\n    const message = String(data.get(\"message\") || \"\")\n    const subject = `Enquiry for ${profile.studio}`\n    const text = `${subject}\\n\\n${message}\\n\\nFrom: ${name}\\nEmail: ${email}`\n\n    if (typeof navigator !== \"undefined\" && navigator.share) {\n      try {\n        await navigator.share({ title: subject, text })\n        setStatus(\"shared\")\n        return\n      } catch (error) {\n        /* The user closing the sheet is not a failure — leave the form alone. */\n        if (error instanceof Error && error.name === \"AbortError\") return\n      }\n    }\n\n    try {\n      await navigator.clipboard.writeText(text)\n      setStatus(\"copied\")\n      return\n    } catch {\n      /*\n       * Clipboard access fails on insecure origins and whenever permission is\n       * refused. Fall through rather than swallowing the message.\n       */\n    }\n\n    try {\n      const url = `mailto:${profile.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(text)}`\n      window.location.href = url\n      setStatus(\"mail\")\n    } catch {\n      setStatus(\"failed\")\n    }\n  }\n\n  async function copyEmail() {\n    try {\n      await navigator.clipboard.writeText(profile.email)\n      setCopied(true)\n      window.setTimeout(() => setCopied(false), 1800)\n    } catch {\n      setStatus(\"failed\")\n    }\n  }\n\n  return (\n    <div className=\"contactFormWrap\">\n      <form className=\"contactForm\" onSubmit={submit}>\n        <label>\n          <span>Your name</span>\n          <input name=\"name\" required placeholder=\"Write it here...\" autoComplete=\"name\" />\n        </label>\n        <label>\n          <span>Your email</span>\n          <input\n            name=\"email\"\n            type=\"email\"\n            required\n            placeholder=\"you@example.com\"\n            autoComplete=\"email\"\n          />\n        </label>\n        <label className=\"fullField\">\n          <span>What are we building?</span>\n          <textarea\n            name=\"message\"\n            required\n            rows={7}\n            placeholder=\"Tell me about the idea, the problem, or the collaboration...\"\n          />\n        </label>\n        <button className=\"inkButton\" type=\"submit\">\n          Share / copy message <span aria-hidden=\"true\">→</span>\n        </button>\n        <button className=\"paperButton\" type=\"button\" onClick={copyEmail}>\n          {copied ? \"Email copied ✓\" : \"Copy email address\"}\n        </button>\n      </form>\n      <p className=\"formHint\" role=\"status\" aria-live=\"polite\">\n        {status === \"idle\" ? \"\" : HINTS[status]}\n      </p>\n    </div>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/print-button.tsx","target":"app/paper-portfolio/components/print-button.tsx","content":"\"use client\"\n\nexport function PrintButton() {\n  return (\n    <button type=\"button\" className=\"inkButton printButton\" onClick={() => window.print()}>\n      Print / Save as PDF <span aria-hidden=\"true\">→</span>\n    </button>\n  )\n}\n","type":"registry:component"},{"path":"app/paper-portfolio/components/doodles.tsx","target":"app/paper-portfolio/components/doodles.tsx","content":"/**\n * The photographic slots in this template: the portrait collage and the project\n * marks.\n *\n * These were briefly redrawn as generated SVG. They are bitmaps again, because a\n * photograph of a person and five product logos are not derivable from a hash.\n * The files live in `assets/` beside this template; the install copies them to\n * `public/paper-portfolio/`, and Next serves `public` from the base URL — so the\n * URLs are `/paper-portfolio/<file>` and, unlike the routes, they do not move\n * when `BASE` changes.\n *\n * Plain `<img>`, not `next/image`, on purpose. A template is installed into an\n * app whose `next.config` it does not control, and `next/image` has two\n * configuration dependencies this folder cannot satisfy from here:\n *\n *   - `output: \"export\"` cannot use the default loader at all. It needs\n *     `images.loader: \"custom\"` plus a `loaderFile`, so an export build fails\n *     rather than degrades.\n *   - `images.localPatterns`, once a consumer sets it for their own images,\n *     turns every path outside those patterns into a 400. A consumer who has\n *     never touched it is fine; one who has must remember to add this folder.\n *\n * What is given up is small. Five of the six files are 52–73px wide and render\n * at 62–230px, so the optimizer has nothing to shrink and would only upscale\n * them into a larger, softer variant. The collage is the real cost — 525,048\n * bytes for a 565px box — but the fix there is re-encoding the file, not making\n * a consumer's build satisfy `next/image`.\n *\n * The stylesheet's `.paperSite img` rule already supplies `display: block` and\n * `max-width: 100%`. The inline styles below add only the box-filling geometry,\n * which is inline because the rules for these three slots still select `svg`.\n * Once `paper-portfolio.css` selects `img` too, the `style` props can go.\n */\n\nimport { profile, projects } from \"../data/projects\"\n\n/**\n * Intrinsic pixels of `hero-collage.png`, read from its PNG header. Both slots\n * share that one file, so there is one pair rather than a table.\n *\n * The marks get no such constant. Their box is a fixed CSS square and they are\n * cropped to fill it, so their five different shapes (52×58 up to 73×75) cannot\n * shift the layout and the browser never needs the ratio to reserve space.\n */\nconst COLLAGE = { width: 525, height: 500 } as const\n\n/**\n * Shown when a `seed` matches no project. `star.png` is the spare mark the data\n * already gives to projects with no logo of their own, so an unknown seed lands\n * on something drawn for this purpose instead of a broken image.\n */\nconst SPARE_MARK = \"/paper-portfolio/star.png\"\n\n/** One description, two slots — it is the same photograph on both pages. */\nconst COLLAGE_ALT = `Portrait collage of ${profile.name} ${profile.surname}`\n\n/**\n * The hero artwork: the cut-out collage, cropped to fill its box.\n *\n * `.heroArtwork` is 565×520 on desktop, so `cover` trims a little off the sides\n * of the 525×500 file. At 900px and under that box goes auto-height, the\n * percentage height resolves against nothing, and the intrinsic ratio takes\n * over.\n *\n * Eager and high priority: it is above the fold and the likely LCP element.\n */\nexport function TornCollage() {\n  return (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      src={profile.portrait}\n      alt={COLLAGE_ALT}\n      width={COLLAGE.width}\n      height={COLLAGE.height}\n      loading=\"eager\"\n      fetchPriority=\"high\"\n      decoding=\"async\"\n      style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n    />\n  )\n}\n\n/**\n * The about-page portrait. Same file as the hero, fitted to the panel rather\n * than cropped: the panel has no fixed height, so width drives and height\n * follows.\n *\n * `height: auto` is what keeps it from squashing, and it always matters here —\n * the panel sits in a 470px grid column on desktop and is capped at 520px below\n * that, so the box is never as wide as the file's 525px.\n */\nexport function StudioPortrait() {\n  return (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      src={profile.portrait}\n      alt={COLLAGE_ALT}\n      width={COLLAGE.width}\n      height={COLLAGE.height}\n      loading=\"lazy\"\n      decoding=\"async\"\n      style={{ width: \"100%\", height: \"auto\" }}\n    />\n  )\n}\n\nexport type ProjectMarkProps = {\n  /** A project's `mark`, looked up in `projects` to find that project's logo. */\n  seed: string\n}\n\n/**\n * The square logo mark on project cards and case-study headers.\n *\n * It takes a `mark` and looks the project up, rather than taking the image path,\n * because the case-study page has only the `mark` to hand and this component\n * cannot reach into that file. `mark` has no other purpose now that the art is\n * not generated from it — passing the project straight in and deleting the field\n * would be the better shape.\n *\n * Decorative: every mark sits next to the project title it belongs to, so an\n * alt text would only repeat the adjacent heading. Hence `alt=\"\"`.\n */\nexport function ProjectMark({ seed }: ProjectMarkProps) {\n  const src = projects.find((project) => project.mark === seed)?.image ?? SPARE_MARK\n\n  return (\n    <span className=\"projectMark\" aria-hidden=\"true\">\n      {/* eslint-disable-next-line @next/next/no-img-element */}\n      <img\n        src={src}\n        alt=\"\"\n        loading=\"lazy\"\n        decoding=\"async\"\n        style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n      />\n    </span>\n  )\n}\n","type":"registry:component"}],"assets":[{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/hero-collage.png","target":"public/paper-portfolio/hero-collage.png","bytes":525048},{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/artbloom.png","target":"public/paper-portfolio/artbloom.png","bytes":10845},{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/bloom-chat.png","target":"public/paper-portfolio/bloom-chat.png","bytes":11996},{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/browser.png","target":"public/paper-portfolio/browser.png","bytes":11043},{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/pandu.png","target":"public/paper-portfolio/pandu.png","bytes":11057},{"url":"https://ui.artbloom.tech/r/asset/templates/paper-portfolio/assets/star.png","target":"public/paper-portfolio/star.png","bytes":6566}],"meta":{"kind":"templates","categories":["portfolio","personal","resume"],"docs":"https://ui.artbloom.tech/artbloom/templates/paper-portfolio"}}