# Digital College Yearbook

> Design, build, and refine a web-based digital college yearbook — a permanent, interactive, and emotionally resonant record of a graduating batch's academic life. Triggers on: "build a digital yearbook", "create an online yearbook", "design a batch website", "make an interactive yearbook", "college graduation website", "batch memento site", "student roster web app", "class of [year] website", or any request to produce a scrollable, searchable, shareable yearbook experience on the web. Covers all phases: information architecture, visual design, animation choreography, student data modeling, photo management, and deployment.

- Skill: `aibot88/digital-college-yearbook` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aibot88/digital-college-yearbook`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aibot88/digital-college-yearbook/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: aibot88 (https://skillmd.com/u/aibot88)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/aibot88/digital-college-yearbook

---


<!--
  ╔══════════════════════════════════════════════════════════════════════════╗
  ║       DIGITAL COLLEGE YEARBOOK — AGENT SKILL SPECIFICATION              ║
  ║       The complete system for building a batch's permanent record        ║
  ╚══════════════════════════════════════════════════════════════════════════╝

  AGENT BEHAVIOR MANDATE
  ──────────────────────
  A digital yearbook is not a website. It is a time capsule.
  Every decision — color, font, animation timing, section order, photo crop —
  must be made with one question in mind:

      "Will this still feel right when someone opens this ten years from now?"

  The agent must produce work that is:

    ① Timeless    — Design choices that do not date themselves in 2 years
    ② Emotional   — Every scroll triggers memory, pride, or warmth
    ③ Complete    — No section is a placeholder; every feature is implemented
    ④ Performant  — Loads fast even with hundreds of high-res portraits
    ⑤ Preserved   — Static-deployable; no database required; survives decades
    ⑥ Accessible  — Graduates of all abilities can experience it fully

  DO NOT build a generic photo gallery.
  DO NOT produce a portfolio template with student names pasted in.
  BUILD a yearbook — structured like a book, felt like a memory.
-->

---

## 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

```json
{
  "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

```json
{
  "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)

```html
<!-- 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

```html
<!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

```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)

```js
// 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

```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

```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)
