Static portfolio site for ctw.studio. No build framework — plain HTML, CSS, and vanilla JS.
- HTML/CSS/JS — Static pages, no framework
- Tailwind CSS — Utility-first styling via CLI-generated static CSS (no CDN)
- Three.js — Blueprint background effects (loaded async)
- Anime.js — Entry animations on landing page (loaded async)
- Google Fonts — Inter typeface (non-render-blocking)
- Vercel — Hosting and deployment
ctw.studio/
├── index.html # Landing page
├── tailwind.css # Generated Tailwind CSS (do not edit directly)
├── tailwind.config.js # Tailwind configuration
├── portfolio/
│ ├── index.html # Portfolio grid page
│ ├── portfolio.css # Portfolio styles + utility classes
│ ├── projects.js # Project data
│ └── projects/ # Project media assets
├── favicon.png
└── og-image.png
Edit ctw.studio/portfolio/projects.js. Each project object supports these fields:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | URL-friendly slug (used in #hash routing) |
title |
string | yes | Project name |
client |
string | yes | Client or institution name |
category |
string | yes | Short category label (e.g. "Climate Science") |
headline |
string | yes | Detail page headline |
description |
string | yes | Multi-paragraph description (template literal) |
coverImage |
string|null | yes | Path to cover image (relative to portfolio/), or null for gradient |
gradientFrom |
string | - | Gradient start color (when coverImage is null) |
gradientTo |
string | - | Gradient end color (when coverImage is null) |
gridSpan |
number | yes | Grid width: 1 (1/3), 3 (1/2), or 4 (2/3) |
liveUrl |
string|null | - | Live site URL |
repoUrl |
string | - | GitHub repository URL |
blogUrl |
string | - | Related blog post URL |
tags |
string[] | yes | Technology/topic tags |
institution |
string|null | - | Associated institution badge |
gallery |
array | yes | Media items for detail view (see below) |
{ type: 'image', src: 'projects/my-project/img.avif', caption: 'Optional caption' }
{ type: 'video', src: 'projects/my-project/demo.mp4', caption: 'Optional caption' }
{ type: 'pair', src: 'projects/my-project/a.avif', src2: 'projects/my-project/b.avif', caption: 'Optional caption' }- Use AVIF format for all images (convert with
avifenc --min 20 --max 40 input.png output.avif) - Place media in
ctw.studio/portfolio/projects/<project-id>/ - Cover images are displayed in the grid; gallery images appear in the detail view
- Images use
loading="lazy",decoding="async", and CSS fade-in on load - The first 3 grid cards use
fetchpriority="high"instead of lazy loading
The landing page (ctw.studio/index.html) uses a pre-built tailwind.css file generated by the Tailwind CLI. The portfolio page uses hand-written utility classes in portfolio.css instead.
After adding or changing Tailwind classes in index.html, rebuild:
cd ctw.studio
bunx tailwindcss -i <(echo '@tailwind base; @tailwind components; @tailwind utilities;') -o tailwind.css --content './index.html' --minifyAll third-party scripts are loaded asynchronously to avoid render-blocking:
- Tailwind CSS — Static pre-built CSS file (~20KB) instead of the 124KB CDN JIT compiler
- Three.js & Anime.js — Dynamically injected via
document.createElement('script')after initial render - Google Fonts — Loaded with
media="print" onload="this.media='all'"pattern
No build tool (Vite, Webpack, etc.) is needed. The Tailwind CLI handles CSS generation and minification. Inline scripts are small enough that bundling provides no meaningful benefit.
MIT