/* === 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; }
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

5
Sessions Completed
6
Key Decisions
1,067
Messages
  • 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