Skip to content

Commit f28d64e

Browse files
committed
Fix mission header alignment and load performance
1 parent c3e6f22 commit f28d64e

8 files changed

Lines changed: 128 additions & 69 deletions

File tree

src/components/Header.astro

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,12 @@ import Logo from './Logo.astro';
257257
display: inline-grid;
258258
}
259259
.brand {
260+
gap: 0;
260261
min-width: 0;
261262
}
263+
:global(.brand__logo) {
264+
display: none;
265+
}
262266
.primary-nav {
263267
position: fixed;
264268
top: 4.25rem;
@@ -300,16 +304,6 @@ import Logo from './Logo.astro';
300304
padding: 0.85em 1em;
301305
}
302306
}
303-
304-
@media (max-width: 56rem) {
305-
.brand {
306-
gap: 0;
307-
}
308-
:global(.brand__logo) {
309-
display: none;
310-
}
311-
}
312-
313307
@media (max-width: 28rem) {
314308
.site-header__bar {
315309
min-height: 4rem;

src/components/Hero.astro

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,16 @@
22
import { hero, motto } from '../data/site';
33
import GitHubMark from './GitHubMark.astro';
44
5-
// Precompute the headline as a trusted HTML string. Each word is its own
6-
// reveal target with a staggered delay. (Avoids fragile nested-map JSX.)
7-
// Preserve the final amber phosphor state without animating non-composited text effects.
8-
let wordIndex = 0;
5+
// Precompute the headline as a trusted HTML string. Keep the H1 visible at
6+
// first paint: this text is the LCP candidate, so it must not wait for JS reveal.
97
const lastLineIndex = hero.headlineLines.length - 1;
108
const headlineHtml = hero.headlineLines
119
.map((line, lineIndex) => {
1210
const isLast = lineIndex === lastLineIndex;
1311
const words = line
1412
.map((word) => {
15-
const delay = wordIndex++ * 70;
1613
const innerCls = isLast ? 'hero__word-inner kinetic' : 'hero__word-inner';
17-
return `<span class="hero__word" data-reveal style="--reveal-delay:${delay}ms"><span class="${innerCls}">${word}</span></span>`;
14+
return `<span class="hero__word"><span class="${innerCls}">${word}</span></span>`;
1815
})
1916
.join(' ');
2017
return `<span class="hero__line">${words}</span>`;
@@ -155,7 +152,6 @@ const mottoHalf = Array.from({ length: 4 })
155152
}
156153
:global(.hero__word) {
157154
display: inline-block;
158-
overflow: hidden;
159155
vertical-align: bottom;
160156
}
161157
:global(.hero__word:not(:last-child)) {
@@ -177,15 +173,6 @@ const mottoHalf = Array.from({ length: 4 })
177173
0 0 18px color-mix(in srgb, var(--amber) 55%, transparent),
178174
0 0 4px color-mix(in srgb, var(--amber) 35%, transparent);
179175
}
180-
/* let the amber phosphor bleed past the reveal-mask once the slide is done */
181-
:global(:root.js .hero__line:last-child .hero__word[data-reveal].is-visible) {
182-
overflow: visible;
183-
}
184-
@media (prefers-reduced-motion: reduce) {
185-
:global(.hero__line:last-child .hero__word) {
186-
overflow: visible;
187-
}
188-
}
189176
@media (prefers-reduced-motion: reduce) {
190177
:global(.hero__line:last-child .hero__word-inner) {
191178
text-shadow: none;

src/components/Mascot.astro

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
tasteful fixed-corner companion. Idle bob is motion-gated and
77
settles to a deterministic rest state for visual tests.
88
================================================================= */
9+
import { mascotCompanion } from '../data/site';
10+
911
export interface Props {
1012
/** 'corner' = fixed bottom-left companion; 'inline' = seam ornament. */
1113
variant?: 'corner' | 'inline';
@@ -54,7 +56,14 @@ const cells = pixels
5456
.join('');
5557
---
5658

57-
<div class={`mascot mascot--${variant}`} aria-hidden="true" data-mascot>
59+
<div
60+
class={`mascot mascot--${variant}`}
61+
aria-hidden="true"
62+
data-mascot
63+
data-first-quip={mascotCompanion.firstQuip}
64+
data-cta-quip={mascotCompanion.ctaQuip}
65+
data-idle-quips={JSON.stringify(mascotCompanion.idleQuips)}
66+
>
5867
<svg
5968
class="mascot__sprite"
6069
viewBox="0 0 16 16"

src/data/site.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ export const miniGame = {
7373
'WORKS HERE',
7474
],
7575
winFinale: {
76-
title: 'FUNDED [check]',
77-
lead: '',
78-
detail: '',
79-
stars: '',
76+
title: 'WORK FUNDED [check]',
77+
lead: 'A maintainer is on call.',
78+
detail: 'Triage, fixes, releases.',
79+
stars: 'A written SLA backs it.',
8080
},
8181
gameOverFinales: [
8282
{

src/layouts/BaseLayout.astro

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import '../styles/global.css';
44
import { motto, site, seo } from '../data/site';
55
import Mascot from '../components/Mascot.astro';
66
import pkg from '../../package.json';
7+
import pressStartFontUrl from '@fontsource/press-start-2p/files/press-start-2p-latin-400-normal.woff2?url';
8+
import spaceGroteskFontUrl from '@fontsource-variable/space-grotesk/files/space-grotesk-latin-wght-normal.woff2?url';
79
810
export interface Props {
911
title?: string;
@@ -78,34 +80,64 @@ const analyticsBootstrap = `
7880
7981
const googleTagId = ${JSON.stringify(googleTagId)};
8082
const clarityProjectId = ${JSON.stringify(clarityProjectId)};
83+
let loaded = false;
8184
8285
window.dataLayer = window.dataLayer || [];
8386
window.gtag =
8487
window.gtag ||
8588
function gtag() {
8689
window.dataLayer.push(arguments);
8790
};
91+
window.clarity =
92+
window.clarity ||
93+
function clarity() {
94+
(window.clarity.q = window.clarity.q || []).push(arguments);
95+
};
96+
97+
const events = ['pointerdown', 'keydown', 'touchstart', 'scroll'];
98+
const cleanup = () => {
99+
events.forEach((eventName) => window.removeEventListener(eventName, loadAnalytics, true));
100+
};
101+
const loadAnalytics = () => {
102+
if (loaded) return;
103+
loaded = true;
104+
cleanup();
105+
106+
const googleTag = document.createElement('script');
107+
googleTag.async = true;
108+
googleTag.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(googleTagId);
109+
document.head.appendChild(googleTag);
110+
111+
window.gtag('js', new Date());
112+
window.gtag('config', googleTagId);
113+
114+
const clarityScript = document.createElement('script');
115+
clarityScript.async = true;
116+
clarityScript.src = 'https://www.clarity.ms/tag/' + clarityProjectId;
117+
document.head.appendChild(clarityScript);
118+
};
119+
const scheduleIdleFallback = () => {
120+
window.setTimeout(() => {
121+
if ('requestIdleCallback' in window) {
122+
window.requestIdleCallback(loadAnalytics, { timeout: 2500 });
123+
} else {
124+
window.setTimeout(loadAnalytics, 0);
125+
}
126+
}, 12000);
127+
};
88128
89-
const googleTag = document.createElement('script');
90-
googleTag.async = true;
91-
googleTag.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(googleTagId);
92-
document.head.appendChild(googleTag);
93-
94-
window.gtag('js', new Date());
95-
window.gtag('config', googleTagId);
96-
97-
(function(c, l, a, r, i, t, y) {
98-
c[a] =
99-
c[a] ||
100-
function clarity() {
101-
(c[a].q = c[a].q || []).push(arguments);
102-
};
103-
t = l.createElement(r);
104-
t.async = 1;
105-
t.src = 'https://www.clarity.ms/tag/' + i;
106-
y = l.getElementsByTagName(r)[0];
107-
y.parentNode.insertBefore(t, y);
108-
})(window, document, 'clarity', 'script', clarityProjectId);
129+
events.forEach((eventName) =>
130+
window.addEventListener(eventName, loadAnalytics, {
131+
once: true,
132+
passive: true,
133+
capture: true,
134+
})
135+
);
136+
if (document.readyState === 'complete') {
137+
scheduleIdleFallback();
138+
} else {
139+
window.addEventListener('load', scheduleIdleFallback, { once: true });
140+
}
109141
})();
110142
`.trim();
111143
---
@@ -165,6 +197,8 @@ const analyticsBootstrap = `
165197

166198
<link rel="sitemap" href="/sitemap-index.xml" />
167199
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
200+
<link rel="preload" href={spaceGroteskFontUrl} as="font" type="font/woff2" crossorigin />
201+
<link rel="preload" href={pressStartFontUrl} as="font" type="font/woff2" crossorigin />
168202

169203
<!-- No-flash theme + first-load intro arm: runs before paint -->
170204
<script is:inline>

src/scripts/app.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
MISSION · client interactivity. Progressive, reduced-motion-safe.
33
================================================================= */
44

5-
import { mascotCompanion } from '../data/site';
6-
import { createMascotGame } from './game';
5+
export {};
76

87
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
98
const hashSettleDelays = [80, 350, 1000, 1800, 2800] as const;
@@ -53,6 +52,8 @@ type MissionSoundName =
5352
| 'firework'
5453
| 'restart';
5554

55+
type MascotGame = { launch: () => void };
56+
5657
/* ---------- Scroll reveal ---------- */
5758
function initReveal() {
5859
const items = document.querySelectorAll<HTMLElement>('[data-reveal]');
@@ -478,7 +479,6 @@ function initAnalyticsScrollTracking() {
478479
}
479480
};
480481

481-
requestUpdate();
482482
window.addEventListener('scroll', requestUpdate, { passive: true });
483483
window.addEventListener('resize', requestUpdate, { passive: true });
484484
analyticsScrollCleanup = () => {
@@ -526,16 +526,18 @@ function initMagnetic() {
526526
if (reduceMotion) return;
527527
if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) return;
528528
document.querySelectorAll<HTMLElement>('.hero__cta .btn, .final-cta__cta .btn').forEach((el) => {
529-
let rect = el.getBoundingClientRect();
529+
let rect: DOMRect | null = null;
530530
el.addEventListener('pointerenter', () => {
531531
rect = el.getBoundingClientRect();
532532
});
533533
el.addEventListener('mousemove', (e) => {
534+
rect ??= el.getBoundingClientRect();
534535
const x = e.clientX - (rect.left + rect.width / 2);
535536
const y = e.clientY - (rect.top + rect.height / 2);
536537
el.style.transform = `translate(${x * 0.25}px, ${y * 0.32}px)`;
537538
});
538539
el.addEventListener('mouseleave', () => {
540+
rect = null;
539541
el.style.transform = '';
540542
});
541543
});
@@ -972,6 +974,18 @@ function initMascot() {
972974
// the sprite is decorative (pointer-events:none) — make it clickable
973975
mascot.style.pointerEvents = 'auto';
974976
mascot.style.cursor = 'pointer';
977+
const firstQuip = mascot.dataset.firstQuip ?? '';
978+
const ctaQuip = mascot.dataset.ctaQuip ?? '';
979+
const idleQuips = (() => {
980+
try {
981+
const parsed = JSON.parse(mascot.dataset.idleQuips ?? '[]');
982+
return Array.isArray(parsed)
983+
? parsed.filter((item): item is string => typeof item === 'string')
984+
: [];
985+
} catch {
986+
return [];
987+
}
988+
})();
975989

976990
const canRoam =
977991
!reduceMotion &&
@@ -981,7 +995,7 @@ function initMascot() {
981995
!navigator.webdriver ||
982996
window.__missionMascotRoamingTest === true ||
983997
window.__missionMascotQuipTest === true;
984-
const autoQuips = [mascotCompanion.firstQuip, ...mascotCompanion.idleQuips].filter(Boolean);
998+
const autoQuips = [firstQuip, ...idleQuips].filter(Boolean);
985999
if (navigator.webdriver) window.__missionMascotQuips = autoQuips;
9861000
const speechHoldMs = window.__missionMascotQuipTest === true ? 1000 : 5600;
9871001
const firstQuipDelayMs = window.__missionMascotQuipTest === true ? 120 : 900;
@@ -1056,12 +1070,29 @@ function initMascot() {
10561070
window.setTimeout(() => mascot.classList.remove('is-hop'), 500);
10571071
}
10581072
};
1059-
// The game DOM is built lazily on first launch() — never on load — so it can
1060-
// never surface in screenshots/tests unless a real user clicks the mascot.
1061-
const game = createMascotGame();
1073+
// The game code and DOM are built lazily on first launch() — never on load.
1074+
let game: MascotGame | null = null;
1075+
let gameLoad: Promise<MascotGame> | null = null;
1076+
const getGame = () => {
1077+
if (game) return Promise.resolve(game);
1078+
gameLoad ??= import('./game')
1079+
.then(({ createMascotGame }) => {
1080+
game = createMascotGame();
1081+
return game;
1082+
})
1083+
.catch((error) => {
1084+
gameLoad = null;
1085+
throw error;
1086+
});
1087+
return gameLoad;
1088+
};
10621089
const onMascotClick = () => {
10631090
triggerHop();
1064-
game.launch();
1091+
void getGame()
1092+
.then((loadedGame) => loadedGame.launch())
1093+
.catch(() => {
1094+
/* keep the page usable if the optional game chunk fails */
1095+
});
10651096
};
10661097
mascot.addEventListener('click', onMascotClick);
10671098

@@ -1095,7 +1126,8 @@ function initMascot() {
10951126
syncSurface();
10961127
});
10971128
};
1098-
syncSurface();
1129+
mascot.dataset.overInvert = 'false';
1130+
queueSurfaceSync();
10991131
window.addEventListener('scroll', queueSurfaceSync, { passive: true });
11001132
window.addEventListener('resize', queueSurfaceSync, { passive: true });
11011133

@@ -1147,7 +1179,7 @@ function initMascot() {
11471179
};
11481180
const onCtaEnter = () => {
11491181
triggerHop();
1150-
if (Math.random() < 0.5) speak(mascotCompanion.ctaQuip);
1182+
if (Math.random() < 0.5) speak(ctaQuip);
11511183
};
11521184

11531185
window.addEventListener('pointermove', onPointerMove, { passive: true });

src/styles/fonts.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
@font-face {
44
font-family: 'Space Grotesk Variable';
55
font-style: normal;
6-
font-display: swap;
6+
font-display: fallback;
77
font-weight: 300 700;
88
src: url('@fontsource-variable/space-grotesk/files/space-grotesk-latin-wght-normal.woff2')
99
format('woff2-variations');
@@ -15,7 +15,7 @@
1515
@font-face {
1616
font-family: 'Newsreader Variable';
1717
font-style: normal;
18-
font-display: swap;
18+
font-display: fallback;
1919
font-weight: 200 800;
2020
src: url('@fontsource-variable/newsreader/files/newsreader-latin-wght-normal.woff2')
2121
format('woff2-variations');
@@ -27,7 +27,7 @@
2727
@font-face {
2828
font-family: 'JetBrains Mono Variable';
2929
font-style: normal;
30-
font-display: swap;
30+
font-display: fallback;
3131
font-weight: 100 800;
3232
src: url('@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2')
3333
format('woff2-variations');
@@ -39,7 +39,7 @@
3939
@font-face {
4040
font-family: 'Press Start 2P';
4141
font-style: normal;
42-
font-display: swap;
42+
font-display: fallback;
4343
font-weight: 400;
4444
src: url('@fontsource/press-start-2p/files/press-start-2p-latin-400-normal.woff2') format('woff2');
4545
unicode-range:

0 commit comments

Comments
 (0)