Skip to content

Commit 0f37488

Browse files
committed
feat: add dark mode (candlelit morgue theme)
1 parent 103315a commit 0f37488

16 files changed

Lines changed: 213 additions & 128 deletions

next.config.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ const nextConfig = {
2222
key: 'Content-Security-Policy',
2323
value: [
2424
"default-src 'self'",
25-
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com",
25+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com https://plausible.io",
2626
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
2727
"font-src 'self' https://fonts.gstatic.com",
28-
"img-src 'self' data: blob: https://img.shields.io https://avatars.githubusercontent.com",
29-
"connect-src 'self' https://vitals.vercel-insights.com https://va.vercel-scripts.com",
28+
"img-src 'self' data: blob: https://img.shields.io https://avatars.githubusercontent.com https://plausible.io",
29+
"connect-src 'self' https://vitals.vercel-insights.com https://va.vercel-scripts.com https://plausible.io",
3030
"frame-ancestors 'none'",
3131
].join('; '),
3232
},

src/app/globals.css

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,46 @@
99
/* Backgrounds */
1010
--c-bg: #FAF6EF; /* page background */
1111
--c-surface: #EDE8E1; /* cards */
12+
--c-surface-raised: #F5F0E8; /* cards lifted above surface */
1213

1314
/* Text */
1415
--c-ink: #160A06; /* primary text */
1516
--c-ink-2: #4a4440; /* body text on cards */
16-
--c-muted: #9a9288; /* metadata, labels */
17+
--c-muted: #8a8a8a; /* metadata, labels */
1718
--c-faint: #b0aca8; /* very quiet text */
1819

1920
/* Borders */
2021
--c-border: #1a1a1a; /* all solid borders — cards, inputs, buttons */
2122
--c-border-light: #cec6bb; /* dividers, ghost buttons */
2223

2324
/* Accent */
24-
--c-red: #8B0000; /* dead / error */
25+
--c-red: #8B1A1A; /* dead / error */
2526
--c-green: #2d7a3c; /* alive */
2627
--c-amber: #b45309; /* struggling */
28+
--c-stamp-red: #8B1A1A; /* semantic alias for certificate stamp */
29+
}
2730

28-
/* Extended */
29-
--c-surface-raised: #F5F0E8; /* cards lifted above surface */
30-
--c-stamp-red: #8B0000; /* semantic alias for certificate stamp */
31+
[data-theme="dark"] {
32+
/* Backgrounds */
33+
--c-bg: #0F0C09; /* page background */
34+
--c-surface: #1A1510; /* cards */
35+
--c-surface-raised: #241D16; /* cards lifted above surface */
36+
37+
/* Text */
38+
--c-ink: #EDE8E1; /* primary text */
39+
--c-ink-2: #9a9288; /* body text on cards */
40+
--c-muted: #6a6058; /* metadata, labels */
41+
--c-faint: #4a4038; /* very quiet text */
42+
43+
/* Borders */
44+
--c-border: #3a3028; /* all solid borders — cards, inputs, buttons */
45+
--c-border-light: #2a241e; /* dividers, ghost buttons */
46+
47+
/* Accent */
48+
--c-red: #C0392B; /* dead / error */
49+
--c-green: #27ae60; /* alive */
50+
--c-amber: #d35400; /* struggling */
51+
--c-stamp-red: #C0392B;
3152
}
3253

3354
/* ── Unified record / institutional card system ───────────────────── */
@@ -106,7 +127,7 @@
106127
.page-shell-main {
107128
min-height: 100vh;
108129
min-height: 100dvh;
109-
background: #FAF6EF;
130+
background: var(--c-bg);
110131
display: flex;
111132
flex-direction: column;
112133
align-items: center;
@@ -164,7 +185,7 @@
164185
display: inline-block;
165186
width: 8px;
166187
height: 8px;
167-
background: #8b0000;
188+
background: var(--c-red);
168189
border-radius: 50%;
169190
animation: loading-bounce 1s ease-in-out infinite;
170191
}
@@ -180,14 +201,14 @@
180201
font-size: clamp(3.2rem, 9vw, 5rem);
181202
line-height: 0.95;
182203
margin: 6px 0 12px 0;
183-
color: #160a06;
204+
color: var(--c-ink);
184205
}
185206

186207
.page-hero-sub {
187208
font-family: var(--font-courier), system-ui, sans-serif;
188209
font-size: clamp(13px, 3vw, 15px);
189210
font-weight: 400;
190-
color: #5f5f5f;
211+
color: var(--c-ink-2);
191212
line-height: 1.35;
192213
margin: 0 auto;
193214
max-width: 520px;
@@ -222,7 +243,7 @@
222243
.page-hero-micro {
223244
font-family: var(--font-courier), system-ui, sans-serif;
224245
font-size: 11px;
225-
color: #8a8a8a;
246+
color: var(--c-muted);
226247
line-height: 1.5;
227248
margin: 4px auto 0;
228249
letter-spacing: 0.03em;
@@ -347,7 +368,7 @@ button {
347368
}
348369
@media (hover: hover) and (pointer: fine) {
349370
.cert-btn-primary:hover {
350-
background: #2a2a2a !important;
371+
opacity: 0.9 !important;
351372
}
352373
}
353374
.cert-btn-primary:active {
@@ -360,7 +381,7 @@ button {
360381
}
361382
@media (hover: hover) and (pointer: fine) {
362383
.cert-btn-secondary:hover {
363-
background: #EDE8E1 !important;
384+
background: var(--c-surface-raised) !important;
364385
}
365386
}
366387
.cert-btn-secondary:active {
@@ -387,7 +408,7 @@ button {
387408
}
388409
.readme-badge-caption {
389410
font-size: 10px;
390-
color: #b0a89e;
411+
color: var(--c-muted);
391412
letter-spacing: 0.04em;
392413
margin: 10px 0 0;
393414
}
@@ -462,7 +483,7 @@ button {
462483

463484

464485
a.subpage-inline-mail {
465-
color: #1a1a1a;
486+
color: var(--c-ink);
466487
text-decoration: underline;
467488
text-underline-offset: 3px;
468489
word-break: break-all;
@@ -483,10 +504,11 @@ a.subpage-inline-mail {
483504
right: 0;
484505
bottom: 0;
485506
z-index: 40;
486-
background: rgba(250, 246, 239, 0.92);
507+
background: var(--c-bg);
508+
opacity: 0.92;
487509
backdrop-filter: blur(6px);
488510
-webkit-backdrop-filter: blur(6px);
489-
border-top: 1px solid #e2dcd1;
511+
border-top: 1px solid var(--c-border-light);
490512
}
491513

492514
.site-footer--compact {
@@ -504,25 +526,26 @@ a.subpage-inline-mail {
504526
}
505527

506528
.input-button-wrapper:focus-within {
507-
box-shadow: 0 0 0 3px rgba(26,26,26,0.12);
529+
box-shadow: 0 0 0 3px var(--c-border-light);
530+
opacity: 0.8;
508531
}
509532

510533
.input-submit-button:hover {
511-
box-shadow: 0 0 0 1px rgba(0,0,0,0.22);
534+
opacity: 0.9;
512535
}
513536

514537
.input-submit-button:active {
515538
opacity: 0.9;
516539
}
517540

518541
.input-submit-button--dark {
519-
background: #1a1a1a !important;
520-
color: #fff !important;
542+
background: var(--c-ink) !important;
543+
color: var(--c-bg) !important;
521544
}
522545

523546
@media (hover: hover) and (pointer: fine) {
524547
.input-submit-button--dark:hover {
525-
background: #2a2a2a !important;
548+
opacity: 0.9 !important;
526549
}
527550
}
528551

@@ -531,7 +554,7 @@ a.subpage-inline-mail {
531554
}
532555

533556
.recent-card:focus-visible {
534-
outline: 2px solid #1a1a1a;
557+
outline: 2px solid var(--c-border);
535558
outline-offset: 4px;
536559
}
537560

@@ -589,8 +612,8 @@ a.subpage-inline-mail {
589612
@apply antialiased;
590613
font-family: var(--font-courier), system-ui, sans-serif;
591614
font-size: 14px;
592-
color: #160A06;
593-
background-color: #FAF6EF;
615+
color: var(--c-ink);
616+
background-color: var(--c-bg);
594617
overflow-x: hidden;
595618
}
596619
html { overflow-x: hidden; }
@@ -600,11 +623,11 @@ a.subpage-inline-mail {
600623
cursor: default !important;
601624
}
602625
* { box-sizing: border-box; }
603-
input::placeholder { color: #aaa; opacity: 1; }
626+
input::placeholder { color: var(--c-faint); opacity: 0.7; }
604627
input:-webkit-autofill,
605628
input:-webkit-autofill:focus {
606-
-webkit-text-fill-color: #160A06;
607-
-webkit-box-shadow: 0 0 0 1000px #FAF6EF inset;
629+
-webkit-text-fill-color: var(--c-ink);
630+
-webkit-box-shadow: 0 0 0 1000px var(--c-bg) inset;
608631
transition: background-color 5000s ease-in-out 0s;
609632
}
610633
}
@@ -640,7 +663,7 @@ a.subpage-inline-mail {
640663
text-align: center;
641664
font-family: var(--font-courier), system-ui, sans-serif;
642665
font-size: 11px;
643-
color: #7a7a7a;
666+
color: var(--c-muted);
644667
letter-spacing: 0.02em;
645668
}
646669

@@ -679,7 +702,7 @@ a.subpage-inline-mail {
679702
.input-submit-button {
680703
width: 100%;
681704
border-left: none !important;
682-
border-top: 2px solid #1a1a1a !important;
705+
border-top: 2px solid var(--c-border) !important;
683706
margin-top: 0 !important;
684707
justify-content: center !important;
685708
text-align: center !important;

src/app/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,14 @@ const jsonLd = {
9494

9595
export default function RootLayout({ children }: { children: React.ReactNode }) {
9696
return (
97-
<html lang="en">
97+
<html lang="en" suppressHydrationWarning>
9898
<head>
9999
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
100+
<script
101+
dangerouslySetInnerHTML={{
102+
__html: `(function(){try{var t=localStorage.getItem('theme');if(!t&&window.matchMedia('(prefers-color-scheme:dark)').matches)t='dark';if(!t)t='light';document.documentElement.setAttribute('data-theme',t)}catch(e){}})();`,
103+
}}
104+
/>
100105
</head>
101106
<body className={`${spaceGrotesk.variable} ${unifraktur.variable} ${lora.variable} antialiased`}>
102107
<ScannerBanner />

src/app/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ function HomePage() {
185185
{/* User scan error */}
186186
{userFetchError && !userLoading && (
187187
<div style={{ textAlign: 'center', padding: '40px 0' }}>
188-
<p style={{ fontFamily: MONO, fontSize: '13px', color: '#8B0000', marginBottom: '20px' }}>{userFetchError}</p>
189-
<button className="alive-interactive" onClick={resetUser} style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: '#160A06', cursor: 'pointer' }}>
188+
<p style={{ fontFamily: MONO, fontSize: '13px', color: 'var(--c-red)', marginBottom: '20px' }}>{userFetchError}</p>
189+
<button className="alive-interactive" onClick={resetUser} style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: 'var(--c-ink)', cursor: 'pointer' }}>
190190
← examine another subject
191191
</button>
192192
</div>
@@ -206,8 +206,8 @@ function HomePage() {
206206
letterSpacing: '0.06em',
207207
transition: 'color 0.15s',
208208
}}
209-
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = '#1a1a1a' }}
210-
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = '#9a9288' }}
209+
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-ink)' }}
210+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-muted)' }}
211211
>
212212
← back
213213
</button>

src/components/CertificateCard.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
386386
letterSpacing: '0.06em',
387387
transition: 'color 0.15s',
388388
}}
389-
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = '#1a1a1a' }}
390-
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = '#9a9288' }}
389+
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-ink)' }}
390+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-muted)' }}
391391
>
392392
{referrerUser ? `back to @${referrerUser}` : 'back'}
393393
</button>
@@ -412,8 +412,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
412412
minHeight: '44px',
413413
transition: 'color 0.15s',
414414
}}
415-
onMouseEnter={e => (e.currentTarget.style.color = '#1a1a1a')}
416-
onMouseLeave={e => (e.currentTarget.style.color = '#5f5f5f')}
415+
onMouseEnter={e => (e.currentTarget.style.color = 'var(--c-ink)')}
416+
onMouseLeave={e => (e.currentTarget.style.color = 'var(--c-muted)')}
417417
>
418418
<GitHubIcon size={14} />
419419
view on github
@@ -480,8 +480,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
480480
style={{
481481
fontFamily: MONO, fontSize: '13px', fontWeight: 700, letterSpacing: '0.06em',
482482
flex: 1, height: '44px',
483-
background: '#1a1a1a', color: '#fff',
484-
border: '2px solid #1a1a1a',
483+
background: 'var(--c-ink)', color: 'var(--c-bg)',
484+
border: '2px solid var(--c-ink)',
485485
cursor: isGeneratingShare ? 'wait' : 'pointer',
486486
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
487487
}}
@@ -496,8 +496,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
496496
style={{
497497
fontFamily: MONO, fontSize: '13px', fontWeight: 700, letterSpacing: '0.06em',
498498
flex: 1, height: '44px',
499-
background: '#FAF6EF', color: '#1a1a1a',
500-
border: '2px solid #1a1a1a',
499+
background: 'var(--c-bg)', color: 'var(--c-ink)',
500+
border: '2px solid var(--c-ink)',
501501
cursor: isDownloading ? 'wait' : 'pointer',
502502
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px',
503503
}}
@@ -516,8 +516,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
516516
className="cert-btn-primary"
517517
style={{
518518
fontFamily: MONO, fontSize: '14px', fontWeight: 700, letterSpacing: '0.06em',
519-
width: '100%', height: '44px', background: '#1a1a1a', color: '#fff',
520-
border: '2px solid #1a1a1a', cursor: 'pointer',
519+
width: '100%', height: '44px', background: 'var(--c-ink)', color: 'var(--c-bg)',
520+
border: '2px solid var(--c-ink)', cursor: 'pointer',
521521
display: 'flex', alignItems: 'center', justifyContent: 'center',
522522
}}
523523
>
@@ -529,8 +529,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
529529
className="cert-btn-secondary"
530530
style={{
531531
fontFamily: MONO, fontSize: '13px', fontWeight: 700, letterSpacing: '0.06em',
532-
width: '100%', height: '44px', background: '#FAF6EF', color: '#1a1a1a',
533-
border: '2px solid #1a1a1a', cursor: 'pointer',
532+
width: '100%', height: '44px', background: 'var(--c-bg)', color: 'var(--c-ink)',
533+
border: '2px solid var(--c-ink)', cursor: isDownloading ? 'wait' : 'pointer',
534534
display: 'flex', alignItems: 'center', justifyContent: 'center',
535535
}}
536536
>
@@ -669,7 +669,7 @@ export default function CertificateCard({ cert, onReset }: Props) {
669669

670670
{/* Export error */}
671671
{exportError && (
672-
<p style={{ fontFamily: MONO, fontSize: '12px', color: '#8B1A1A', textAlign: 'center', margin: '0' }}>
672+
<p style={{ fontFamily: MONO, fontSize: '12px', color: 'var(--c-red)', textAlign: 'center', margin: '0' }}>
673673
{exportError}
674674
</p>
675675
)}
@@ -694,8 +694,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
694694
textDecorationColor: 'rgba(0,0,0,0.2)',
695695
transition: 'color 0.15s',
696696
}}
697-
onMouseEnter={e => { e.currentTarget.style.color = '#1f1f1f' }}
698-
onMouseLeave={e => { e.currentTarget.style.color = '#5f5f5f' }}
697+
onMouseEnter={e => { e.currentTarget.style.color = 'var(--c-ink)' }}
698+
onMouseLeave={e => { e.currentTarget.style.color = 'var(--c-muted)' }}
699699
>
700700
{referrerUser ? `back to @${referrerUser}'s graveyard →` : 'certify another repo →'}
701701
</button>

src/components/ClickSpark.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface Props {
77
count?: number
88
}
99

10-
export default function ClickSpark({ children, color = '#1a1a1a', count = 10 }: Props) {
10+
export default function ClickSpark({ children, color, count = 10 }: Props) {
1111
const canvasRef = useRef<HTMLCanvasElement>(null)
1212

1313
useEffect(() => {
@@ -31,6 +31,10 @@ export default function ClickSpark({ children, color = '#1a1a1a', count = 10 }:
3131

3232
const x = e.clientX
3333
const y = e.clientY
34+
35+
// Resolve color from CSS variable if not provided
36+
const sparkColor = color || (typeof window !== 'undefined' ? getComputedStyle(document.documentElement).getPropertyValue('--c-ink').trim() : '#1a1a1a')
37+
3438
const sparks = Array.from({ length: count }, (_, i) => ({
3539
angle: (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.5,
3640
len: 14 + Math.random() * 14,
@@ -49,7 +53,7 @@ export default function ClickSpark({ children, color = '#1a1a1a', count = 10 }:
4953
ctx!.beginPath()
5054
ctx!.moveTo(x + Math.cos(s.angle) * r0, y + Math.sin(s.angle) * r0)
5155
ctx!.lineTo(x + Math.cos(s.angle) * r1, y + Math.sin(s.angle) * r1)
52-
ctx!.strokeStyle = color
56+
ctx!.strokeStyle = sparkColor
5357
ctx!.globalAlpha = 1 - p
5458
ctx!.lineWidth = 1.5
5559
ctx!.stroke()

0 commit comments

Comments
 (0)