From 923f5304269dda4a3e128cf18714528b7a1336aa Mon Sep 17 00:00:00 2001 From: LeOu Date: Mon, 3 Aug 2026 14:06:58 -0400 Subject: [PATCH] Refocus Home on the particle stage --- app/components/SiteShell.tsx | 18 +- .../ParticleBackground.module.css | 8 +- app/home.module.css | 399 ++++++++++++++++++ app/page.tsx | 118 +++++- tests/rendered-html.test.mjs | 29 +- .../03-home-particle-system.md | 16 + 6 files changed, 560 insertions(+), 28 deletions(-) create mode 100644 app/home.module.css diff --git a/app/components/SiteShell.tsx b/app/components/SiteShell.tsx index dd90b2f..f877bbc 100644 --- a/app/components/SiteShell.tsx +++ b/app/components/SiteShell.tsx @@ -6,17 +6,23 @@ export function SiteShell({ active, children, background, + frameClassName, + showProfile = true, + skipHref = "#main-content", variant = "default", }: { active?: ActivePage; children: ReactNode; background?: ReactNode; + frameClassName?: string; + showProfile?: boolean; + skipHref?: `#${string}`; variant?: "default" | "particle"; }) { return (
{background} - + Skip to main content
@@ -39,8 +45,11 @@ export function SiteShell({
-
- + ) : null}
{children} diff --git a/app/components/particle-background/ParticleBackground.module.css b/app/components/particle-background/ParticleBackground.module.css index 6f987e2..558f846 100644 --- a/app/components/particle-background/ParticleBackground.module.css +++ b/app/components/particle-background/ParticleBackground.module.css @@ -1,7 +1,11 @@ .root { - position: fixed; + position: absolute; z-index: 0; - inset: 0; + top: 0; + right: 0; + left: 0; + height: 100vh; + height: 100svh; overflow: hidden; pointer-events: none; } diff --git a/app/home.module.css b/app/home.module.css new file mode 100644 index 0000000..04f87f7 --- /dev/null +++ b/app/home.module.css @@ -0,0 +1,399 @@ +.homeFrame { + display: block; + width: 100%; + margin: 0; + padding: 0; +} + +.particleStage { + position: relative; + display: flex; + z-index: 1; + align-items: flex-end; + justify-content: center; + min-height: calc(100vh - 68px); + min-height: calc(100svh - 68px); + padding: 0 24px 30px; +} + +.scrollCue { + display: grid; + min-width: 56px; + min-height: 64px; + place-items: center; + color: var(--faint); + font-family: var(--sans); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.11em; + line-height: 1; + text-transform: uppercase; + transition: color 160ms var(--ease-out), transform 160ms var(--ease-out); +} + +.scrollCue:active { + transform: scale(0.97); +} + +.scrollLine { + width: 1px; + height: 28px; + background: linear-gradient(to bottom, var(--accent), rgba(195, 95, 61, 0.12)); +} + +.profileSection { + position: relative; + z-index: 2; + border-top: 1px solid var(--rule-strong); + background: linear-gradient(180deg, var(--paper-raised) 0%, var(--paper) 46%); + box-shadow: 0 -24px 60px rgba(52, 39, 29, 0.035); +} + +.profileInner { + width: min(1180px, calc(100% - 48px)); + margin: 0 auto; + padding: 112px 0 128px; +} + +.identity { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + align-items: center; + padding-bottom: 88px; + border-bottom: 1px solid var(--rule-strong); + gap: clamp(48px, 7vw, 112px); +} + +.portrait { + position: relative; + width: 176px; + height: 176px; + overflow: hidden; + border: 1px solid var(--rule-strong); + border-radius: 50%; + background: var(--paper-raised); + box-shadow: 0 18px 54px rgba(70, 49, 36, 0.12); +} + +.portrait::after { + position: absolute; + inset: 7px; + border: 1px solid rgba(255, 253, 248, 0.76); + border-radius: inherit; + content: ""; + pointer-events: none; +} + +.portrait picture, +.portrait img { + display: block; + width: 100%; + height: 100%; +} + +.portrait img { + object-fit: cover; + object-position: center 22%; +} + +.identityCopy { + min-width: 0; +} + +.sectionLabel { + margin: 0; + color: var(--accent-dark); + font-family: var(--sans); + font-size: 0.7rem; + font-weight: 680; + letter-spacing: 0.11em; + line-height: 1.4; + text-transform: uppercase; +} + +.identity h1 { + max-width: 920px; + margin: 12px 0 0; + color: var(--ink-strong); + font-family: var(--body-serif); + font-size: clamp(4rem, 8vw, 7.2rem); + font-weight: 520; + letter-spacing: -0.058em; + line-height: 0.9; + text-wrap: balance; +} + +.credentials { + display: grid; + max-width: 560px; + margin: 34px 0 0; + padding: 0; + color: var(--muted); + font-family: var(--body-serif); + font-size: 1.05rem; + line-height: 1.52; + list-style: none; + gap: 2px; +} + +.biography, +.contactSection { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + gap: clamp(48px, 7vw, 112px); +} + +.biography { + padding: 82px 0 88px; +} + +.biographyCopy { + max-width: 760px; +} + +.biographyCopy p { + margin: 0; + color: var(--ink); + font-family: var(--body-serif); + font-size: clamp(1.22rem, 2vw, 1.5rem); + letter-spacing: -0.015em; + line-height: 1.58; +} + +.biographyCopy p:last-child { + margin-top: 26px; + color: var(--accent-dark); + font-family: var(--sans); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.025em; + line-height: 1.45; +} + +.contactSection { + align-items: start; +} + +.contactStrip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; + padding: 0; + border-top: 1px solid var(--rule-strong); + border-bottom: 1px solid var(--rule-strong); + list-style: none; +} + +.contactStrip li { + display: grid; + align-content: center; + min-width: 0; + min-height: 112px; + padding: 20px 22px; + color: var(--ink); + font-family: var(--body-serif); + font-size: 0.96rem; + line-height: 1.35; + gap: 10px; +} + +.contactStrip li + li { + border-left: 1px solid var(--rule); +} + +.contactLabel { + display: flex; + align-items: center; + color: var(--faint); + font-family: var(--sans); + font-size: 0.66rem; + font-weight: 650; + letter-spacing: 0.08em; + line-height: 1; + text-transform: uppercase; + gap: 8px; +} + +.contactLabel > span { + min-width: 18px; + color: var(--accent); + font-size: 0.7rem; + text-align: center; +} + +.contactStrip a { + display: inline-flex; + align-items: center; + min-height: 44px; + width: fit-content; + max-width: 100%; + overflow-wrap: anywhere; + text-decoration-color: rgba(195, 95, 61, 0.36); + text-decoration-line: underline; + text-underline-offset: 4px; + transition: color 160ms var(--ease-out), transform 120ms var(--ease-out); +} + +.contactStrip a:active { + transform: scale(0.97); +} + +@media (max-width: 930px) { + .profileInner { + padding-top: 94px; + } + + .identity, + .biography, + .contactSection { + grid-template-columns: 152px minmax(0, 1fr); + gap: 48px; + } + + .portrait { + width: 152px; + height: 152px; + } + + .contactStrip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .contactStrip li:nth-child(3) { + border-top: 1px solid var(--rule); + border-left: 0; + } + + .contactStrip li:nth-child(4) { + border-top: 1px solid var(--rule); + } +} + +@media (max-width: 720px) { + .particleStage { + min-height: calc(100vh - 122px); + min-height: max(22rem, calc(100svh - 122px)); + padding-bottom: 20px; + } + + .scrollCue { + min-height: 58px; + } + + .profileInner { + width: min(100% - 32px, 640px); + padding: 76px 0 92px; + } + + .identity { + grid-template-columns: 112px minmax(0, 1fr); + padding-bottom: 56px; + gap: 28px; + } + + .portrait { + width: 112px; + height: 112px; + } + + .identity h1 { + margin-top: 8px; + font-size: clamp(3rem, 12vw, 4.6rem); + line-height: 0.93; + } + + .credentials { + margin-top: 24px; + font-size: 0.9rem; + } + + .biography, + .contactSection { + grid-template-columns: 1fr; + gap: 20px; + } + + .biography { + padding: 58px 0 64px; + } + + .biographyCopy p { + font-size: 1.17rem; + line-height: 1.6; + } +} + +@media (max-width: 470px) { + .particleStage { + min-height: calc(100vh - 170px); + min-height: max(22rem, calc(100svh - 170px)); + } + + .identity { + grid-template-columns: 1fr; + gap: 30px; + } + + .portrait { + width: 104px; + height: 104px; + } + + .identity h1 { + max-width: 330px; + font-size: clamp(3.3rem, 17vw, 4.4rem); + } + + .contactStrip { + grid-template-columns: 1fr; + } + + .contactStrip li { + min-height: 96px; + padding-inline: 16px; + } + + .contactStrip li + li, + .contactStrip li:nth-child(3) { + border-top: 1px solid var(--rule); + border-left: 0; + } +} + +@media (hover: hover) and (pointer: fine) { + .scrollCue:hover, + .contactStrip a:hover { + color: var(--accent-dark); + } + + .scrollCue:hover { + transform: translateY(2px); + } +} + +@media (prefers-reduced-motion: reduce) { + .scrollCue, + .contactStrip a { + transition-duration: 120ms; + } + + .scrollCue:hover, + .scrollCue:active, + .contactStrip a:active { + transform: none; + } +} + +@media (prefers-contrast: more) { + .profileSection { + border-color: var(--rule-strong); + background: var(--paper-raised); + } + + .contactStrip, + .contactStrip li + li, + .contactStrip li:nth-child(3), + .contactStrip li:nth-child(4) { + border-color: var(--rule-strong); + } +} diff --git a/app/page.tsx b/app/page.tsx index 00c914d..ff8c4cb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,6 +6,7 @@ import { DEFAULT_DESCRIPTION, HOME_TITLE, } from "./lib/content/site"; +import styles from "./home.module.css"; export const metadata: Metadata = createPageMetadata({ title: HOME_TITLE, @@ -18,27 +19,108 @@ export default function Home() { return ( } + frameClassName={styles.homeFrame} + showProfile={false} + skipHref="#home-profile" + variant="particle" > -
-
-

Quis ego sum?

-

- Theodore Ouyang -

-

Identity emerges from possibility.

-
+ + +
+
+
+
+ + + + Illustrated portrait of Theodore Ouyang + +
+ +
+

Profile

+

Theodore Ouyang

+
    +
  • Exploring practical AI use cases
  • +
  • Duke B.S. & M.Eng.
  • +
  • Sequoia Scholar, Cohort 8
  • +
+
+
+ +
+

About

+
+

+ Theodore Ouyang holds a Bachelor of Science and a Master of + Engineering from Duke University. His interests span artificial + intelligence, strategy, and finance, with a particular curiosity + about how emerging technologies become genuinely useful in + everyday life. +

+

He is a Sequoia Scholar in Cohort 8.

+
+
-
-

- Theodore Ouyang holds a Bachelor of Science and a Master of - Engineering from Duke University. His interests span artificial - intelligence, strategy, and finance, with a particular curiosity - about how emerging technologies become genuinely useful in everyday - life. -

-

He is a Sequoia Scholar in Cohort 8.

+
+

Coordinates

+
    +
  • + + + Location + + Beijing | Boston +
  • +
  • + + + Education + + Duke University +
  • +
  • + + + Email + + 10@alumni.duke.edu +
  • +
  • + + + Online + + + GitHub + +
  • +
+
diff --git a/tests/rendered-html.test.mjs b/tests/rendered-html.test.mjs index 13bac4b..2d96f9a 100644 --- a/tests/rendered-html.test.mjs +++ b/tests/rendered-html.test.mjs @@ -67,6 +67,10 @@ const PROTECTED_SOURCE_HASHES = new Map([ "app/education/page.tsx", "7c2d6dee0679d4737a23ece91ef14c0c903a237188619c680bece518b9945e99", ], + [ + "app/now/page.tsx", + "d7d92116e56afc9f708fdac57030e91eb15d94769d414a2f9d29fbdcba842f8f", + ], [ "app/past-experience/page.tsx", "c22ffecb129567be473ce2230103fd80d954faeaa6ebae171c9418085e6769e9", @@ -309,7 +313,7 @@ test("every route has canonical metadata and the same four-item navigation", asy } }); -test("Home keeps semantic identity, approved copy, and a decorative resilient visual layer", async () => { +test("Home gives the particle stage visual priority and moves identity details below it", async () => { const html = await routeHtml("/"); const body = stripMarkup(html); const title = stripMarkup(html.match(/([\s\S]*?)<\/title>/iu)?.[1] ?? ""); @@ -318,21 +322,38 @@ test("Home keeps semantic identity, approved copy, and a decorative resilient vi metaContent(html, "name", "description"), "Theodore Ouyang is a Duke University graduate and Sequoia Scholar in Cohort 8, exploring how artificial intelligence can become useful in everyday life.", ); - assert.match(body, /Quis ego sum\?/u); - assert.match(body, /Identity emerges from possibility\./u); + assert.doesNotMatch(body, /Quis ego sum\?/u); + assert.doesNotMatch(body, /Identity emerges from possibility\./u); + assert.match(body, /Exploring practical AI use cases/u); + assert.match(body, /Duke B\.S\. & M\.Eng\./u); assert.match(body, /He is a Sequoia Scholar in Cohort 8\./u); assert.match(body, /genuinely useful in everyday life/u); + assert.match(body, /Beijing \| Boston/u); + assert.match(body, /10@alumni\.duke\.edu/u); const heading = [...html.matchAll(/<h1\b([^>]*)>([\s\S]*?)<\/h1>/giu)].find( (match) => attribute(match[1], "id") === "home-heading", ); assert.equal(stripMarkup(heading?.[2] ?? ""), "Theodore Ouyang"); + assert.equal(pairedElementsWithClass(html, "aside", "profile-sidebar").length, 0); + assert.match(html, /id="home-profile"/u); const canvas = openingTags(html, "canvas"); assert.equal(canvas.length, 1); assert.equal(attribute(canvas[0], "aria-hidden"), "true"); assert.match(html, /data-state="checking"/u, "server-rendered fallback state is missing"); }); +test("protected routes retain the shared profile sidebar", async () => { + for (const route of ["/education/", "/past-experience/", "/now/"]) { + const html = await routeHtml(route); + assert.equal( + pairedElementsWithClass(html, "aside", "profile-sidebar").length, + 1, + `${route} profile sidebar changed`, + ); + } +}); + test("Current Chapter is concise and limited to practical everyday AI use cases", async () => { const html = await routeHtml("/now/"); const body = stripMarkup(html); @@ -402,7 +423,7 @@ test("robots, sitemap, and HTML share the exact route manifest", async () => { assert.equal(new Set(routes).size, routes.length, "Sitemap routes must be unique"); }); -test("Education and Past Experience protected sources remain byte-for-byte equivalent", async () => { +test("Education, Past Experience, and Current Chapter protected sources remain byte-for-byte equivalent", async () => { for (const [relative, expected] of PROTECTED_SOURCE_HASHES) { const source = path.join(ROOT, ...relative.split("/")); assert.equal(sha256(normalizedTextBytes(await readFile(source))), expected, `${relative} changed`); diff --git a/website-maintenance/03-home-particle-system.md b/website-maintenance/03-home-particle-system.md index 91a8fde..e1eb79e 100644 --- a/website-maintenance/03-home-particle-system.md +++ b/website-maintenance/03-home-particle-system.md @@ -4,6 +4,20 @@ The Home page uses a progressive visual enhancement. Its HTML identity and biographical copy render without JavaScript; Three.js adds an `aria-hidden` background only after the client is ready. +## Page composition + +- The first viewport is a dedicated particle stage. It contains no biography + card or profile sidebar; only the shared navigation and a quiet scroll cue + sit above the visual. +- `app/home.module.css` owns every Home-specific layout rule. The shared + Education, Past Experience, and Current Chapter layout remains unchanged. +- The visible identity, biography, and contact coordinates live in the solid + editorial section below the particle stage. This section is the Home page's + semantic content and the skip-link target. +- `SiteShell` keeps the profile sidebar on by default. Home explicitly opts out + with `showProfile={false}` and supplies its own frame class, so new routes do + not inherit the immersive layout accidentally. + ## Module boundaries - `ParticleBackground.tsx` owns feature detection and lazy engine loading. @@ -13,6 +27,8 @@ background only after the client is ready. - `particle-config.ts` is the single source for point counts, timing, camera, and the `SCATTER → THEODORE → SCATTER → OUYANG` loop. - `ParticleBackground.module.css` owns the static fallback and canvas layers. + Its absolute layer is bounded to the opening viewport instead of following + the reader behind the biography and footer. ## Accessibility and failure behavior