/* === CSS VARIABLES === */
:root {
--bg-base: #3d566e;
--bg-surface: #3d5a7a;
--bg-elevated: #486a8f;
--bg-border: rgba(255,255,255,0.08);
--bg-subtle: #2d4059;
--border-strong: rgba(255,255,255,0.14);
--pillar-1: #8b96e8;
--pillar-1-soft: rgba(139,150,232,0.18);
--pillar-2: #4dbac4;
--pillar-2-soft: rgba(77,186,196,0.18);
--pillar-3: #e0993a;
--pillar-3-soft: rgba(224,153,58,0.18);
--text-primary: #e2e8f0;
--text-body: #e2e8f0;
--text-muted: #a8b8cc;
--font-display: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-body: 'Crimson Pro', Georgia, 'Times New Roman', serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1.05rem;
--text-body: 1.15rem;
--text-md: 1.25rem;
--text-lg: 1.5rem;
--text-xl: clamp(1.8rem, 3vw, 2.75rem);
--text-2xl: clamp(2rem, 4vw, 3.5rem);
--text-3xl: clamp(2.5rem, 5vw, 4rem);
--text-hero: clamp(2.4rem, 5vw, 4.4rem);
--glow-indigo: rgba(139,150,232,0.22);
--glow-cyan: rgba(77,186,196,0.18);
--hover-subtle: rgba(255,255,255,0.04);
--nav-separator: rgba(120,144,168,0.30);
}
[data-theme="light"] {
--bg-base: #DDEAF7;
--bg-surface: #F4F7FB;
--bg-elevated: #FFFFFF;
--bg-border: rgba(30,48,72,0.10);
--bg-subtle: #a8c4e0;
--border-strong: rgba(30,48,72,0.18);
--text-primary: #1e3048;
--text-body: #2d4a66;
--text-muted: #4a6080;
--hover-subtle: rgba(30,48,72,0.04);
--nav-separator: rgba(30,48,72,0.18);
--glow-indigo: rgba(101,112,200,0.10);
--glow-cyan: rgba(58,157,166,0.10);
--pillar-1: #6570c8;
--pillar-1-soft: rgba(101,112,200,0.18);
--pillar-2: #3a9da6;
--pillar-2-soft: rgba(58,157,166,0.18);
--pillar-3: #c07a28;
--pillar-3-soft: rgba(192,122,40,0.18);
--text-muted: #4a6080;
}
body, .nav-mobile-overlay,
h1, h2, h3, p, li, span, a {
transition:
background-color 0.25s ease,
color 0.20s ease,
border-color 0.20s ease;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg-base);
color: var(--text-body);
font-family: var(--font-body);
line-height: 1.9;
overflow-x: hidden;
}
/* === NAV === */
.site-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
display: flex; align-items: center; justify-content: space-between;
padding: 0 64px; height: 54px;
background: rgba(61,86,110,0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255,255,255,0.06);
transition: background 0.4s ease, backdrop-filter 0.4s ease, border-color 0.4s ease;
}
[data-theme="light"] .site-nav { background: rgba(255,255,255,0.55); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,0.65); box-shadow: 0 2px 16px rgba(30,48,72,0.08); }
[data-theme="light"] .site-nav.scrolled { background: rgba(255,255,255,0.70); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); box-shadow: 0 4px 24px rgba(30,48,72,0.12); }
[data-theme="dark"] .site-nav { background: rgba(35,52,72,0.75); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,0.08); box-shadow: 0 2px 16px rgba(0,0,0,0.30); }
[data-theme="dark"] .site-nav.scrolled { background: rgba(30,45,65,0.88); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); box-shadow: 0 4px 24px rgba(0,0,0,0.40); }
.site-nav.scrolled { background: rgba(61,86,110,0.75); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom-color: rgba(255,255,255,0.10); }
.nav-brand { text-decoration: none; display: flex; flex-direction: column; gap: 2px; }
.nav-brand-name { font-family: var(--font-display); font-size: var(--text-md); font-weight: 700; color: var(--text-primary); letter-spacing: 0.18em; text-transform: uppercase; }
.nav-brand-sub { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted); letter-spacing: 0.1em; text-transform: uppercase; }
.nav-links { display: flex; align-items: center; gap: 40px; list-style: none; margin: 0; padding: 0; }
.nav-link { font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--text-body); text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; position: relative; }
.nav-link:hover, .nav-link.active { color: var(--text-primary); }
.nav-link.active::after { content: ''; position: absolute; bottom: -4px; left: 0; right: 0; height: 3px; background: var(--pillar-2); border-radius: 1px; }
.nav-hamburger { display: none; flex-direction: column; gap: 5px; background: none; border: none; cursor: pointer; padding: 8px; }
.nav-hamburger span { display: block; width: 22px; height: 2px; background: var(--text-primary); transition: transform 0.3s ease, opacity 0.3s ease; }
.nav-hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger.open span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.nav-hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-mobile-overlay {
position: fixed; inset: 0; z-index: 999;
background: var(--bg-base); display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transform: translateY(-12px); transition: opacity 0.3s ease, transform 0.3s ease;
}
.nav-mobile-overlay.open { opacity: 1; pointer-events: all; transform: translateY(0); }
.nav-mobile-links { list-style: none; text-align: center; padding: 0; }
.nav-mobile-links li { margin: 24px 0; }
.nav-mobile-links a { font-family: var(--font-display); font-size: 2rem; font-weight: 700; color: var(--text-muted); text-decoration: none; transition: color 0.2s; }
.nav-mobile-links a:hover { color: var(--text-primary); }
.mobile-cta { color: var(--pillar-2) !important; }
@media (max-width: 768px) {
.site-nav { padding: 0 24px; }
.nav-links, .nav-cta { display: none; }
.nav-hamburger { display: flex; }
}
/* Nav pipe separators */
.nav-links { gap: 40px !important; }
.nav-brand-sub { display: none !important; }
/* === REVEAL === */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.5s ease, transform 0.5s ease; }
.reveal.visible { opacity: 1; transform: none; }
/* === PAGE HERO === */
.page-hero {
position: relative;
padding: 96px 80px 72px;
overflow: hidden;
border-bottom: 1px solid rgba(255,255,255,0.08);
background: var(--bg-subtle) !important;
}
.hero-lines {
position: absolute; inset: 0; pointer-events: none; overflow: hidden;
}
.hero-line {
position: absolute; height: 1px;
background: linear-gradient(90deg, transparent, rgba(58,157,166,0.18), rgba(101,112,200,0.18), transparent);
animation: lineSweep linear infinite;
}
@keyframes lineSweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(100vw); }
}
.hero-glow {
position: absolute; top: -60px; right: 10%;
width: 500px; height: 500px;
background: radial-gradient(ellipse, rgba(6,182,212,0.07) 0%, transparent 70%);
pointer-events: none;
}
.hero-eyebrow {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--pillar-2); letter-spacing: 0.22em;
text-transform: uppercase; margin-bottom: 20px;
display: block;
opacity: 0;
animation: fadeUp 0.7s cubic-bezier(0.16,1,0.3,1) 0.1s both;
}
.page-hero h1 {
font-family: var(--font-display);
font-size: clamp(2.2rem, 4.5vw, 4rem);
font-weight: 600;
color: var(--text-primary);
line-height: 1.1;
letter-spacing: 0.01em;
max-width: 800px;
margin-bottom: 20px;
opacity: 0;
animation: fadeUp 0.7s cubic-bezier(0.16,1,0.3,1) 0.2s both;
}
.page-hero-sub {
font-family: var(--font-body); font-size: var(--text-md);
color: var(--text-body); max-width: 640px;
line-height: 1.9; margin-bottom: 48px;
opacity: 0;
animation: fadeUp 0.7s cubic-bezier(0.16,1,0.3,1) 0.32s both;
}
.hero-metrics {
display: flex; gap: 48px; flex-wrap: wrap;
opacity: 0;
animation: fadeUp 0.7s cubic-bezier(0.16,1,0.3,1) 0.44s both;
}
.hero-metric {
display: flex; flex-direction: column; gap: 4px;
}
.hero-metric-val {
font-family: var(--font-mono); font-size: var(--text-lg);
font-weight: 500; color: var(--pillar-2); letter-spacing: 0;
}
.hero-metric-label {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--text-muted); letter-spacing: 0.1em; text-transform: uppercase;
}
.hero-tags {
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 32px;
opacity: 0;
animation: fadeUp 0.7s cubic-bezier(0.16,1,0.3,1) 0.38s both;
}
.hero-tag {
font-family: var(--font-mono); font-size: var(--text-sm);
padding: 4px 12px; border: 1px solid var(--bg-border);
color: var(--text-muted); letter-spacing: 0.04em;
}
.hero-status {
display: inline-flex; align-items: center; gap: 8px;
font-family: var(--font-mono); font-size: var(--text-sm);
color: #fbbf24; background: rgba(251,191,36,0.10);
border: 1px solid rgba(251,191,36,0.25);
padding: 6px 14px; margin-bottom: 28px; letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-status::before {
content: ''; width: 7px; height: 7px; border-radius: 50%;
background: #fbbf24; flex-shrink: 0;
animation: pulseDot 2s ease-in-out infinite;
}
@keyframes pulseDot {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-parallels {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--text-muted); letter-spacing: 0.04em;
margin-top: 24px; line-height: 1.8;
}
@media (max-width: 768px) {
.page-hero { padding: 64px 24px 48px; }
.hero-metrics { gap: 28px; }
}
/* === CASE STUDY CONTENT === */
.cs-section {
padding: 80px 80px;
border-bottom: 1px solid var(--bg-border);
}
.cs-section:last-of-type { border-bottom: none; }
.cs-section-dark { background: var(--bg-surface); }
.cs-inner {
max-width: 1000px;
margin: 0 auto;
}
.cs-label {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--pillar-2); letter-spacing: 0.22em;
text-transform: uppercase; margin-bottom: 16px; display: block;
}
.cs-heading {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
color: var(--text-primary);
margin-bottom: 32px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.cs-body {
font-family: var(--font-body);
font-size: var(--text-base);
color: var(--text-body);
line-height: 1.9;
max-width: 800px;
}
.cs-body p { margin-bottom: 20px; }
.cs-body strong { color: var(--text-primary); font-weight: 600; }
.cs-body code {
font-family: var(--font-mono);
font-size: 0.85rem;
background: rgba(255,255,255,0.06);
padding: 2px 8px;
border: 1px solid var(--bg-border);
color: var(--pillar-2);
}
/* === TECH STACK === */
.stack-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32px;
margin-top: 32px;
}
.stack-category {
background: var(--bg-surface);
border: 1px solid var(--bg-border);
padding: 28px 32px;
}
.stack-cat-label {
font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--pillar-2); letter-spacing: 0.2em;
text-transform: uppercase; margin-bottom: 16px;
}
.stack-tags {
display: flex; flex-wrap: wrap; gap: 8px;
}
.stack-tag {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--text-muted); background: var(--bg-elevated);
border: 1px solid var(--bg-border);
padding: 5px 12px; letter-spacing: 0.04em;
}
@media (max-width: 768px) {
.stack-grid { grid-template-columns: 1fr; }
.cs-section { padding: 48px 24px; }
}
/* === ADR CARDS === */
.adr-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-top: 32px;
}
.adr-card {
background: var(--bg-surface);
border: 1px solid var(--bg-border);
border-left: 4px solid var(--pillar-2);
padding: 28px 28px;
transition: transform 0.3s, box-shadow 0.3s;
}
.adr-card:hover {
transform: translateY(-4px);
box-shadow: 2px 16px 40px rgba(58,157,166,0.15);
}
.adr-card:nth-child(2) { border-left-color: var(--pillar-1); }
.adr-card:nth-child(2):hover { box-shadow: 2px 16px 40px rgba(101,112,200,0.15); }
.adr-decision {
font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--text-muted); letter-spacing: 0.15em;
text-transform: uppercase; margin-bottom: 8px;
}
.adr-title {
font-family: var(--font-display); font-size: var(--text-md);
font-weight: 700; color: var(--text-primary);
margin-bottom: 12px; line-height: 1.3;
}
.adr-desc {
font-family: var(--font-body); font-size: var(--text-base);
color: var(--text-body); line-height: 1.7;
}
.adr-desc strong { color: var(--text-primary); }
@media (max-width: 768px) {
.adr-grid { grid-template-columns: 1fr; }
}
/* === TIMELINE === */
.timeline {
margin-top: 40px;
position: relative;
}
.timeline::before {
content: '';
position: absolute; left: 0; top: 0; bottom: 0;
width: 1px;
background: linear-gradient(to bottom, var(--pillar-2), var(--pillar-1), var(--pillar-3), transparent);
}
.timeline-item {
padding-left: 32px;
margin-bottom: 40px;
position: relative;
}
.timeline-item::before {
content: '';
position: absolute; left: -5px; top: 6px;
width: 11px; height: 11px;
border-radius: 50%;
background: var(--pillar-2);
border: 2px solid var(--bg-base);
}
.timeline-item:nth-child(2)::before { background: var(--pillar-1); }
.timeline-item:nth-child(3)::before { background: var(--pillar-3); }
.timeline-item:nth-child(4)::before { background: var(--pillar-2); }
.tl-label {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--text-muted); letter-spacing: 0.12em;
text-transform: uppercase; margin-bottom: 4px;
}
.tl-title {
font-family: var(--font-display); font-size: var(--text-lg);
font-weight: 700; color: var(--text-primary);
margin-bottom: 4px;
}
.tl-meta {
font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--pillar-2); letter-spacing: 0.1em; margin-bottom: 12px;
}
.tl-desc {
font-family: var(--font-body); font-size: var(--text-base);
color: var(--text-body); line-height: 1.7;
}
/* === OUTCOME === */
.outcome-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-top: 32px;
}
.outcome-card {
background: var(--bg-surface);
border: 1px solid var(--bg-border);
padding: 28px 28px;
text-align: center;
}
.outcome-val {
font-family: var(--font-display);
font-size: clamp(2rem, 3vw, 2.8rem);
font-weight: 800;
color: var(--pillar-2);
line-height: 1;
margin-bottom: 8px;
}
.outcome-card:nth-child(2) .outcome-val { color: var(--pillar-1); }
.outcome-card:nth-child(3) .outcome-val { color: var(--pillar-3); }
.outcome-label {
font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--text-muted); letter-spacing: 0.15em;
text-transform: uppercase;
}
.outcome-list {
margin-top: 32px;
list-style: none;
}
.outcome-list li {
font-family: var(--font-body); font-size: var(--text-base);
color: var(--text-body);
padding: 12px 0;
border-bottom: 1px solid var(--bg-border);
display: flex; align-items: center; gap: 12px;
}
.outcome-list li::before {
content: '◆';
color: var(--pillar-2);
font-size: 0.6rem;
flex-shrink: 0;
}
.outcome-list li:last-child { border-bottom: none; }
@media (max-width: 768px) {
.outcome-grid { grid-template-columns: 1fr; }
}
/* === RELATED PROJECTS === */
.related-section {
padding: 80px 80px 120px;
background: var(--bg-base);
}
.related-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-top: 32px;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.related-card {
background: var(--bg-surface);
border: 1px solid var(--bg-border);
padding: 32px;
text-decoration: none;
display: flex;
flex-direction: column;
transition: border-color 0.3s, transform 0.3s, box-shadow 0.3s;
}
.related-card:hover {
border-color: var(--pillar-2);
transform: translateY(-4px);
box-shadow: 2px 16px 40px rgba(58,157,166,0.12);
}
.related-direction {
font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--text-muted); letter-spacing: 0.15em;
text-transform: uppercase; margin-bottom: 8px;
}
.related-title {
font-family: var(--font-display); font-size: var(--text-md);
font-weight: 700; color: var(--text-primary);
margin-bottom: 8px;
}
.related-desc {
font-family: var(--font-body); font-size: var(--text-base);
color: var(--text-body); line-height: 1.6;
}
.related-arrow {
font-family: var(--font-mono); font-size: var(--text-sm);
color: var(--pillar-2); margin-top: auto; padding-top: 16px;
letter-spacing: 0.08em;
}
@media (max-width: 768px) {
.related-grid { grid-template-columns: 1fr; }
.related-section { padding: 48px 24px 80px; }
}
/* === FOOTER === */
.site-footer {
background: var(--bg-surface);
border-top: 1px solid var(--bg-border);
padding: 64px 80px 32px;
}
.footer-inner {
display: grid; grid-template-columns: 1.5fr 2fr 0.5fr;
gap: 64px; align-items: start;
padding-bottom: 48px; border-bottom: 1px solid var(--bg-border);
margin-bottom: 32px;
}
.footer-brand { display: flex; flex-direction: column; gap: 6px; }
.footer-name { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; color: var(--text-primary); }
.footer-title { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--pillar-2); text-transform: uppercase; letter-spacing: 0.12em; }
.footer-location { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-body); margin-top: 8px; }
.footer-nav { display: flex; gap: 48px; }
.footer-nav-col { display: flex; flex-direction: column; gap: 12px; }
.footer-nav-heading { font-family: var(--font-ui); font-size: var(--text-sm); font-weight: 700; color: var(--text-body); text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 4px; }
.footer-nav-col a { font-family: var(--font-body); font-size: var(--text-base); color: var(--text-muted); text-decoration: none; transition: color 0.2s; }
.footer-nav-col a:hover { color: var(--text-primary); }
.footer-social { display: flex; flex-direction: column; gap: 12px; align-items: flex-end; }
.footer-social-link {
width: 36px; height: 36px; border: 1px solid var(--bg-border);
display: flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted);
text-decoration: none; text-transform: lowercase;
transition: border-color 0.2s, color 0.2s;
}
.footer-social-link:hover { border-color: var(--pillar-2); color: var(--pillar-2); }
.footer-bottom {
display: flex; justify-content: space-between; align-items: center;
font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-body);
}
.footer-tagline { font-style: italic; opacity: 0.85; }
@media (max-width: 768px) {
.site-footer { padding: 48px 24px 24px; }
.footer-inner { grid-template-columns: 1fr; gap: 32px; }
.footer-nav { flex-direction: column; gap: 32px; }
.footer-social { flex-direction: row; align-items: flex-start; }
.footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
}
p, li { font-size: var(--text-body); letter-spacing: 0.01em; }
h2, h3, h4, .cs-heading, .card-title, .page-title {
text-transform: uppercase;
letter-spacing: 0.05em !important;
}
h1 { text-transform: uppercase; }
.btn-primary, .btn-outline, .nav-cta,
a.btn-primary, a.btn-outline,
input, textarea, select {
border-radius: 0 !important;
}
.nav-cta {
background: transparent !important;
border: 1px solid var(--pillar-2) !important;
color: var(--pillar-2) !important;
}
.nav-cta:hover {
background: var(--pillar-2) !important;
color: var(--bg-base) !important;
transform: none !important;
}
/* === SVG DIAGRAM === */
.diagram-wrap {
position: relative;
margin: 48px auto;
max-width: 900px;
background: var(--bg-surface);
border: 1px solid var(--bg-border);
padding: 24px;
}
.diagram-svg {
width: 100%;
height: auto;
display: block;
}
.diagram-svg text {
font-family: 'JetBrains Mono', monospace;
}
/* Pulsing orbit rings */
@keyframes orbitSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes orbitPulse {
0%, 100% { opacity: 0.25; }
50% { opacity: 0.5; }
}
@keyframes planetGlow {
0%, 100% { r: 6; opacity: 0.6; }
50% { r: 8; opacity: 1; }
}
/* Theatre.js playhead */
@keyframes playheadMove {
0% { offset-distance: 0%; }
100% { offset-distance: 100%; }
}
@keyframes playheadPulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* Frame capture */
@keyframes frameFlash {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
@keyframes frameCapture {
0% { opacity: 0; transform: translateY(0); }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; transform: translateY(-30px); }
}
/* Data flow particles on connections */
@keyframes flowDot {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { offset-distance: 100%; opacity: 0; }
}
.diagram-caption {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-muted);
text-align: center;
padding: 16px 0 0;
letter-spacing: 0.04em;
}
/* === DARK THEME FIXES === */
[data-theme="dark"] .adr-card,
[data-theme="dark"] .outcome-card,
[data-theme="dark"] .stack-category,
[data-theme="dark"] .related-card,
[data-theme="dark"] .diagram-wrap {
box-shadow: 2px 6px 28px rgba(0,0,0,0.40), 0 1px 0 rgba(255,255,255,0.06) inset;
}
.cs-section-dark {
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
}
/* === DIAGRAM ANIMATIONS (ENHANCED) === */
@keyframes seqStep {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes seqArrow {
to { stroke-dashoffset: -30; }
}
@keyframes glowPulse {
0%, 100% { filter: drop-shadow(0 0 3px rgba(77,186,196,0.4)); }
50% { filter: drop-shadow(0 0 12px rgba(77,186,196,0.9)); }
}
@keyframes floatUp {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
@keyframes nodeGlow {
0%, 100% { stroke-opacity: 0.3; }
50% { stroke-opacity: 1; }
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes progressFill {
from { width: 0%; }
to { width: 100%; }
}
@keyframes spinSlow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes dashFlowRev {
to { stroke-dashoffset: 60; }
}
.diag-label {
font-family: 'JetBrains Mono', monospace;
font-size: 8px;
fill: var(--text-muted);
text-anchor: middle;
letter-spacing: 0.08em;
}
.diag-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 10px;
fill: var(--text-primary);
text-anchor: middle;
font-weight: 700;
}
.diag-step {
font-family: 'JetBrains Mono', monospace;
font-size: 7px;
fill: var(--pillar-2);
text-anchor: middle;
}
.seq-arrow {
stroke-dasharray: 6 4;
animation: seqArrow 1s linear infinite;
}
.seq-dashed {
stroke-dasharray: 4 5;
animation: dashFlowRev 1.5s linear infinite;
}
.deploy-node {
animation: floatUp 3s ease-in-out infinite;
}
.glow-node {
animation: glowPulse 2.5s ease-in-out infinite;
}
.step-circle {
animation: seqStep 2s ease-in-out infinite;
}
.node-pulse {
animation: nodeGlow 2s ease-in-out infinite;
}
Vikram
Builder Case Study
Stale (1 month)
3D Experiments & Animation
Sandbox for solar-system tours, Theatre.js animations, forest-census visualizations — Three.js, Theatre.js, and HyperFrames rendering pipeline.
Three.js
Theatre.js
HyperFrames
GSAP
Industry parallels: AI-assisted 3D (Runway ML) · Programmatic animation (Theatre.js)
5
Sessions
1,067
Messages
6
Key Decisions
2
Rendered Scenes
Problem Statement
The Programmatic 3D Rendering Gap
Creating 3D animated videos traditionally requires either a 3D artist wielding Blender or Maya, or complex game engine programming. For data visualizations, product demos, and scientific simulations, neither extreme is ideal — the artist pipeline is labor-intensive and hard to version-control, while game engines are overkill for linear narrative content.
The sweet spot is programmatic 3D : write code, get animation. Three.js brings WebGL rendering to the browser with a rich scene graph; Theatre.js adds a professional animation timeline with keyframes, sequencing, and easing curves; HyperFrames bridges the gap between real-time rendering and offline video output by capturing individual frames and assembling them into a playable file.
The exploration question: can a Three.js scene be driven entirely by a Theatre.js timeline and rendered losslessly to video through HyperFrames? The answer scales from a solar-system tour to a forest-census data visualization — two radically different scenes sharing the same pipeline architecture.
Approach & Architecture
Three.js → Theatre.js → HyperFrames → Video
The pipeline is a linear frame-processing chain. Three.js constructs the 3D scene graph — meshes, lights, cameras, and materials for solar-system bodies or forest-census point clouds. Theatre.js drives every animated property through a declarative timeline: keyframes on position, rotation, scale, and material uniforms with configurable easing. HyperFrames captures each frame from the WebGL canvas at a target resolution and framerate, then assembles the sequence into an MP4 video file. GSAP supplements Theatre.js for simpler timeline orchestration where full keyframe editing is unnecessary.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1920/1080, 0.1, 1000);
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
new THREE.MeshStandardMaterial({ color: 0xfbbf24 })
);
scene.add(sphere);
const timeline = new Theatre.Timeline("solar-system");
timeline.add(sphere.position, { x: 0, y: 0, z: -10 }); // keyframe 0
timeline.add(sphere.position, { x: 5, y: 0, z: -10 }); // keyframe 1
timeline.play();
THREE.JS SCENE
Camera · Lights · Materials
THEATRE.JS
Position X
Rotation Y
Scale
Opacity
Keyframes · Curves · Easing
HYPERFRAMES
Frame 0042
Frame Capture → Video
MP4
scene graph
timeline props
frame stream
Scene data
Animated properties
Captured frames
Linear pipeline: Three.js scene graph → Theatre.js keyframe timeline → HyperFrames frame capture → MP4 video output. Animated elements show planet orbit motion, timeline playhead advancing, and frames being captured sequentially.
Component Interaction Diagram
Three.js ↔ Theatre.js ↔ HyperFrames Renderer
3D PIPELINE COMPONENT MAP
Solid = sync call · Dashed = event/async
THREE.JS SCENE
Scene Graph
Camera · Lights · Mesh
Materials
Shading · Textures
WebGL Render
Frame Buffer
GPU Compute
THEATRE.JS
Timeline
Keyframes · Curves
Animation State
Sequences
Cues
Events · Triggers
Audio Cues
HYPERFRAMES
Frame Buffer
Pixel Pipeline
Depth Compositing
Multi-plane
Render Queue
Batch Processing
Export
OUTPUT
Video File
.mp4 · .webm
Depth Map
.exr · .png
Metadata
Camera Track
bind
event
frame
done
write
Depth Buffer → Compositing
Sequence Diagram
Frame Render → Capture → Assemble
FRAME RENDER SEQUENCE
SCENE SETUP
Three.js Scene
Camera · Lighting
1
ANIMATE
Theatre.js Timeline
Keyframe Interp
2
RENDER FRAME
WebGL → Buffer
HyperFrames
3
CAPTURE
Frame Grab
Encode
4
Loop: N frames → Video
FRAME PIPELINE DETAIL
Depth Pass
Depth Anything V2
Reconstruction
3D Point Cloud
Volumetric
NeRF-style Render
Compositing
Multi-plane Merge
N
Step
Sequence
Stage (floating)
Technology Stack
3D, Animation & Rendering
3D Engine
Three.js (WebGL)
Scene Graph
Camera System
PBR Materials
Animation System
Theatre.js
Keyframe Timeline
Easing Curves
Sequencing
Rendering Pipeline
HyperFrames
Frame Capture
Video Assembly
Lossless Output
Supplementary
GSAP
Timeline Control
WebGL Canvas
Architecture Decisions
Key Design Records
ADR-001
Three.js over Unity
Lighter weight, browser-native, Theatre.js has first-class Three.js support. Unity would require a standalone build target, a heavier runtime, and a fundamentally different development loop. Three.js runs in the same browser context as Theatre.js and HyperFrames, enabling instant iteration without compile steps. The WebGL renderer is sufficient for the visual complexity of solar-system tours and forest-census point clouds.
ADR-002
HyperFrames over Puppeteer Screenshot
Purpose-built for high-fidelity frame capture at target resolutions. Puppeteer's page.screenshot is designed for single captures and PDF generation — not sequencing thousands of frames into a coherent video. HyperFrames handles frame pacing, resolution consistency, and video assembly automatically. Its programmatic scene assembly API also enables dynamic scene composition per frame, which Puppeteer would require manual orchestration to achieve.
ADR-003
GSAP over native requestAnimationFrame — battle-tested timeline control, cross-browser consistency, easing functions built-in
Battle-tested, cross-browser, rich easing library. Native requestAnimationFrame requires manual delta-time tracking, custom easing implementations, and per-animation state management. GSAP provides a declarative timeline API with built-in easing curves (Power, Elastic, Back, Bounce), automatic cross-browser normalization of transform origins and property prefixes, and a scrubbable timeline that simplifies debugging complex multi-object animations. For Theatre.js supplemental animation work where full keyframe editing is unnecessary, GSAP reduces animation code by ~70% compared to raw rAF loops. The library is 28KB gzipped — acceptable for a development-focused pipeline where the output is video, not page load performance.
Session Timeline
Development Journey
Session 1
Solar System Animation via HyperFrames
870 messages · Core pipeline
Building and rendering a Three.js solar system to video — sun with emissive glow, orbiting planets with orbital paths, camera flythrough driven by Theatre.js timeline, and frame capture via HyperFrames. Proved the core pipeline works end-to-end.
Session 2
Forest Census 3D Scene
169 messages · Integration
Integrating HyperFrames + Theatre.js with the threejs-forest-census visualization. Tree canopies rendered as Three.js point clouds, with Theatre.js driving camera orbit and canopy opacity animations. GSAP timeline testing for supplementary controls.
Session 3
Explore threejs-forest-census Codebase
13 messages · Discovery
Deep-dive into the threejs-forest-census repository — understanding the data format (tree positions, canopy radii, species classification), the Three.js geometry generation pipeline, and how to retrofit Theatre.js timelines onto existing animation loops.
Session 4
Explore Theatre.js Repo
8 messages · Research
Evaluating Theatre.js internals — how the studio panel serializes timelines, the Three.js extension object API, and the sequence-to-video export pathway. Identified the @theatrejs/studio and @theatrejs/r3f packages as key integration points.
Outcome & Impact
Results & Industry Alignment
Working pipeline: Three.js → Theatre.js → HyperFrames → video — proven across two distinct scene types (solar system, forest census)
Industry alignment: Runway ML / Gen-3 AI-assisted 3D — programmatic 3D animation shares the same goal of reducing manual artist labour through procedural generation and timeline-driven output
Industry alignment: Theatre.js programmatic animation mirrors the direction of real-time animation tools like Rive and Lottie — declarative timelines driving native rendering engines
Solar system tour and forest census visualization both rendered to video through the same pipeline architecture