0 — Agent Initialization Protocol
Before generating any code or design output, execute this sequence in order:
STEP 1 → CLASSIFY the yearbook scope
[ ] Single department/program [ ] Full college/university
[ ] One graduating batch [ ] Multi-batch alumni archive
STEP 2 → EXTRACT brief signals
— Batch name / theme word (e.g., "Meridian", "Ascendance")
— Graduation year (e.g., 2026)
— School / Department name
— Estimated student count (affects data architecture)
— Existing brand colors / logo (if none → generate)
— Primary device of audience (mobile-first vs. desktop-first)
— Tone keywords (warm, elegant, bold, minimal...)
— Sections needed (see § 3 for full menu)
— Special features requested (search, audio, print mode...)
STEP 3 → SELECT the Yearbook Aesthetic Archetype (§ 2)
STEP 4 → DESIGN the Information Architecture (§ 3)
STEP 5 → BUILD the Technical Stack (§ 4)
STEP 6 → IMPLEMENT section by section (§ 5–12)
STEP 7 → APPLY the Performance & Refinement pass (§ 13–14)
STEP 8 → OUTPUT the Deployment Checklist (§ 15)
If Step 2 signals are incomplete, the agent INFERS from college batch defaults and logs all assumptions in the Agent Assumptions Log (§ 16).
1 — What a Digital Yearbook Is (and Is Not)
1.1 Definitional Boundaries
IS a digital yearbook: IS NOT a digital yearbook:
────────────────────────────────────── ──────────────────────────────────────
A structured, chapter-based narrative A photo dump / gallery site
A permanent record tied to a year A general student organization site
An emotionally curated experience A portfolio or resume site
A searchable student directory A social media feed
A time-stamped cultural artifact A CMS-managed blog
Designed to be opened once a year Designed for daily active use
Static-deployable, link-shareable Requires login or account
1.2 The Three Experience Goals
Every feature and design decision must serve at least one of these:
GOAL 1 — DISCOVERY
"I want to find a specific classmate / photo / moment quickly."
Served by: Search, filtering, alphabetical roster, section nav
GOAL 2 — IMMERSION
"I want to relive the experience of being in this batch."
Served by: Scroll animations, music, timeline, quotes, memories
GOAL 3 — PRESERVATION
"I want this to exist forever and still work in 2040."
Served by: Static deployment, no auth, progressive enhancement,
semantic HTML, no proprietary dependencies
2 — Aesthetic Archetype Selection
2.1 The Six Yearbook Aesthetics
The agent MUST select exactly ONE primary aesthetic. All design decisions (color, font, layout, animation style, photo treatment) derive from it.
╔══════════════════╦═══════════════════════════════════════════════════════╗
║ ARCHETYPE ║ FINGERPRINT ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Clean white space · Serif editorial type ║
║ EDITORIAL ║ Large portrait photography · Minimal ornament ║
║ LUXURY ║ Gold or copper accent · Think: Vogue meets academia ║
║ ║ Best for: medicine, law, architecture batches ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Dark background · Glowing accent color ║
║ CINEMATIC ║ Full-bleed photography · Dramatic typography ║
║ DARK ║ GSAP-heavy scroll · Think: film credits + memorial ║
║ ║ Best for: design, arts, engineering batches ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Warm paper tones · Serif + handwritten ║
║ VINTAGE ║ Sepia-tinted photos · Grain texture overlays ║
║ NOSTALGIC ║ Aged borders · Think: analog photography album ║
║ ║ Best for: nursing, education, humanities batches ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Vivid color blocks · Bold display typography ║
║ BOLD ║ Asymmetric layouts · High-energy animations ║
║ CONTEMPORARY ║ Grid-breaking design · Think: creative agency annual ║
║ ║ Best for: marketing, tourism, multimedia batches ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Sky blue + warm white · Rounded corners ║
║ SOFT ║ Gentle animations · Illustrated accents ║
║ ACADEMIC ║ Institutional pride · Think: official commencement ║
║ ║ Best for: education, social work, theology batches ║
╠══════════════════╬═══════════════════════════════════════════════════════╣
║ ║ Monochrome base + 1 electric accent ║
║ MINIMAL ║ Data-forward type design · Tight grid ║
║ TYPOGRAPHIC ║ Photography as pure content · Think: Swiss design ║
║ ║ Best for: IT, computer science, mathematics batches ║
╚══════════════════╩═══════════════════════════════════════════════════════╝
2.2 Aesthetic → Design Token Quick Map
AESTHETIC PRIMARY BG TEXT ACCENT FONT STYLE
────────────── ──────────── ─────────── ───────────── ──────────────────
Editorial Luxury #FAFAF7 #1A1A18 #B8963E (gold) Serif display + thin sans
Cinematic Dark #070810 #F0EFF6 #00E5FF (cyan) Condensed sans + mono
Vintage Nostalgic #F5EDD8 #2C1F0E #8B5E3C (sepia) Serif + script accent
Bold Contemporary #F4F2EE #0D0D0D #FF3D00 (red) Black display + clean sans
Soft Academic #F0F5FF #1C2B4A #3B82F6 (blue) Rounded sans + serif body
Minimal Typo #FFFFFF #111111 #6EE7B7 (mint) Geometric mono + body sans
3 — Information Architecture
3.1 The Yearbook Section Menu
Every digital yearbook is assembled from these canonical sections.
The agent marks each as [REQUIRED], [RECOMMENDED], or [OPTIONAL].
╔══════════════════════════════════════════════════════════════════════════╗
║ SECTION TYPE PRIORITY NOTES ║
╠══════════════════════════════════════════════════════════════════════════╣
║ 00 · COVER / LANDING Full-bleed REQUIRED First impression ║
║ 01 · TABLE OF CONTENTS Interactive REQUIRED Chapter nav ║
║ 02 · OPENING MESSAGE Editorial REQUIRED Dean/adviser note ║
║ 03 · BATCH THEME & STORY Narrative REQUIRED Year in words ║
║ 04 · STUDENT ROSTER Interactive REQUIRED The core section ║
║ 05 · OFFICERS & LEADERS Feature REQUIRED Org chart style ║
║ 06 · IN MEMORIAM Editorial REQUIRED* If applicable ║
║ 07 · TIMELINE / HIGHLIGHTS Scroll-story RECOMMENDED Key events ║
║ 08 · MEMORIES GALLERY Masonry grid RECOMMENDED Candid photos ║
║ 09 · SUPERLATIVES Card grid RECOMMENDED Batch awards ║
║ 10 · STATISTICS & FACTS Infographic RECOMMENDED Batch by numbers ║
║ 11 · FAREWELL MESSAGES Scrolling RECOMMENDED Student quotes ║
║ 12 · FACULTY TRIBUTES Feature cards RECOMMENDED Prof portraits ║
║ 13 · SPONSORS / SUPPORTERS Logo grid OPTIONAL Funding partners ║
║ 14 · CLOSING / COLOPHON Editorial REQUIRED Credits, version ║
╚══════════════════════════════════════════════════════════════════════════╝
* [REQUIRED] if any deceased classmates. Handle with dignity and care.
3.2 Narrative Flow — The Emotional Arc
The section order must follow this emotional arc. Do not reorder without reason.
ACT I — ARRIVAL (Sections 00–03)
Purpose: Orient, establish identity, set the emotional tone.
Feeling: "I'm home. This is us."
Pacing: Slow, deliberate, cinematic reveals.
ACT II — THE PEOPLE (Sections 04–06)
Purpose: Center the humans. This is the most-visited section.
Feeling: Recognition, pride, belonging.
Pacing: Interactive, browsable, searchable.
ACT III — THE STORY (Sections 07–11)
Purpose: Narrate the journey. Trigger memories.
Feeling: Nostalgia, laughter, collective identity.
Pacing: Varied — timeline is slow; gallery is fast-browsable.
ACT IV — GRATITUDE (Sections 12–13)
Purpose: Acknowledge those who shaped the journey.
Feeling: Warmth, respect, closure.
Pacing: Calm, unhurried.
ACT V — CLOSE (Section 14)
Purpose: A quiet, final page. The book closes.
Feeling: Bittersweet completeness.
Pacing: Very slow, minimal.
3.3 URL / Route Architecture
/ → Cover / Landing (§ 5)
/toc → Table of Contents
/message → Opening Message (§ 6)
/theme → Batch Theme & Story (§ 7)
/roster → Student Roster index (§ 8)
/roster/[slug] → Individual student profile
/officers → Officers & Leaders (§ 9)
/timeline → Timeline / Highlights (§ 10)
/gallery → Memories Gallery (§ 11)
/superlatives → Superlatives (§ 12)
/stats → Statistics & Facts (§ 13)
/messages → Farewell Messages (§ 14)
/faculty → Faculty Tributes (§ 15)
/colophon → Closing / Credits
ROUTING STRATEGY:
Static site (preferred): Hash-based SPA or flat HTML pages
Dynamic: Next.js with SSG (getStaticPaths per student)
Prefer static generation. A yearbook must work offline, on slow
connections, and without a server — forever.
3.4 Student Data Schema
{
"students": [
{
"id": "2026-001",
"slug": "maria-santos",
"name": {
"first": "Maria Celeste",
"last": "Santos",
"nickname": "Cel",
"honorific": null
},
"photo": {
"portrait": "photos/portraits/maria-santos.webp",
"portrait_2x": "photos/portraits/maria-santos@2x.webp",
"candid": ["photos/candid/santos-01.webp", "photos/candid/santos-02.webp"]
},
"academic": {
"program": "BS Computer Science",
"track": "Software Engineering",
"student_number": "2022-00123",
"latin_honor": "Cum Laude",
"awards": ["Best Thesis", "Dean's Lister"]
},
"personal": {
"quote": "The best is yet to come.",
"quote_source": "F. Scott Fitzgerald",
"ambition": "Build technology that matters.",
"favorite_memory": "Late nights at the CS lab",
"message_to_batch": "Thank you for making every hard day worth it."
},
"roles": ["President, ACM Student Chapter", "Lead Developer, Thesis 2026"],
"socials": {
"linkedin": "https://linkedin.com/in/mariasantos",
"github": "https://github.com/celdev"
},
"superlatives": ["Most Likely to Become a CTO", "Best Coder"],
"section": "CS-4A",
"group_number": 1
}
]
}
3.5 Site Configuration Schema
{
"yearbook": {
"title": "Meridian",
"subtitle": "Class of 2026",
"institution": "University of the Philippines Visayas",
"department": "College of Management",
"graduation_date": "2026-06-15",
"theme_statement": "We crossed the meridian together.",
"color_primary": "#0F3460",
"color_accent": "#C8A951",
"font_display": "Cormorant Garamond",
"font_body": "DM Sans",
"cover_video": "assets/cover-reel.mp4",
"cover_image": "assets/cover-still.webp",
"logo": "assets/batch-logo.svg",
"school_logo": "assets/school-logo.svg",
"total_graduates": 187,
"sections_enabled": [
"cover", "toc", "message", "theme", "roster",
"officers", "timeline", "gallery", "superlatives",
"stats", "messages", "faculty", "colophon"
],
"features": {
"search": true,
"audio_ambient": false,
"print_mode": true,
"dark_mode_toggle": true,
"share_card_generation": true
}
}
}
4 — Technical Stack Architecture
4.1 Stack Decision Tree
Is the student count > 300?
YES → Use a build-time static generator (Next.js SSG, Astro, Eleventy)
NO → Can use a single-page vanilla HTML/JS application
Is a CMS / admin panel needed for content updates?
YES → Headless CMS (Contentful, Sanity) + static generator frontend
NO → JSON-file-driven static build (preferred for permanence)
Will the yearbook be self-hosted (shared hosting)?
YES → Must be pure static output (HTML + CSS + JS only)
NO → Can use Vercel / Netlify with serverless functions if needed
Is the development team one person?
YES → Vanilla HTML + CSS + JS + GSAP (no build step required)
NO → Vite + vanilla JS modules OR Next.js static export
4.2 Recommended Stack (Greenfield, 50–300 students)
LAYER TECHNOLOGY RATIONALE
───────────── ────────────────────── ──────────────────────────────────────
Structure Semantic HTML5 Permanent, browser-agnostic
Styling Vanilla CSS + Tokens No dependency, fully portable
Animation GSAP 3 + ScrollTrigger Industry standard; CDN-available
Data JSON flat files No database; editable by non-devs
Build None (or Vite) Zero build = zero future breakage
Fonts Google Fonts (self-host preferred)
Images WebP with JPEG fallback + native lazy loading
Icons Phosphor Icons (CDN) or SVG inline
Hosting GitHub Pages / Netlify Free forever; git-versioned
Search Fuse.js (fuzzy search) Lightweight, no server needed
4.3 CDN Dependencies (Pin Versions — Critical for Longevity)
<!-- In <head> — load order matters -->
<!-- Fonts: Self-host if possible, else preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@400;500;600&display=swap">
<!-- GSAP Core (ALWAYS pin to specific version) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"
integrity="sha512-[SRI_HASH]" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"
integrity="sha512-[SRI_HASH]" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollToPlugin.min.js"
crossorigin="anonymous"></script>
<!-- Fuse.js for search (pin version) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/7.0.0/fuse.min.js"
crossorigin="anonymous"></script>
<!-- CRITICAL: Always include local fallback copies in /vendor/ -->
<!-- If CDN fails in 2040, local copies still work -->
4.4 Project File Structure
/yearbook-[batch-name]-[year]/
│
├── index.html ← Cover / Landing
├── toc.html ← Table of Contents
├── message.html ← Opening Message
├── theme.html ← Batch Theme
├── roster.html ← Roster Index
├── roster/
│ └── [slug].html ← Individual profile pages (generated)
├── officers.html
├── timeline.html
├── gallery.html
├── superlatives.html
├── stats.html
├── messages.html
├── faculty.html
├── colophon.html
│
├── assets/
│ ├── css/
│ │ ├── tokens.css ← Design tokens (variables only)
│ │ ├── reset.css ← CSS reset
│ │ ├── base.css ← Typography, body, global elements
│ │ ├── layout.css ← Grid, containers, responsive
│ │ ├── components.css ← Cards, nav, modals, badges
│ │ ├── animations.css ← CSS-only animation declarations
│ │ └── print.css ← @media print overrides
│ │
│ ├── js/
│ │ ├── main.js ← Init, nav, global interactions
│ │ ├── roster.js ← Roster scroll + search logic
│ │ ├── gallery.js ← Gallery lightbox + masonry
│ │ ├── timeline.js ← Timeline scroll narrative
│ │ ├── superlatives.js ← Superlatives card interactions
│ │ ├── stats.js ← Animated counters + charts
│ │ ├── search.js ← Fuse.js search implementation
│ │ └── utils.js ← Shared helpers (debounce, etc.)
│ │
│ ├── fonts/ ← Self-hosted font files (.woff2)
│ │
│ ├── icons/ ← SVG icon sprites
│ │
│ └── vendor/ ← Local copies of all CDN libraries
│ (critical for long-term preservation)
│
├── data/
│ ├── students.json ← Master student data
│ ├── officers.json ← Leadership roster
│ ├── faculty.json ← Faculty profiles
│ ├── timeline.json ← Events & milestones
│ ├── gallery.json ← Photo metadata
│ ├── superlatives.json ← Awards data
│ ├── messages.json ← Farewell messages
│ └── config.json ← Site-wide configuration
│
└── photos/
├── portraits/ ← [slug].webp + [slug]@2x.webp
├── candid/ ← candid-[n].webp
├── events/ ← [event-slug]-[n].webp
├── cover/ ← cover.webp, cover-mobile.webp
└── faculty/ ← [slug].webp
5 — Section 00: Cover / Landing Page
5.1 Design Specification
PURPOSE: The emotional hook. Sets the entire yearbook's tone.
Must stop the viewer within 3 seconds.
LAYOUT: Full-bleed. No scrollbar visible on load.
Content is vertically and horizontally centered.
LAYERS (back to front):
Layer 1: Cover image or video (full viewport, object-fit: cover)
Layer 2: Gradient overlay (darkens image without losing texture)
Layer 3: Batch theme word (hero typography — the largest type on the site)
Layer 4: Subtitle (graduation year + institution)
Layer 5: Scroll indicator (animated arrow or "Scroll to begin")
Layer 6: Navigation (minimal top bar, fades in after 1.5s delay)
COVER IMAGE REQUIREMENTS:
— Group photo preferred (batch identity, not individual)
— Must work with text overlay (avoid bright, flat images)
— Mobile crop: center-focused (heads must be visible at 390px wide)
— WebP, 1920×1080px (desktop), 828×1792px (mobile portrait)
— Use <picture> for art-direction responsive serving
5.2 HTML Structure
<!DOCTYPE html>
<html lang="en" data-theme="dark" data-aesthetic="cinematic">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="[Batch Name] — [Year] · [Institution]">
<!-- Open Graph (for link sharing — every graduate will share this) -->
<meta property="og:title" content="[Batch Name] — Class of [Year]">
<meta property="og:description" content="[Theme Statement]">
<meta property="og:image" content="photos/cover/og-cover.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height"content="630">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<title>[Batch Name] · Class of [Year] · [Institution]</title>
<link rel="icon" href="assets/icons/favicon.ico">
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png">
<link rel="stylesheet" href="assets/css/tokens.css">
<link rel="stylesheet" href="assets/css/reset.css">
<link rel="stylesheet" href="assets/css/base.css">
<link rel="stylesheet" href="assets/css/layout.css">
<link rel="stylesheet" href="assets/css/components.css">
<link rel="stylesheet" href="assets/css/animations.css">
</head>
<body class="page-cover">
<!-- ── Navigation ─────────────────────────────── -->
<nav class="site-nav site-nav--transparent" aria-label="Main navigation"
id="site-nav">
<a href="/" class="site-nav__logo" aria-label="Home">
<img src="assets/batch-logo.svg" alt="[Batch Name] Logo" width="40" height="40">
</a>
<button class="site-nav__menu-btn" aria-label="Open navigation menu"
aria-expanded="false" aria-controls="nav-drawer">
<span class="sr-only">Menu</span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</nav>
<!-- ── Full-screen Drawer Nav ─────────────────── -->
<div class="nav-drawer" id="nav-drawer" role="dialog" aria-modal="true"
aria-label="Navigation menu" hidden>
<button class="nav-drawer__close" aria-label="Close menu">×</button>
<nav aria-label="Site chapters">
<ol class="nav-drawer__list" role="list">
<li><a href="toc.html" class="nav-drawer__link">Table of Contents</a></li>
<li><a href="message.html" class="nav-drawer__link">Opening Message</a></li>
<li><a href="theme.html" class="nav-drawer__link">Batch Theme</a></li>
<li><a href="roster.html" class="nav-drawer__link">Student Roster</a></li>
<li><a href="officers.html" class="nav-drawer__link">Officers</a></li>
<li><a href="timeline.html" class="nav-drawer__link">Timeline</a></li>
<li><a href="gallery.html" class="nav-drawer__link">Gallery</a></li>
<li><a href="superlatives.html" class="nav-drawer__link">Superlatives</a></li>
<li><a href="messages.html" class="nav-drawer__link">Messages</a></li>
<li><a href="faculty.html" class="nav-drawer__link">Faculty</a></li>
</ol>
</nav>
</div>
<div class="nav-drawer__backdrop" aria-hidden="true"></div>
<!-- ── Cover Hero ─────────────────────────────── -->
<main class="cover-hero" id="main-content">
<!-- Background: video with image fallback -->
<div class="cover-hero__media" aria-hidden="true">
<picture>
<source media="(max-width: 768px)"
srcset="photos/cover/cover-mobile.webp" type="image/webp">
<source srcset="photos/cover/cover.webp" type="image/webp">
<img src="photos/cover/cover.jpg"
alt="" role="presentation"
width="1920" height="1080"
loading="eager" fetchpriority="high">
</picture>
<!-- Optional: loop video for ambience -->
<!-- <video autoplay loop muted playsinline poster="photos/cover/cover.jpg">
<source src="assets/cover-reel.mp4" type="video/mp4">
</video> -->
</div>
<!-- Overlay gradient -->
<div class="cover-hero__overlay" aria-hidden="true"></div>
<!-- Content -->
<div class="cover-hero__content">
<p class="cover-hero__eyebrow" data-animate="fade-up" data-delay="0.2">
<span>[Institution]</span>
<span class="separator" aria-hidden="true">·</span>
<span>[Department]</span>
</p>
<h1 class="cover-hero__title" data-animate="split-reveal" data-delay="0.5">
[BATCH THEME WORD]
</h1>
<p class="cover-hero__subtitle" data-animate="fade-up" data-delay="0.9">
Class of [Year]
</p>
<p class="cover-hero__theme" data-animate="fade-up" data-delay="1.1">
"[Theme Statement]"
</p>
</div>
<!-- Scroll indicator -->
<div class="cover-hero__scroll-cue" aria-hidden="true">
<span class="scroll-cue__label">Scroll to begin</span>
<div class="scroll-cue__line"></div>
</div>
</main>
<script src="assets/vendor/gsap.min.js"></script>
<script src="assets/vendor/ScrollTrigger.min.js"></script>
<script src="assets/js/main.js" type="module"></script>
</body>
</html>
5.3 Cover CSS
/* ── Cover Hero Layout ───────────────────────── */
.cover-hero {
position: relative;
width: 100%;
height: 100dvh; /* dvh = accounts for mobile address bar */
min-height: 600px;
display: grid;
place-items: center;
overflow: hidden;
}
/* ── Media Layer ─────────────────────────────── */
.cover-hero__media {
position: absolute;
inset: 0;
z-index: 0;
}
.cover-hero__media img,
.cover-hero__media video {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 30%; /* Prioritize faces */
}
/* ── Gradient Overlay ────────────────────────── */
.cover-hero__overlay {
position: absolute;
inset: 0;
z-index: 1;
background:
linear-gradient(
to bottom,
rgba(0,0,0,0.1) 0%,
rgba(0,0,0,0.3) 40%,
rgba(0,0,0,0.7) 75%,
rgba(0,0,0,0.9) 100%
);
}
/* ── Content Layer ───────────────────────────── */
.cover-hero__content {
position: relative;
z-index: 2;
text-align: center;
padding: 0 var(--space-6);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.cover-hero__eyebrow {
font-family: var(--font-accent);
font-size: var(--text-xs);
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255,255,255,0.6);
display: flex;
gap: var(--space-3);
align-items: center;
opacity: 0; /* GSAP will animate in */
}
.cover-hero__title {
font-family: var(--font-display);
font-size: clamp(4rem, 15vw, 10rem);
font-weight: 600;
line-height: 0.9;
letter-spacing: -0.03em;
color: var(--clr-text-primary);
opacity: 0; /* GSAP will animate in */
}
.cover-hero__subtitle {
font-family: var(--font-body);
font-size: var(--text-lg);
font-weight: 400;
color: rgba(255,255,255,0.8);
opacity: 0;
}
.cover-hero__theme {
font-family: var(--font-display);
font-style: italic;
font-size: var(--text-base);
color: var(--clr-accent);
opacity: 0;
}
/* ── Scroll Cue ──────────────────────────────── */
.cover-hero__scroll-cue {
position: absolute;
bottom: var(--space-8);
left: 50%;
transform: translateX(-50%);
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
}
.scroll-cue__label {
font-family: var(--font-accent);
font-size: var(--text-xs);
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(255,255,255,0.4);
}
.scroll-cue__line {
width: 1px;
height: 48px;
background: linear-gradient(to bottom, var(--clr-accent), transparent);
animation: scroll-cue-pulse 2s ease-in-out infinite;
}
@keyframes scroll-cue-pulse {
0%, 100% { opacity: 0.3; transform: scaleY(1); }
50% { opacity: 1; transform: scaleY(1.15); }
}
5.4 Cover Animation (GSAP)
// cover-animation.js
import { gsap } from 'gsap';
export function initCoverAnimation() {
// Respect reduced motion preference
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) {
// Simple instant reveal
document.querySelectorAll('[data-animate]').forEach(el => {
el.style.opacity = '1';
el.style.transform = 'none';
});
return;
}
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Phase 1: Image scale up (Ken Burns on load)
tl.from('.cover-hero__media img', {
scale: 1.08,
duration: 2.5,
ease: 'power2.out',
}, 0);
// Phase 2: Eyebrow
tl.to('.cover-hero__eyebrow', {
opacity: 1,
y: 0,
duration: 0.7,
}, 0.3);
// Phase 3: Title — character-by-character reveal
const titleEl = document.querySelector('.cover-hero__title');
if (titleEl) {
// Split into characters manually (no SplitText plugin needed)
const text = titleEl.textContent.trim();
titleEl.innerHTML = text
.split('')
.map(ch => ch === ' '
? '<span class="char char--space"> </span>'
: `<span class="char" aria-hidden="true">${ch}</span>`)
.join('');
titleEl.setAttribute('aria-label', text); // screen reader gets the full word
tl.from('.cover-hero__title .char', {
opacity: 0,
y: 40,
rotateX: -60,
transformOrigin: 'top center',
duration: 0.6,
stagger: 0.04,
}, 0.6);
tl.to('.cover-hero__title', { opacity: 1, duration: 0 }, 0.6);
}
// Phase 4: Subtitle + theme
tl.to(['.cover-hero__subtitle', '.cover-hero__theme'], {
opacity: 1,
y: 0,
duration: 0.6,
stagger: 0.15,
}, 1.2);
// Phase 5: Scroll cue (delayed entrance)
tl.from('.cover-hero__scroll-cue', {
opacity: 0,
y: 10,
duration: 0.8,
}, 2.0);
// Nav fades in independently
gsap.to('.site-nav', {
opacity: 1,
duration: 0.6,
delay: 1.5,
});
}
6 — Section 04: Student Roster (Core Section)
6.1 Roster Architecture
The roster is the most critical and most-visited section. It must support:
FEATURE IMPLEMENTATION
────────────────────── ──────────────────────────────────────────────
Split-text scroll reveal GSAP ScrollTrigger (horizontal split, desktop)
Vertical stack (mobile — NO horizontal split)
Background crossfade A/B layer swap with GSAP opacity tween
Portrait expansion CSS width/height transition from 0 to full
Fuzzy search Fuse.js on name, nickname, program, section
Filter by program JavaScript array filter, instant re-render
Filter by section Same filter mechanism
Alphabetical sort Default; secondary sort by program
Individual profile page Static HTML per student OR JS modal
Virtual scroll Required for batches > 100 students
Keyboard navigation Arrow keys cycle through students
6.2 Roster Layout Variants
VARIANT A — CINEMATIC SCROLL (Default for ≤ 200 students)
One student fills the full viewport at a time.
Scrolling triggers the next student's reveal.
Background crossfades to blurred portrait.
Name splits left/right. Portrait expands in center gap.
[See full implementation: § 6.3–6.5]
VARIANT B — MAGAZINE GRID (For > 200 students or "browse" preference)
4-column desktop / 2-column mobile photo grid.
On hover: portrait lifts, name overlays.
On click: student profile modal or page.
Filter/search bar at top, sticky.
VARIANT C — HYBRID
Cinematic scroll for officers/honor students.
Magazine grid for the full class.
Toggle between views with animated transition.
6.3 Roster HTML
<section class="roster-section" id="roster" aria-label="Student Roster">
<!-- Search & Filter Bar (sticky) -->
<div class="roster-controls" role="search" aria-label="Search students">
<div class="search-field">
<label for="roster-search" class="sr-only">Search students</label>
<input type="search" id="roster-search"
placeholder="Search by name, nickname, program..."
autocomplete="off" spellcheck="false"
aria-controls="roster-results"
aria-describedby="search-hint">
<span id="search-hint" class="sr-only">
Results update as you type
</span>
</div>
<div class="filter-group" role="group" aria-label="Filter by program">
<button class="filter-btn is-active" data-filter="all">All</button>
<!-- Generated dynamically from data -->
</div>
<p class="roster-count" aria-live="polite" aria-atomic="true">
Showing <span id="visible-count">187</span> graduates
</p>
</div>
<!-- Roster Wrapper (creates scroll height) -->
<div class="roster-wrapper" id="roster-wrapper"
aria-label="Student roster, scroll to browse">
<!-- Background crossfade layers -->
<div class="bg-layer bg-layer--a is-active" aria-hidden="true"
role="presentation"></div>
<div class="bg-layer bg-layer--b" aria-hidden="true"
role="presentation"></div>
<!-- Progress indicator -->
<div class="roster-progress" aria-hidden="true">
<div class="roster-progress__bar"></div>
<span class="roster-progress__counter">
<span id="current-student">1</span> / <span id="total-students">187</span>
</span>
</div>
<!-- Sticky viewport -->
<div class="roster-sticky" id="roster-sticky">
<div class="roster-list" id="roster-list"
role="list" aria-label="Graduate profiles">
<!-- Populated by roster.js from students.json -->
</div>
</div>
</div>
<!-- Keyboard navigation hint -->
<div class="keyboard-hint" aria-hidden="true">
<kbd>↑</kbd><kbd>↓</kbd> to navigate
</div>
</section>
6.4 Roster CSS
/* ── Controls ────────────────────────────────── */
.roster-controls {
position: sticky;
top: var(--nav-height);
z-index: 50;
background: rgba(var(--clr-bg-rgb), 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: var(--space-4) var(--space-6);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
border-bottom: 1px solid var(--clr-border);
}
.search-field {
flex: 1;
min-width: 200px;
position: relative;
}
.search-field input {
width: 100%;
background: var(--clr-surface);
border: 1px solid var(--clr-border);
border-radius: var(--radius-full);
padding: var(--space-2) var(--space-4);
color: var(--clr-text-primary);
font-size: var(--text-sm);
transition: border-color var(--duration-fast);
}
.search-field input:focus {
outline: none;
border-color: var(--clr-accent);
}
/* ── Roster Wrapper (scroll container) ────────── */
.roster-wrapper {
position: relative;
/* Height set by JS: students.length × window.innerHeight */
}
/* ── Sticky Viewport ─────────────────────────── */
.roster-sticky {
position: sticky;
top: calc(var(--nav-height) + var(--controls-height, 72px));
height: calc(100dvh - var(--nav-height) - var(--controls-height, 72px));
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Individual Student Item ─────────────────── */
.roster-item {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: clamp(var(--space-6), 4vw, var(--space-16));
padding: 0 clamp(var(--space-6), 6vw, var(--space-24));
opacity: 0;
pointer-events: none;
transition: opacity 0.5s var(--ease-out-quart);
}
.roster-item.is-active {
opacity: 1;
pointer-events: auto;
}
/* ── Student Name ────────────────────────────── */
.roster-item__name {
font-family: var(--font-display);
font-size: clamp(2rem, 8vw, 7rem);
font-weight: 600;
line-height: 1;
letter-spacing: -0.025em;
color: var(--clr-text-primary);
transition: transform 0.6s var(--ease-out-quart),
opacity 0.4s ease;
white-space: nowrap;
flex: 1;
}
.roster-item__name--first {
text-align: right;
transform: translateX(0);
}
.roster-item__name--last {
text-align: left;
transform: translateX(0);
}
/* Active state: names split apart */
@media (min-width: 769px) {
.roster-item.is-active .roster-item__name--first {
transform: translateX(clamp(-2rem, -3vw, -4rem));
}
.roster-item.is-active .roster-item__name--last {
transform: translateX(clamp(2rem, 3vw, 4rem));
}
}
/* ── Portrait Container ──────────────────────── */
.roster-item__portrait-wrap {
flex-shrink: 0;
overflow: hidden;
border-radius: var(--radius-lg);
width: 0;
height: 0;
transition: width 0.6s var(--ease-out-quart),
height 0.6s var(--ease-out-quart);
position: relative;
}
.roster-item.is-active .roster-item__portrait-wrap {
width: clamp(100px, 16vw, 240px);
height: clamp(130px, 20vw, 300px);
}
.roster-item__portrait-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top; /* Prioritize faces */
display: block;
}
/* ── Info Overlay on Portrait ────────────────── */
.roster-item__info-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: var(--space-3);
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white;
opacity: 0;
transition: opacity 0.3s ease;
}
.roster-item.is-active:hover .roster-item__info-overlay,
.roster-item.is-active:focus-within .roster-item__info-overlay {
opacity: 1;
}
.roster-item__program {
font-size: var(--text-xs);
font-family: var(--font-accent);
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
.roster-item__honor {
font-size: var(--text-xs);
color: var(--clr-accent);
font-weight: 600;
}
/* ── Background Layers ───────────────────────── */
.bg-layer {
position: fixed;
inset: 0;
background-size: cover;
background-position: center top;
filter: blur(28px) brightness(0.3) saturate(1.4);
transform: scale(1.12); /* Prevents blur edge artifacts */
opacity: 0;
z-index: -1;
transition: opacity 0.9s var(--ease-out-quart);
will-change: opacity;
}
.bg-layer.is-active { opacity: 1; }
/* ── Progress Indicator ──────────────────────── */
.roster-progress {
position: fixed;
right: var(--space-6);
top: 50%;
transform: translateY(-50%);
z-index: 30;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}
.roster-progress__bar {
width: 2px;
height: 120px;
background: var(--clr-border);
border-radius: var(--radius-full);
position: relative;
overflow: hidden;
}
.roster-progress__bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
background: var(--clr-accent);
border-radius: inherit;
height: var(--progress, 0%);
transition: height 0.3s ease;
}
.roster-progress__counter {
font-family: var(--font-accent);
font-size: 0.6rem;
letter-spacing: 0.1em;
color: var(--clr-text-secondary);
writing-mode: vertical-rl;
}
/* ── MOBILE OVERRIDE — Vertical stack ────────── */
@media (max-width: 768px) {
.roster-item {
flex-direction: column;
text-align: center;
padding: var(--space-6);
gap: var(--space-3);
overflow: hidden; /* NO horizontal overflow ever */
}
.roster-item__name {
white-space: normal; /* Allow wrapping on small screens */
word-break: break-word;
flex: unset;
width: 100%;
font-size: clamp(2rem, 10vw, 3.5rem);
}
/* Hard override: no horizontal transform on mobile */
.roster-item.is-active .roster-item__name--first,
.roster-item.is-active .roster-item__name--last {
transform: none !important;
text-align: center !important;
}
/* Vertical order: First Name → Portrait →
…(truncated)