Skip to content

Commit a2fd71a

Browse files
ManaiakalaniCopilot
andcommitted
feat: add custom 404 page with glitch effect and table flip easter egg
- Glitch-animated 404 code with gradient text - ASCII art box, witty lost message, path echo - Table flip easter egg (click to flip/unflip) - Full nav, theme toggle, GeoCities mode support - GeoCities overrides: blinking red 404, green text, retro button - Azure SWA config to route 404s to custom page - Playwright tests for 404 page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df330d8 commit a2fd71a

File tree

3 files changed

+263
-0
lines changed

3 files changed

+263
-0
lines changed

404.html

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>404 — Maximilian Stein</title>
7+
<link rel="icon" type="image/png" href="favicon.png">
8+
<link rel="preconnect" href="https://fonts.googleapis.com">
9+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10+
<link href="https://fonts.googleapis.com/css2?family=Doto:wght@400;700&display=swap" rel="stylesheet">
11+
<link rel="stylesheet" href="style.css">
12+
<link rel="stylesheet" href="geocities.css">
13+
<script>
14+
(function() {
15+
var t = localStorage.getItem('theme');
16+
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
17+
document.documentElement.setAttribute('data-theme', 'dark');
18+
}
19+
if (localStorage.getItem('geocities') === 'true') {
20+
document.documentElement.setAttribute('data-geocities', 'true');
21+
}
22+
})();
23+
</script>
24+
<style>
25+
.four-oh-four {
26+
min-height: 70vh;
27+
display: flex;
28+
flex-direction: column;
29+
align-items: center;
30+
justify-content: center;
31+
text-align: center;
32+
padding: 2rem;
33+
}
34+
.glitch-code {
35+
font-family: 'Doto', monospace;
36+
font-size: clamp(6rem, 20vw, 12rem);
37+
font-weight: 700;
38+
background: var(--gradient);
39+
-webkit-background-clip: text;
40+
background-clip: text;
41+
-webkit-text-fill-color: transparent;
42+
line-height: 1;
43+
margin-bottom: 0.5rem;
44+
animation: glitch-shake 3s infinite;
45+
position: relative;
46+
}
47+
.glitch-code::after {
48+
content: '404';
49+
position: absolute;
50+
left: 2px;
51+
top: 2px;
52+
background: var(--gradient);
53+
-webkit-background-clip: text;
54+
background-clip: text;
55+
-webkit-text-fill-color: transparent;
56+
opacity: 0.3;
57+
animation: glitch-shift 3s infinite;
58+
}
59+
@keyframes glitch-shake {
60+
0%, 93%, 100% { transform: translate(0); }
61+
94% { transform: translate(-2px, 1px); }
62+
95% { transform: translate(2px, -1px); }
63+
96% { transform: translate(-1px, 2px); }
64+
}
65+
@keyframes glitch-shift {
66+
0%, 93%, 100% { clip-path: inset(0 0 0 0); }
67+
94% { clip-path: inset(20% 0 60% 0); transform: translate(4px, 0); }
68+
95% { clip-path: inset(50% 0 20% 0); transform: translate(-4px, 0); }
69+
96% { clip-path: inset(70% 0 5% 0); transform: translate(3px, 0); }
70+
}
71+
.lost-message {
72+
font-family: 'Doto', monospace;
73+
font-size: 1.25rem;
74+
color: var(--text-secondary);
75+
margin-bottom: 1.5rem;
76+
max-width: 500px;
77+
}
78+
.lost-message strong {
79+
color: var(--text-primary);
80+
}
81+
.ascii-lost {
82+
font-family: 'Doto', monospace;
83+
font-size: 0.7rem;
84+
line-height: 1.15;
85+
color: var(--accent);
86+
white-space: pre;
87+
margin-bottom: 2rem;
88+
opacity: 0.8;
89+
}
90+
.home-btn {
91+
font-family: 'Doto', monospace;
92+
font-size: 1rem;
93+
font-weight: 700;
94+
padding: 0.75rem 2rem;
95+
background: var(--gradient);
96+
color: #fff;
97+
border: none;
98+
border-radius: 6px;
99+
text-decoration: none;
100+
cursor: pointer;
101+
transition: transform 0.2s, box-shadow 0.2s;
102+
}
103+
.home-btn:hover {
104+
transform: translateY(-2px);
105+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
106+
}
107+
.easter-egg {
108+
margin-top: 3rem;
109+
font-family: 'Doto', monospace;
110+
font-size: 0.85rem;
111+
color: var(--text-secondary);
112+
opacity: 0.6;
113+
cursor: pointer;
114+
transition: opacity 0.3s;
115+
}
116+
.easter-egg:hover {
117+
opacity: 1;
118+
}
119+
.path-echo {
120+
font-family: 'Doto', monospace;
121+
font-size: 0.8rem;
122+
color: var(--text-secondary);
123+
opacity: 0.5;
124+
margin-top: 0.75rem;
125+
word-break: break-all;
126+
}
127+
.path-echo code {
128+
background: var(--card-bg, rgba(255,255,255,0.05));
129+
padding: 0.2rem 0.5rem;
130+
border-radius: 4px;
131+
}
132+
/* GeoCities overrides */
133+
[data-geocities="true"] .glitch-code {
134+
background: none;
135+
-webkit-text-fill-color: #ff0000;
136+
text-shadow: 3px 3px #ffff00;
137+
animation: gc-blink 0.8s step-end infinite;
138+
}
139+
[data-geocities="true"] .glitch-code::after { display: none; }
140+
@keyframes gc-blink {
141+
50% { opacity: 0.4; }
142+
}
143+
[data-geocities="true"] .lost-message {
144+
color: #00ff00;
145+
font-weight: bold;
146+
}
147+
[data-geocities="true"] .home-btn {
148+
background: #0000ff;
149+
border: 3px outset #c0c0c0;
150+
border-radius: 0;
151+
font-size: 1.1rem;
152+
}
153+
[data-geocities="true"] .ascii-lost {
154+
color: #ff00ff;
155+
opacity: 1;
156+
}
157+
</style>
158+
</head>
159+
<body>
160+
<header>
161+
<div class="container">
162+
<a href="index.html" class="logo gradient-text" aria-label="Home">M.</a>
163+
<nav class="site-nav" aria-label="Main">
164+
<a href="index.html">About</a>
165+
<a href="projects.html">Projects</a>
166+
<a href="thoughts.html">Thoughts</a>
167+
</nav>
168+
</div>
169+
<button class="theme-toggle" aria-label="Toggle dark mode">
170+
<i class="fas fa-moon"></i>
171+
<i class="fas fa-sun"></i>
172+
</button>
173+
<button class="geocities-toggle" aria-label="Toggle GeoCities mode" title="Welcome to 1997!">
174+
<i class="fas fa-floppy-disk"></i>
175+
</button>
176+
</header>
177+
178+
<main>
179+
<section class="four-oh-four">
180+
<div class="glitch-code">404</div>
181+
<div class="ascii-lost" aria-hidden="true">
182+
┌─────────────────────────┐
183+
│ ╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗ │
184+
│ ║ ║║ ║║ ║╠═╝╚═╗║ │
185+
│ ╚═╝╚═╝║ ║ ╚═╝╚═╝ │
186+
│ │
187+
│ File not found. │
188+
└─────────────────────────┘</div>
189+
<p class="lost-message">
190+
This page got lost somewhere between <strong>localhost</strong> and <strong>production</strong>.
191+
It happens to the best of us.
192+
</p>
193+
<p class="path-echo"><code id="path-display"></code></p>
194+
<a href="index.html" class="home-btn">← Take me home</a>
195+
<p class="easter-egg" id="egg" title="Click me">( ╯°□°)╯︵ ┻━┻</p>
196+
</section>
197+
</main>
198+
199+
<footer>
200+
<div class="container">
201+
<p class="footer-text">Made with <span class="heart-beat" aria-hidden="true">❤️</span> in Seattle, WA</p>
202+
</div>
203+
</footer>
204+
205+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
206+
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
207+
crossorigin="anonymous" referrerpolicy="no-referrer" />
208+
<script src="script.js" defer></script>
209+
<script src="geocities.js" defer></script>
210+
<script>
211+
// Show the path that 404'd
212+
document.getElementById('path-display').textContent = window.location.pathname;
213+
214+
// Easter egg: table flip → table unflip
215+
var egg = document.getElementById('egg');
216+
var flipped = false;
217+
egg.addEventListener('click', function() {
218+
if (!flipped) {
219+
egg.textContent = '┬─┬ ノ( ゜-゜ノ)';
220+
flipped = true;
221+
} else {
222+
egg.textContent = '( ╯°□°)╯︵ ┻━┻';
223+
flipped = false;
224+
}
225+
});
226+
</script>
227+
</body>
228+
</html>

staticwebapp.config.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"responseOverrides": {
3+
"404": {
4+
"rewrite": "/404.html"
5+
}
6+
}
7+
}

tests/fit-and-finish.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,34 @@ test('index: has meta description', async ({ page }) => {
194194
expect(desc.length).toBeGreaterThan(20);
195195
});
196196

197+
// ── 404 page ──
198+
test('404: shows custom 404 page', async ({ page }) => {
199+
const res = await page.goto('/this-page-does-not-exist-at-all');
200+
expect(res.status()).toBe(404);
201+
await expect(page.locator('.glitch-code')).toBeVisible();
202+
await expect(page.locator('.glitch-code')).toHaveText('404');
203+
await expect(page.locator('.home-btn')).toBeVisible();
204+
});
205+
206+
test('404: table flip easter egg works', async ({ page }) => {
207+
await page.goto('/nope-404');
208+
const egg = page.locator('#egg');
209+
await expect(egg).toContainText('╯︵ ┻━┻');
210+
await egg.click();
211+
await expect(egg).toContainText('┬─┬');
212+
});
213+
214+
test('404: nav links present', async ({ page }) => {
215+
await page.goto('/nope-404');
216+
await expect(page.locator('nav')).toBeVisible();
217+
await expect(page.locator('nav a[href="index.html"]')).toBeVisible();
218+
});
219+
220+
test('404: shows attempted path', async ({ page }) => {
221+
await page.goto('/some/fake/path');
222+
await expect(page.locator('#path-display')).toContainText('/some/fake/path');
223+
});
224+
197225
// ── Font loading ──
198226
test('index: Doto font is loaded', async ({ page }) => {
199227
await page.goto('/', { waitUntil: 'networkidle' });

0 commit comments

Comments
 (0)