@@ -13,7 +13,8 @@ const reports = (await getCollection('reports'))
1313 bodyClass =" home-page"
1414>
1515 <section class =" minimal-hero" >
16- <div >
16+ <canvas class =" starfield" aria-hidden =" true" ></canvas >
17+ <div class =" hero-copy" >
1718 <div class =" overline" >Accelerated systems engineering</div >
1819 <h1 >Open systems,<br /><span >accelerated.</span ></h1 >
1920 <p class =" intro" >GPU software adaptation and performance research, published with reproducible evidence.</p >
@@ -79,8 +80,109 @@ const reports = (await getCollection('reports'))
7980 </section >
8081</BaseLayout >
8182
83+ <script >
84+ const canvas = document.querySelector<HTMLCanvasElement>('.starfield');
85+ const hero = canvas?.closest<HTMLElement>('.minimal-hero');
86+ const context = canvas?.getContext('2d');
87+
88+ if (canvas && hero && context) {
89+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
90+ let width = 0;
91+ let height = 0;
92+ let stars: { x: number; y: number; radius: number; phase: number; color: string }[] = [];
93+ let animationFrame = 0;
94+
95+ const resize = () => {
96+ window.cancelAnimationFrame(animationFrame);
97+ const bounds = hero.getBoundingClientRect();
98+ const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
99+ width = bounds.width;
100+ height = bounds.height;
101+ canvas.width = Math.round(width * pixelRatio);
102+ canvas.height = Math.round(height * pixelRatio);
103+ context!.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
104+
105+ const count = Math.max(72, Math.round((width * height) / 6200));
106+ stars = Array.from({ length: count }, () => ({
107+ x: Math.random() * width,
108+ y: Math.random() * height,
109+ radius: Math.random() * 1.25 + 0.35,
110+ phase: Math.random() * Math.PI * 2,
111+ color: Math.random() > 0.82 ? '155,234,52' : Math.random() > 0.5 ? '178,220,255' : '232,242,255',
112+ }));
113+ draw(0);
114+ };
115+
116+ const draw = (time: number) => {
117+ context!.clearRect(0, 0, width, height);
118+
119+ // Connect only nearby stars to form sparse, quiet constellations.
120+ for (let i = 0; i < stars.length; i += 1) {
121+ const a = stars[i];
122+ for (let j = i + 1; j < stars.length; j += 1) {
123+ const b = stars[j];
124+ const dx = a.x - b.x;
125+ const dy = a.y - b.y;
126+ const distance = Math.hypot(dx, dy);
127+ if (distance < 92) {
128+ context!.strokeStyle = `rgba(171, 205, 224, ${(1 - distance / 92) * 0.12})`;
129+ context!.lineWidth = 0.6;
130+ context!.beginPath();
131+ context!.moveTo(a.x, a.y);
132+ context!.lineTo(b.x, b.y);
133+ context!.stroke();
134+ }
135+ }
136+ }
137+
138+ stars.forEach((star) => {
139+ const twinkle = reducedMotion.matches ? 0.72 : 0.48 + Math.sin(time * 0.0007 + star.phase) * 0.24;
140+ context!.beginPath();
141+ context!.fillStyle = `rgba(${star.color}, ${twinkle})`;
142+ context!.shadowColor = `rgba(${star.color}, ${twinkle * 0.55})`;
143+ context!.shadowBlur = star.radius * 5;
144+ context!.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
145+ context!.fill();
146+ });
147+ context!.shadowBlur = 0;
148+
149+ // A small periodic meteor adds motion without competing with the headline.
150+ if (!reducedMotion.matches) {
151+ const cycle = (time % 9200) / 9200;
152+ if (cycle > 0.68 && cycle < 0.76) {
153+ const progress = (cycle - 0.68) / 0.08;
154+ const x = width * (0.72 - progress * 0.42);
155+ const y = height * (0.12 + progress * 0.28);
156+ const trail = context!.createLinearGradient(x + 90, y - 58, x, y);
157+ trail.addColorStop(0, 'rgba(155,234,52,0)');
158+ trail.addColorStop(1, 'rgba(206,246,255,0.78)');
159+ context!.strokeStyle = trail;
160+ context!.lineWidth = 1.2;
161+ context!.beginPath();
162+ context!.moveTo(x + 90, y - 58);
163+ context!.lineTo(x, y);
164+ context!.stroke();
165+ }
166+ }
167+
168+ if (!reducedMotion.matches) animationFrame = window.requestAnimationFrame(draw);
169+ };
170+
171+ resize();
172+ const observer = new ResizeObserver(resize);
173+ observer.observe(hero);
174+ reducedMotion.addEventListener('change', () => {
175+ window.cancelAnimationFrame(animationFrame);
176+ draw(0);
177+ });
178+ }
179+ </script >
180+
82181<style >
83- .minimal-hero { min-height: 540px; display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 64px; align-items: end; padding: 74px 0 54px; }
182+ .minimal-hero { position: relative; isolation: isolate; min-height: 540px; display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 64px; align-items: end; padding: 74px 0 54px; overflow: hidden; }
183+ .minimal-hero::before { position: absolute; z-index: -1; inset: -8% -8% 2% 36%; content: ""; pointer-events: none; background: radial-gradient(ellipse at 68% 40%, rgba(45,91,121,.16), transparent 55%), radial-gradient(ellipse at 76% 34%, rgba(155,234,52,.08), transparent 42%); }
184+ .starfield { position: absolute; z-index: -1; inset: 0; width: 100%; height: 100%; pointer-events: none; }
185+ .hero-copy, .profile, .signal-line { position: relative; z-index: 1; }
84186 .overline { color: var(--accent); font: 700 10px/1.4 monospace; letter-spacing: .18em; text-transform: uppercase; }
85187 .minimal-hero h1 { max-width: 900px; margin: 20px 0 24px; font-size: clamp(58px, 8vw, 112px); font-weight: 610; line-height: .88; letter-spacing: -.075em; }
86188 .minimal-hero h1 span { color: var(--muted); }
0 commit comments