diff --git a/episode-07/portfolio/README.md b/episode-07/portfolio/README.md
new file mode 100644
index 0000000..a156afc
--- /dev/null
+++ b/episode-07/portfolio/README.md
@@ -0,0 +1,20 @@
+# Episode 7 portfolio website
+
+This folder contains Jamie Rivera's portfolio website for Episode 7.
+
+## Folder contents
+
+| Path | Purpose |
+| --- | --- |
+| `index.html` | Main portfolio page, including the homepage blog preview. |
+| `styles.css` | Shared styling for the portfolio, blog, and published posts. |
+| `script.js` | Responsive navigation, theme switching, animations, and blog listing behavior. |
+| `resume/` | Resume displayed and downloaded from the portfolio. |
+| `blog/index.html` | Full list of published blog posts. |
+| `blog/posts-src/` | Markdown source files used to write and edit posts. |
+| `blog/posts/` | Static HTML pages generated for published posts. |
+| `blog/posts.json` | Post titles, dates, summaries, and links shown in the website's blog lists. |
+| `blog/generate_posts.py` | Converts approved Markdown sources into static post pages and updates `posts.json`. |
+
+The website starts with no published posts. To generate approved posts, run
+`python blog/generate_posts.py` from this folder.
diff --git a/episode-07/portfolio/blog/generate_posts.py b/episode-07/portfolio/blog/generate_posts.py
new file mode 100644
index 0000000..81a7caf
--- /dev/null
+++ b/episode-07/portfolio/blog/generate_posts.py
@@ -0,0 +1,194 @@
+from __future__ import annotations
+
+import html
+import json
+import re
+from datetime import date
+from pathlib import Path
+
+
+BLOG_DIR = Path(__file__).parent
+SOURCE_DIR = BLOG_DIR / "posts-src"
+OUTPUT_DIR = BLOG_DIR / "posts"
+POSTS_FILE = BLOG_DIR / "posts.json"
+REQUIRED_FIELDS = ("title", "date", "summary")
+
+
+def parse_post(source_path: Path) -> tuple[dict[str, str], str]:
+ text = source_path.read_text(encoding="utf-8")
+ if not text.startswith("---\n"):
+ raise ValueError(f"{source_path.name}: expected metadata between --- lines")
+
+ try:
+ metadata_text, body = text[4:].split("\n---\n", 1)
+ except ValueError as error:
+ raise ValueError(f"{source_path.name}: metadata is missing its closing --- line") from error
+
+ metadata: dict[str, str] = {}
+ for line in metadata_text.splitlines():
+ key, separator, value = line.partition(":")
+ if not separator or not key.strip() or not value.strip():
+ raise ValueError(f"{source_path.name}: invalid metadata line: {line}")
+ metadata[key.strip()] = value.strip()
+
+ missing = [field for field in REQUIRED_FIELDS if not metadata.get(field)]
+ if missing:
+ raise ValueError(f"{source_path.name}: missing metadata: {', '.join(missing)}")
+
+ date.fromisoformat(metadata["date"])
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", source_path.stem):
+ raise ValueError(f"{source_path.name}: filename must be a lowercase kebab-case slug")
+
+ return metadata, body.strip()
+
+
+def render_inline(text: str) -> str:
+ escaped = html.escape(text)
+ escaped = re.sub(r"\*\*(.+?)\*\*", r"\1 ", escaped)
+ escaped = re.sub(
+ r"\[([^\]]+)\]\((https?://[^)\s]+)\)",
+ r'\1 ',
+ escaped,
+ )
+ return escaped
+
+
+def render_markdown(markdown: str) -> str:
+ output: list[str] = []
+ paragraph: list[str] = []
+ list_type: str | None = None
+
+ def flush_paragraph() -> None:
+ if paragraph:
+ output.append(f"
{render_inline(' '.join(paragraph))}
")
+ paragraph.clear()
+
+ def close_list() -> None:
+ nonlocal list_type
+ if list_type:
+ output.append(f"{list_type}>")
+ list_type = None
+
+ for raw_line in markdown.splitlines():
+ line = raw_line.strip()
+ if not line:
+ flush_paragraph()
+ close_list()
+ continue
+
+ heading = re.match(r"^(#{2,3})\s+(.+)$", line)
+ unordered = re.match(r"^[-*]\s+(.+)$", line)
+ ordered = re.match(r"^\d+\.\s+(.+)$", line)
+
+ if heading:
+ flush_paragraph()
+ close_list()
+ level = len(heading.group(1))
+ output.append(f"{render_inline(heading.group(2))} ")
+ elif unordered or ordered:
+ flush_paragraph()
+ next_list_type = "ul" if unordered else "ol"
+ if list_type != next_list_type:
+ close_list()
+ output.append(f"<{next_list_type}>")
+ list_type = next_list_type
+ item = (unordered or ordered).group(1)
+ output.append(f"{render_inline(item)} ")
+ elif line.startswith("> "):
+ flush_paragraph()
+ close_list()
+ output.append(f"{render_inline(line[2:])} ")
+ else:
+ close_list()
+ paragraph.append(line)
+
+ flush_paragraph()
+ close_list()
+ return "\n ".join(output)
+
+
+def render_post_page(metadata: dict[str, str], body_html: str) -> str:
+ title = html.escape(metadata["title"])
+ published = html.escape(metadata["date"])
+ summary = html.escape(metadata["summary"])
+ return f"""
+
+
+
+
+ {title} | Jamie Rivera
+
+
+
+
+ Skip to content
+
+
+
+ {published}
+ {title}
+ {summary}
+ {body_html}
+ ← Back to blog
+
+
+
+
+
+
+"""
+
+
+def main() -> None:
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+ posts: list[dict[str, str]] = []
+
+ for output_path in OUTPUT_DIR.glob("*.html"):
+ output_path.unlink()
+
+ for source_path in SOURCE_DIR.glob("*.md"):
+ metadata, body = parse_post(source_path)
+ output_name = f"{source_path.stem}.html"
+ output_path = OUTPUT_DIR / output_name
+ output_path.write_text(
+ render_post_page(metadata, render_markdown(body)),
+ encoding="utf-8",
+ )
+ posts.append(
+ {
+ "title": metadata["title"],
+ "date": metadata["date"],
+ "summary": metadata["summary"],
+ "url": f"posts/{output_name}",
+ }
+ )
+
+ posts.sort(key=lambda post: post["date"], reverse=True)
+ POSTS_FILE.write_text(f"{json.dumps(posts, indent=2)}\n", encoding="utf-8")
+ print(f"Generated {len(posts)} post(s).")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/episode-07/portfolio/blog/index.html b/episode-07/portfolio/blog/index.html
new file mode 100644
index 0000000..b3ad737
--- /dev/null
+++ b/episode-07/portfolio/blog/index.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+ Blog | Jamie Rivera
+
+
+
+
+ Skip to content
+
+
+
+
+
+ Writing and reflections
+ Blog
+
+
+
+
+
+
+
+
+
+
diff --git a/episode-07/portfolio/blog/posts-src/.gitkeep b/episode-07/portfolio/blog/posts-src/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/episode-07/portfolio/blog/posts.json b/episode-07/portfolio/blog/posts.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/episode-07/portfolio/blog/posts.json
@@ -0,0 +1 @@
+[]
diff --git a/episode-07/portfolio/blog/posts/.gitkeep b/episode-07/portfolio/blog/posts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/episode-07/portfolio/index.html b/episode-07/portfolio/index.html
new file mode 100644
index 0000000..fa25828
--- /dev/null
+++ b/episode-07/portfolio/index.html
@@ -0,0 +1,220 @@
+
+
+
+
+
+ Jamie Rivera | Portfolio
+
+
+
+
+ Skip to content
+
+
+
+
+
+ Seattle, WA
+ Jamie Rivera (they/them)
+ Communication & Media Studies Student | Storyteller and data-curious
+
+ Junior Communication & Media Studies major with a Data Analytics minor who turns ideas into clear stories
+ across writing, audio, and social. Comfortable pairing creative work with simple data to show impact.
+
+
+
+
+
+ About
+
+ I am a junior at Cascade State University studying Communication & Media Studies with a minor in Data
+ Analytics. I enjoy creating clear, audience-first content and using simple data to show what is working.
+ I am seeking internship roles in content, communications, and community programs.
+
+
+
+
+ Resume Preview
+
+ If the preview does not load in your browser, use the download button above.
+
+
+
+
+
+
+
+ Experience
+
+
+ Marketing and Communications Intern
+ Greenline Community Trust | Seattle, WA | Jun 2025 - Present
+
+ Rebuilt the nonprofit social presence and grew Instagram following by 40% in one semester.
+ Write and schedule a weekly newsletter reaching more than 1,200 subscribers .
+ Produce short-form volunteer event recaps from filming through captions.
+
+
+
+ Peer Writing Tutor
+ Cascade State Writing Center | Seattle, WA | Sep 2024 - Present
+
+ Coach 60+ students each term on essays, resumes, and application materials.
+ Led two campus workshops on clear, structured writing.
+
+
+
+ Barista
+ Foghorn Coffee | 2023 - 2024
+
+ Trained four new hires and supported reliable service during high-volume shifts.
+
+
+
+
+
+
+ Projects
+
+
+ First-Gen Voices Podcast (Founder & Producer)
+ 2024 - Present
+
+ Launched a student podcast featuring first-generation college stories.
+ Published 15 episodes and reached 2,000+ downloads .
+ Own recording, editing, cover art, and release scheduling.
+
+ Podcast link (TODO: add URL)
+
+
+ Campus Recycling Dashboard
+ Spring 2025
+
+ Analyzed two years of campus waste data and built charts of recycling trends.
+ Recommended three changes that the sustainability office later piloted.
+
+
+
+ Greenline Content Refresh
+ Internship Project | 2025
+
+ Rewrote the Get Involved page and simplified volunteer signup flow, lifting completions.
+
+
+
+ Trail Journal
+ Personal Project | Ongoing
+
+ Growing photo-and-notes log of Pacific Northwest day hikes.
+
+ Project link (TODO: add URL)
+
+
+
+
+
+
+
+ Skills
+
+
+ Tools
+ Microsoft 365, Canva, Adobe Express, Audacity, basic HTML/CSS, Git/GitHub (learning)
+
+
+ Data
+ Spreadsheets, basic charting, survey design
+
+
+ Communication
+ Copywriting, public speaking, social media strategy
+
+
+ Languages
+ English (native), Spanish (conversational)
+
+
+
+
+
+ Education
+
+ Cascade State University
+ Seattle, WA | Expected May 2027
+ B.A. in Communication & Media Studies, Minor in Data Analytics | GPA 3.7 / 4.0
+
+ Relevant coursework: Digital Media Production, Intro to Data Analytics, Public Relations Writing, Web
+ Content and Design
+
+
+
+
+
+ Leadership & Involvement
+
+ Founder, First-Gen Voices Podcast (2024 - Present)
+ Member, Campus Sustainability Club
+ Volunteer, Seattle Public Library reading program
+
+
+
+
+
+
+
+
+
+
+
diff --git a/episode-07/portfolio/resume/Jamie Rivera - Resume.pdf b/episode-07/portfolio/resume/Jamie Rivera - Resume.pdf
new file mode 100644
index 0000000..f2ff974
Binary files /dev/null and b/episode-07/portfolio/resume/Jamie Rivera - Resume.pdf differ
diff --git a/episode-07/portfolio/script.js b/episode-07/portfolio/script.js
new file mode 100644
index 0000000..da56423
--- /dev/null
+++ b/episode-07/portfolio/script.js
@@ -0,0 +1,107 @@
+const navToggle = document.querySelector(".nav-toggle");
+const navLinks = document.querySelector(".nav-links");
+const themeToggle = document.querySelector("#theme-toggle");
+
+const themeStorageKey = "portfolio-theme";
+const savedTheme = localStorage.getItem(themeStorageKey);
+const preferredTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+const initialTheme = savedTheme || preferredTheme;
+
+document.documentElement.setAttribute("data-theme", initialTheme);
+
+if (themeToggle) {
+ const syncThemeToggle = () => {
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ themeToggle.dataset.mode = isDark ? "dark" : "light";
+ themeToggle.textContent = isDark ? "Light mode" : "Dark mode";
+ themeToggle.setAttribute("aria-pressed", String(isDark));
+ themeToggle.setAttribute("aria-label", isDark ? "Switch to light mode" : "Switch to dark mode");
+ };
+
+ syncThemeToggle();
+
+ themeToggle.addEventListener("click", () => {
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ const nextTheme = isDark ? "light" : "dark";
+ document.documentElement.setAttribute("data-theme", nextTheme);
+ localStorage.setItem(themeStorageKey, nextTheme);
+ syncThemeToggle();
+ });
+}
+
+if (navToggle && navLinks) {
+ navToggle.addEventListener("click", () => {
+ const isExpanded = navToggle.getAttribute("aria-expanded") === "true";
+ navToggle.setAttribute("aria-expanded", String(!isExpanded));
+ navLinks.classList.toggle("open", !isExpanded);
+ });
+
+ navLinks.querySelectorAll("a").forEach((link) => {
+ link.addEventListener("click", () => {
+ navToggle.setAttribute("aria-expanded", "false");
+ navLinks.classList.remove("open");
+ });
+ });
+}
+
+const yearElement = document.querySelector("#year");
+if (yearElement) {
+ yearElement.textContent = String(new Date().getFullYear());
+}
+
+const revealElements = document.querySelectorAll(".reveal");
+
+if ("IntersectionObserver" in window) {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add("show");
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ { threshold: 0.15 }
+ );
+
+ revealElements.forEach((element) => observer.observe(element));
+} else {
+ revealElements.forEach((element) => element.classList.add("show"));
+}
+
+const renderPostList = async (list) => {
+ const response = await fetch(list.dataset.postSource);
+ if (!response.ok) {
+ throw new Error(`Unable to load blog posts: ${response.status}`);
+ }
+
+ const posts = await response.json();
+ const limit = Number.parseInt(list.dataset.postLimit, 10);
+ const visiblePosts = Number.isNaN(limit) ? posts : posts.slice(0, limit);
+ const postPrefix = list.dataset.postPrefix || "";
+
+ visiblePosts.forEach((post) => {
+ const article = document.createElement("article");
+ article.className = "blog-card";
+
+ const heading = document.createElement(list.dataset.postLimit ? "h3" : "h2");
+ const link = document.createElement("a");
+ link.href = `${postPrefix}${post.url}`;
+ link.textContent = post.title;
+ heading.append(link);
+
+ const date = document.createElement("p");
+ date.className = "meta";
+ date.textContent = post.date;
+
+ const summary = document.createElement("p");
+ summary.textContent = post.summary;
+
+ article.append(heading, date, summary);
+ list.append(article);
+ });
+};
+
+document.querySelectorAll("[data-post-list]").forEach((list) => {
+ renderPostList(list).catch((error) => console.error(error));
+});
diff --git a/episode-07/portfolio/styles.css b/episode-07/portfolio/styles.css
new file mode 100644
index 0000000..b6a912a
--- /dev/null
+++ b/episode-07/portfolio/styles.css
@@ -0,0 +1,407 @@
+:root {
+ --bg: #f7f8f6;
+ --surface: #ffffff;
+ --text: #111111;
+ --muted: #2f3b2f;
+ --line: #d8ddd2;
+ --primary: #1f6f3b;
+ --primary-hover: #16562d;
+ --accent: #e46f1b;
+ --accent-hover: #bf5a14;
+ --header-bg: rgba(247, 248, 246, 0.95);
+ --focus-ring: #f6b17b;
+ --skip-bg: #111111;
+ --btn-secondary-bg: #fff1e7;
+ --btn-secondary-border: #f6b17b;
+ --btn-secondary-hover-bg: #ffe5d4;
+ --radius: 14px;
+ --shadow: 0 8px 24px rgba(2, 8, 23, 0.08);
+ --space-1: 0.5rem;
+ --space-2: 0.75rem;
+ --space-3: 1rem;
+ --space-4: 1.5rem;
+ --space-5: 2rem;
+ --space-6: 3rem;
+ --max-width: 1100px;
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --bg: #101211;
+ --surface: #181d1a;
+ --text: #f2f7f0;
+ --muted: #b8c8b7;
+ --line: #2d3931;
+ --primary: #37a15b;
+ --primary-hover: #2a8449;
+ --accent: #ff9b45;
+ --accent-hover: #ffb980;
+ --header-bg: rgba(16, 18, 17, 0.95);
+ --focus-ring: #ffb980;
+ --skip-bg: #000000;
+ --btn-secondary-bg: #2a2017;
+ --btn-secondary-border: #775134;
+ --btn-secondary-hover-bg: #35291d;
+ --shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
+ color: var(--text);
+ background: var(--bg);
+ line-height: 1.6;
+}
+
+a {
+ color: var(--primary);
+}
+
+a:hover {
+ color: var(--primary-hover);
+}
+
+.container {
+ width: min(100% - 2rem, var(--max-width));
+ margin-inline: auto;
+}
+
+.skip-link {
+ position: absolute;
+ top: -100px;
+ left: 1rem;
+ background: var(--skip-bg);
+ color: #fff;
+ padding: 0.5rem 0.75rem;
+ z-index: 9999;
+}
+
+.skip-link:focus {
+ top: 1rem;
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ background: var(--header-bg);
+ border-bottom: 1px solid var(--line);
+ backdrop-filter: blur(8px);
+}
+
+.nav {
+ position: relative;
+ min-height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+}
+
+.nav-controls {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.brand {
+ text-decoration: none;
+ color: var(--text);
+ font-weight: 700;
+}
+
+.nav-toggle {
+ display: none;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--surface);
+ padding: 0.4rem 0.7rem;
+ font: inherit;
+}
+
+.theme-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ border: 1px solid var(--btn-secondary-border);
+ border-radius: 8px;
+ background: var(--btn-secondary-bg);
+ color: var(--accent-hover);
+ padding: 0.4rem 0.7rem;
+ font: inherit;
+ font-weight: 600;
+}
+
+.theme-toggle::before {
+ content: "🌙";
+ line-height: 1;
+}
+
+.theme-toggle[data-mode="dark"]::before {
+ content: "☀️";
+}
+
+.theme-toggle:hover {
+ background: var(--btn-secondary-hover-bg);
+}
+
+.nav-links {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.9rem;
+}
+
+.nav-links a {
+ text-decoration: none;
+ color: var(--text);
+ font-weight: 500;
+}
+
+.nav-links a:focus-visible,
+.btn:focus-visible,
+.nav-toggle:focus-visible,
+.theme-toggle:focus-visible {
+ outline: 3px solid var(--focus-ring);
+ outline-offset: 2px;
+ border-radius: 6px;
+}
+
+.hero {
+ padding-block: var(--space-6);
+}
+
+.eyebrow {
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+ font-size: 0.85rem;
+ margin-bottom: var(--space-2);
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1.2;
+ margin: 0;
+}
+
+.headline {
+ font-size: clamp(1.1rem, 2.8vw, 1.35rem);
+ color: var(--muted);
+ margin: var(--space-2) 0 var(--space-3);
+}
+
+.summary {
+ max-width: 70ch;
+}
+
+.section {
+ padding-block: var(--space-5);
+}
+
+.resume-frame-wrap {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+.resume-frame {
+ display: block;
+ width: 100%;
+ min-height: 900px;
+ border: 0;
+}
+
+h2 {
+ margin-top: 0;
+ margin-bottom: var(--space-4);
+ font-size: clamp(1.5rem, 4vw, 2rem);
+}
+
+h3 {
+ margin: 0 0 var(--space-1);
+ font-size: 1.1rem;
+}
+
+.cards,
+.grid,
+.blog-list {
+ display: grid;
+ gap: var(--space-3);
+}
+
+.cards {
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.grid {
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+}
+
+.card,
+.panel,
+.blog-card {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ padding: var(--space-4);
+ box-shadow: var(--shadow);
+}
+
+.blog-card h2,
+.blog-card h3 {
+ margin-bottom: var(--space-1);
+}
+
+.blog-card h2 {
+ font-size: 1.3rem;
+}
+
+.blog-card p:last-child {
+ margin-bottom: 0;
+}
+
+.blog-actions {
+ margin-top: var(--space-4);
+ font-weight: 600;
+}
+
+.blog-post {
+ max-width: 760px;
+}
+
+.blog-post > * + * {
+ margin-top: var(--space-3);
+}
+
+.meta {
+ color: var(--muted);
+ font-size: 0.95rem;
+ margin-top: 0;
+}
+
+.small {
+ font-size: 0.95rem;
+ color: var(--muted);
+}
+
+.muted {
+ color: var(--muted);
+ font-weight: 500;
+}
+
+.list-clean {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ padding: var(--space-4) var(--space-5);
+ box-shadow: var(--shadow);
+}
+
+.list-clean li + li {
+ margin-top: var(--space-2);
+}
+
+.cta-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ margin-top: var(--space-3);
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 10px;
+ padding: 0.62rem 1rem;
+ text-decoration: none;
+ font-weight: 600;
+ border: 1px solid transparent;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: #fff;
+}
+
+.btn-primary:hover {
+ color: #fff;
+ background: var(--primary-hover);
+}
+
+.btn-secondary {
+ background: var(--btn-secondary-bg);
+ border-color: var(--btn-secondary-border);
+ color: var(--accent-hover);
+}
+
+.btn-secondary:hover {
+ background: var(--btn-secondary-hover-bg);
+ color: var(--accent-hover);
+}
+
+.site-footer {
+ border-top: 1px solid var(--line);
+ margin-top: var(--space-6);
+ padding: var(--space-4) 0;
+ color: var(--muted);
+}
+
+.reveal {
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.45s ease, transform 0.45s ease;
+}
+
+.reveal.show {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+@media (max-width: 900px) {
+ .nav-toggle {
+ display: inline-flex;
+ }
+
+ .nav-links {
+ position: absolute;
+ right: 1rem;
+ top: 62px;
+ width: min(240px, 88vw);
+ padding: var(--space-3);
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ box-shadow: var(--shadow);
+ background: var(--surface);
+ display: none;
+ flex-direction: column;
+ }
+
+ .nav-links.open {
+ display: flex;
+ }
+
+ .resume-frame {
+ min-height: 600px;
+ }
+}