Compare commits

3 Commits
main ... dev

Author SHA1 Message Date
eric
dd625007de 's' 2026-05-20 19:23:47 -05:00
eric
204a0a441a 's' 2026-05-20 19:21:51 -05:00
eric
fdd456864b ;s; 2026-05-20 18:45:21 -05:00
14 changed files with 452 additions and 280 deletions

View File

@@ -1,53 +1,100 @@
@import "@fontsource/dm-sans/400.css";
@import "@fontsource/dm-sans/500.css";
@import "@fontsource/dm-sans/600.css";
@import "@fontsource/dm-sans/700.css";
@import "@fontsource/plus-jakarta-sans/500.css";
@import "@fontsource/plus-jakarta-sans/600.css";
@import "@fontsource/plus-jakarta-sans/700.css";
@import "@fontsource/plus-jakarta-sans/800.css";
@import "@fontsource/source-serif-4/400.css";
@import "@fontsource/source-serif-4/500.css";
@import "@fontsource/source-serif-4/600.css";
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #f4f7fb; --font-dm-sans: "DM Sans", system-ui, sans-serif;
--foreground: #0c1929; --font-plus-jakarta: "Plus Jakarta Sans", system-ui, sans-serif;
--muted: #5b6b7c; --font-source-serif: "Source Serif 4", Georgia, serif;
--accent: #0d9488; }
@custom-variant dark (&:where(.dark, .dark *));
/* Light theme — defaults (overridden by inline vars on <html> when toggling) */
html[data-theme="light"],
:root:not(.dark) {
color-scheme: light;
--background: #faf8f5;
--foreground: #1a1614;
--muted: #6b6560;
--accent: #4f46e5;
--accent-secondary: #ea580c; --accent-secondary: #ea580c;
--accent-tertiary: #0284c7; --accent-tertiary: #db2777;
--surface: rgba(255, 255, 255, 0.78); --surface: #ffffff;
--surface-solid: #ffffff; --surface-elevated: #ffffff;
--border: rgba(13, 148, 136, 0.14); --border: #e8e4df;
--gradient-1: #0d9488; --border-strong: #d6d0c8;
--gradient-2: #f59e0b; --gradient-1: #4f46e5;
--gradient-3: #0284c7; --gradient-2: #ea580c;
--glow-sm: 0 8px 32px rgba(13, 148, 136, 0.2); --gradient-3: #db2777;
--glow-lg: 0 24px 64px rgba(234, 88, 12, 0.18); --glow-sm: 0 8px 30px rgba(79, 70, 229, 0.12);
--mesh-1: rgba(13, 148, 136, 0.32); --glow-lg: 0 20px 50px rgba(234, 88, 12, 0.14);
--mesh-2: rgba(245, 158, 11, 0.26); --mesh-1: rgba(79, 70, 229, 0.14);
--mesh-3: rgba(2, 132, 199, 0.22); --mesh-2: rgba(234, 88, 12, 0.1);
--prose: #334155; --mesh-3: rgba(219, 39, 119, 0.08);
--card-shadow: 0 4px 24px rgba(12, 25, 41, 0.07); --prose: #44403c;
--callout-bg: rgba(13, 148, 136, 0.08); --card-shadow: 0 1px 2px rgba(26, 22, 20, 0.04), 0 8px 24px rgba(26, 22, 20, 0.06);
--callout-border: rgba(13, 148, 136, 0.22); --callout-bg: rgba(79, 70, 229, 0.06);
--callout-border: rgba(79, 70, 229, 0.18);
--cover-from: #3730a3;
--cover-via: #7c2d12;
--cover-to: #831843;
--cover-glow: rgba(79, 70, 229, 0.35);
--cta-from: #4338ca;
--cta-via: #c2410c;
--cta-to: #9d174d;
--cta-text: #faf8f5;
--cta-btn-bg: #faf8f5;
--cta-btn-text: #3730a3;
--read-max: 42rem; --read-max: 42rem;
--header-h: 4.25rem; --header-h: 4.25rem;
} }
.dark { /* Dark theme */
--background: #060d18; html[data-theme="dark"],
--foreground: #e8eef4; html.dark {
--muted: #8fa3b8; color-scheme: dark;
--accent: #2dd4bf; --background: #0f0e0d;
--accent-secondary: #fb923c; --foreground: #f5f2ed;
--accent-tertiary: #38bdf8; --muted: #a39e97;
--surface: rgba(12, 25, 41, 0.72); --accent: #a5b4fc;
--surface-solid: #0c1929; --accent-secondary: #fdba74;
--border: rgba(45, 212, 191, 0.14); --accent-tertiary: #f9a8d4;
--gradient-1: #14b8a6; --surface: #1a1816;
--surface-elevated: #242120;
--border: #2e2b28;
--border-strong: #3d3935;
--gradient-1: #818cf8;
--gradient-2: #fb923c; --gradient-2: #fb923c;
--gradient-3: #38bdf8; --gradient-3: #f472b6;
--glow-sm: 0 8px 32px rgba(20, 184, 166, 0.22); --glow-sm: 0 8px 32px rgba(129, 140, 248, 0.18);
--glow-lg: 0 24px 64px rgba(251, 146, 60, 0.16); --glow-lg: 0 20px 56px rgba(251, 146, 60, 0.12);
--mesh-1: rgba(20, 184, 166, 0.35); --mesh-1: rgba(129, 140, 248, 0.12);
--mesh-2: rgba(251, 146, 60, 0.28); --mesh-2: rgba(251, 146, 60, 0.1);
--mesh-3: rgba(56, 189, 248, 0.25); --mesh-3: rgba(244, 114, 182, 0.08);
--prose: #b8c5d4; --prose: #c9c2ba;
--card-shadow: 0 4px 28px rgba(0, 0, 0, 0.4); --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 32px rgba(0, 0, 0, 0.35);
--callout-bg: rgba(20, 184, 166, 0.1); --callout-bg: rgba(129, 140, 248, 0.08);
--callout-border: rgba(45, 212, 191, 0.25); --callout-border: rgba(165, 180, 252, 0.22);
--cover-from: #312e81;
--cover-via: #7c2d12;
--cover-to: #701a3a;
--cover-glow: rgba(129, 140, 248, 0.4);
--cta-from: #1e1b4b;
--cta-via: #431407;
--cta-to: #500724;
--cta-text: #f5f2ed;
--cta-btn-bg: #f5f2ed;
--cta-btn-text: #312e81;
} }
@theme inline { @theme inline {
@@ -63,37 +110,23 @@
} }
html { html {
color-scheme: light;
scroll-padding-top: calc(var(--header-h) + 1rem); scroll-padding-top: calc(var(--header-h) + 1rem);
} }
html.dark {
color-scheme: dark;
}
body { body {
background: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: var(--font-dm-sans), system-ui, sans-serif; font-family: var(--font-dm-sans), system-ui, sans-serif;
transition: background-color 0.25s ease, color 0.25s ease;
} }
/* Latitude grid + ambient mesh */
.mesh-bg { .mesh-bg {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: -2; z-index: -2;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
background-image: background-color: var(--background);
linear-gradient(rgba(13, 148, 136, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(13, 148, 136, 0.04) 1px, transparent 1px);
background-size: 48px 48px;
}
.dark .mesh-bg {
background-image:
linear-gradient(rgba(45, 212, 191, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(45, 212, 191, 0.06) 1px, transparent 1px);
} }
.mesh-bg::before, .mesh-bg::before,
@@ -101,56 +134,56 @@ body {
content: ""; content: "";
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(88px); filter: blur(100px);
animation: mesh-float 20s ease-in-out infinite alternate; animation: mesh-float 24s ease-in-out infinite alternate;
} }
.mesh-bg::before { .mesh-bg::before {
width: min(72vw, 540px); width: min(65vw, 480px);
height: min(72vw, 540px); height: min(65vw, 480px);
top: -14%; top: -10%;
right: -6%; right: -5%;
background: radial-gradient(circle, var(--mesh-1), transparent 68%); background: radial-gradient(circle, var(--mesh-1), transparent 70%);
} }
.mesh-bg::after { .mesh-bg::after {
width: min(58vw, 420px); width: min(55vw, 400px);
height: min(58vw, 420px); height: min(55vw, 400px);
bottom: 8%; bottom: 5%;
left: -12%; left: -8%;
background: radial-gradient(circle, var(--mesh-2), transparent 70%); background: radial-gradient(circle, var(--mesh-2), transparent 70%);
animation-delay: -7s; animation-delay: -8s;
} }
.mesh-orb { .mesh-orb {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(76px); filter: blur(90px);
animation: mesh-float 24s ease-in-out infinite alternate-reverse; animation: mesh-float 28s ease-in-out infinite alternate-reverse;
} }
.mesh-orb--sky { .mesh-orb--accent {
width: min(48vw, 340px); width: min(42vw, 300px);
height: min(48vw, 340px); height: min(42vw, 300px);
top: 42%; top: 45%;
left: 38%; left: 40%;
background: radial-gradient(circle, var(--mesh-3), transparent 70%); background: radial-gradient(circle, var(--mesh-3), transparent 70%);
} }
@keyframes mesh-float { @keyframes mesh-float {
0% { 0% {
transform: translate(0, 0) scale(1); transform: translate(0, 0);
} }
100% { 100% {
transform: translate(20px, -28px) scale(1.06); transform: translate(16px, -20px);
} }
} }
.gradient-text { .gradient-text {
background: linear-gradient( background: linear-gradient(
125deg, 120deg,
var(--gradient-1) 0%, var(--gradient-1) 0%,
var(--gradient-2) 45%, var(--gradient-2) 52%,
var(--gradient-3) 100% var(--gradient-3) 100%
); );
-webkit-background-clip: text; -webkit-background-clip: text;
@@ -161,10 +194,13 @@ body {
.glass-card { .glass-card {
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
backdrop-filter: blur(16px);
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
} }
.dark .glass-card {
background: var(--surface-elevated);
}
.btn-primary { .btn-primary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -175,22 +211,21 @@ body {
border-radius: 9999px; border-radius: 9999px;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: white; color: #fff;
background: linear-gradient( background: linear-gradient(
125deg, 120deg,
var(--gradient-1), var(--gradient-1),
var(--gradient-2), var(--gradient-2),
var(--gradient-3) var(--gradient-3)
); );
background-size: 200% 200%;
box-shadow: var(--glow-sm); box-shadow: var(--glow-sm);
transition: transform 0.2s, box-shadow 0.2s, background-position 0.4s; transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
} }
.btn-primary:hover { .btn-primary:hover {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--glow-lg); box-shadow: var(--glow-lg);
background-position: 100% 50%; filter: brightness(1.05);
} }
.btn-secondary { .btn-secondary {
@@ -205,17 +240,39 @@ body {
font-weight: 600; font-weight: 600;
color: var(--foreground); color: var(--foreground);
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border-strong);
backdrop-filter: blur(12px); transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
} }
.btn-secondary:hover { .btn-secondary:hover {
border-color: color-mix(in srgb, var(--accent) 45%, transparent); border-color: var(--accent);
box-shadow: var(--glow-sm); box-shadow: var(--glow-sm);
transform: translateY(-1px); transform: translateY(-1px);
} }
.cta-panel {
background: linear-gradient(
135deg,
var(--cta-from),
var(--cta-via),
var(--cta-to)
);
}
.cta-panel__title,
.cta-panel__eyebrow {
color: var(--cta-text);
}
.cta-panel__body {
color: color-mix(in srgb, var(--cta-text) 88%, transparent);
}
.cta-panel__btn {
background: var(--cta-btn-bg);
color: var(--cta-btn-text);
}
.font-display { .font-display {
font-family: var(--font-plus-jakarta), var(--font-dm-sans), system-ui, sans-serif; font-family: var(--font-plus-jakarta), var(--font-dm-sans), system-ui, sans-serif;
} }
@@ -224,17 +281,29 @@ body {
animation: cover-float 7s ease-in-out infinite; animation: cover-float 7s ease-in-out infinite;
} }
.cover-card {
background: linear-gradient(
145deg,
var(--cover-from),
var(--cover-via),
var(--cover-to)
);
}
.cover-glow {
background: var(--cover-glow);
}
@keyframes cover-float { @keyframes cover-float {
0%, 0%,
100% { 100% {
transform: translateY(0) rotate(-0.5deg); transform: translateY(0);
} }
50% { 50% {
transform: translateY(-12px) rotate(0.5deg); transform: translateY(-10px);
} }
} }
/* Hub marquee */
.marquee-track { .marquee-track {
display: flex; display: flex;
width: max-content; width: max-content;
@@ -250,15 +319,14 @@ body {
} }
} }
/* Reading progress */
.reading-progress { .reading-progress {
position: fixed; position: fixed;
top: var(--header-h); top: var(--header-h);
left: 0; left: 0;
z-index: 40; z-index: 40;
height: 3px; height: 2px;
width: 100%; width: 100%;
background: transparent; background: var(--border);
pointer-events: none; pointer-events: none;
} }
@@ -272,10 +340,8 @@ body {
var(--gradient-3) var(--gradient-3)
); );
transition: width 0.1s linear; transition: width 0.1s linear;
box-shadow: 0 0 12px color-mix(in srgb, var(--gradient-1) 50%, transparent);
} }
/* Prose */
.prose-ebook { .prose-ebook {
font-size: 1.0625rem; font-size: 1.0625rem;
line-height: 1.88; line-height: 1.88;
@@ -341,7 +407,6 @@ body {
border-bottom: 1px solid var(--callout-border); border-bottom: 1px solid var(--callout-border);
font-size: 0.98rem; font-size: 0.98rem;
color: var(--foreground); color: var(--foreground);
font-style: normal;
} }
.prose-ebook blockquote p { .prose-ebook blockquote p {
@@ -349,9 +414,8 @@ body {
color: inherit; color: inherit;
} }
/* TOC active state */
.toc-link--active { .toc-link--active {
background: color-mix(in srgb, var(--gradient-1) 12%, transparent); background: color-mix(in srgb, var(--gradient-1) 14%, transparent);
color: var(--foreground); color: var(--foreground);
font-weight: 600; font-weight: 600;
} }
@@ -366,8 +430,10 @@ body {
.marquee-track, .marquee-track,
.mesh-bg::before, .mesh-bg::before,
.mesh-bg::after, .mesh-bg::after,
.mesh-orb { .mesh-orb,
body {
animation: none; animation: none;
transition: none;
} }
.btn-primary:hover, .btn-primary:hover,

View File

@@ -1,25 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { DM_Sans, Plus_Jakarta_Sans, Source_Serif_4 } from "next/font/google"; import Script from "next/script";
import "./globals.css"; import "./globals.css";
import { brand, ebookMeta } from "@/content/ebook"; import { ebookMeta } from "@/content/ebook";
const dmSans = DM_Sans({
variable: "--font-dm-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const plusJakarta = Plus_Jakarta_Sans({
variable: "--font-plus-jakarta",
subsets: ["latin"],
weight: ["500", "600", "700", "800"],
});
const sourceSerif = Source_Serif_4({
variable: "--font-source-serif",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: `${ebookMeta.title} — Free Digital Nomad Ebook`, title: `${ebookMeta.title} — Free Digital Nomad Ebook`,
@@ -39,19 +21,6 @@ export const metadata: Metadata = {
}, },
}; };
const themeScript = `
(function () {
try {
var stored = localStorage.getItem('theme');
var dark =
stored === 'dark' ||
(stored !== 'light' &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
} catch (e) {}
})();
`;
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -61,14 +30,12 @@ export default function RootLayout({
<html <html
lang="en" lang="en"
suppressHydrationWarning suppressHydrationWarning
className={`${dmSans.variable} ${plusJakarta.variable} ${sourceSerif.variable} h-full scroll-smooth antialiased`} className="h-full scroll-smooth antialiased"
> >
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="min-h-full font-sans"> <body className="min-h-full font-sans">
<Script src="/theme.js" strategy="beforeInteractive" />
<div className="mesh-bg" aria-hidden> <div className="mesh-bg" aria-hidden>
<div className="mesh-orb mesh-orb--sky" /> <div className="mesh-orb mesh-orb--accent" />
</div> </div>
{children} {children}
</body> </body>

View File

@@ -17,7 +17,7 @@ const hubMarquee = [...nomadHubs, ...nomadHubs];
export default function LandingPage() { export default function LandingPage() {
return ( return (
<div className="relative min-h-screen"> <div className="relative min-h-screen bg-[var(--background)] text-[var(--foreground)]">
<SiteHeader /> <SiteHeader />
<main> <main>
@@ -25,11 +25,8 @@ export default function LandingPage() {
<section className="relative overflow-hidden px-4 pb-14 pt-8 sm:px-6 sm:pb-20 sm:pt-12 lg:px-8 lg:pb-28 lg:pt-16"> <section className="relative overflow-hidden px-4 pb-14 pt-8 sm:px-6 sm:pb-20 sm:pt-12 lg:px-8 lg:pb-28 lg:pt-16">
<div className="mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-2 lg:gap-20"> <div className="mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div className="order-2 text-center lg:order-1 lg:text-left"> <div className="order-2 text-center lg:order-1 lg:text-left">
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--accent)] backdrop-blur-md"> <p className="mb-4 inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--accent)] shadow-sm">
<span className="relative flex h-2 w-2"> <span className="h-2 w-2 rounded-full bg-[var(--gradient-2)]" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--gradient-1)] opacity-60" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--gradient-1)]" />
</span>
Free ebook · Digital nomad · English Free ebook · Digital nomad · English
</p> </p>
<h1 className="font-display text-[2.35rem] font-extrabold leading-[1.06] tracking-tight sm:text-5xl lg:text-[3.4rem]"> <h1 className="font-display text-[2.35rem] font-extrabold leading-[1.06] tracking-tight sm:text-5xl lg:text-[3.4rem]">
@@ -67,7 +64,7 @@ export default function LandingPage() {
{heroStats.map((stat) => ( {heroStats.map((stat) => (
<div <div
key={stat.label} key={stat.label}
className="rounded-xl border border-[var(--border)] bg-[var(--surface)]/60 px-3 py-3 text-center backdrop-blur-sm" className="rounded-xl border border-[var(--border)] bg-[var(--surface)] px-3 py-3 text-center shadow-sm"
> >
<dt className="font-display text-lg font-bold text-[var(--foreground)]"> <dt className="font-display text-lg font-bold text-[var(--foreground)]">
{stat.value} {stat.value}
@@ -88,7 +85,7 @@ export default function LandingPage() {
{/* Hub marquee */} {/* Hub marquee */}
<section <section
className="border-y border-[var(--border)] bg-[var(--surface-solid)]/40 py-4" className="border-y border-[var(--border)] bg-[var(--surface)] py-4 dark:bg-[var(--surface-elevated)]"
aria-label="Popular nomad hubs" aria-label="Popular nomad hubs"
> >
<div className="overflow-hidden"> <div className="overflow-hidden">
@@ -96,7 +93,7 @@ export default function LandingPage() {
{hubMarquee.map((hub, i) => ( {hubMarquee.map((hub, i) => (
<span <span
key={`${hub.city}-${i}`} key={`${hub.city}-${i}`}
className="mx-1.5 inline-flex shrink-0 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-sm backdrop-blur-sm" className="mx-1.5 inline-flex shrink-0 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-sm shadow-sm"
> >
<span className="font-semibold text-[var(--foreground)]"> <span className="font-semibold text-[var(--foreground)]">
{hub.city} {hub.city}
@@ -143,7 +140,7 @@ export default function LandingPage() {
</section> </section>
{/* Pillars */} {/* Pillars */}
<section className="border-y border-[var(--border)] bg-[var(--surface-solid)]/50 px-4 py-16 sm:px-6 sm:py-24 lg:px-8"> <section className="border-y border-[var(--border)] bg-[var(--surface)] px-4 py-16 dark:bg-[var(--surface-elevated)] sm:px-6 sm:py-24 lg:px-8">
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center"> <div className="mx-auto max-w-2xl text-center">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--accent)]"> <p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--accent)]">
@@ -250,7 +247,7 @@ export default function LandingPage() {
</section> </section>
{/* FAQ */} {/* FAQ */}
<section className="border-t border-[var(--border)] bg-[var(--surface-solid)]/40 px-4 py-16 sm:px-6 sm:py-24 lg:px-8"> <section className="border-t border-[var(--border)] bg-[var(--background)] px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
<p className="text-center text-xs font-semibold uppercase tracking-[0.2em] text-[var(--accent)]"> <p className="text-center text-xs font-semibold uppercase tracking-[0.2em] text-[var(--accent)]">
FAQ FAQ
@@ -266,29 +263,25 @@ export default function LandingPage() {
{/* CTA */} {/* CTA */}
<section className="px-4 py-16 sm:px-6 sm:py-24 lg:px-8"> <section className="px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<div className="relative mx-auto max-w-6xl overflow-hidden rounded-3xl"> <div className="cta-panel relative mx-auto max-w-6xl overflow-hidden rounded-3xl shadow-[var(--glow-lg)]">
<div <div
className="absolute inset-0 bg-gradient-to-br from-teal-700 via-teal-600 to-sky-800 dark:from-teal-900 dark:via-teal-800 dark:to-slate-900" className="absolute inset-0 bg-[radial-gradient(ellipse_at_80%_0%,rgba(255,255,255,0.12),transparent_55%)]"
aria-hidden
/>
<div
className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width=%2260%22 height=%2260%22 viewBox=%220 0 60 60%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cg fill=%22none%22 fill-rule=%22evenodd%22%3E%3Cg fill=%22%23ffffff%22 fill-opacity=%220.06%22%3E%3Cpath d=%22M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-80"
aria-hidden aria-hidden
/> />
<div className="relative px-8 py-14 text-center sm:px-16 sm:py-20"> <div className="relative px-8 py-14 text-center sm:px-16 sm:py-20">
<p className="text-xs font-bold uppercase tracking-[0.2em] text-teal-200"> <p className="cta-panel__eyebrow text-xs font-bold uppercase tracking-[0.2em]">
Your next base starts here Your next base starts here
</p> </p>
<h2 className="mt-3 font-display text-2xl font-bold text-white sm:text-4xl"> <h2 className="cta-panel__title mt-3 font-display text-2xl font-bold sm:text-4xl">
Read the nomad playbook tonight Read the nomad playbook tonight
</h2> </h2>
<p className="mx-auto mt-4 max-w-lg text-base text-teal-50/90 sm:text-lg"> <p className="cta-panel__body mx-auto mt-4 max-w-lg text-base sm:text-lg">
No signup. No paywall. Open on any device, switch themes for red-eye No signup. No paywall. Open on any device, switch themes for red-eye
flights, and jump straight to visas or community. flights, and jump straight to visas or community.
</p> </p>
<Link <Link
href="/read" href="/read"
className="mt-8 inline-flex h-12 items-center justify-center rounded-full bg-white px-10 text-sm font-bold text-teal-800 shadow-xl transition-transform hover:scale-[1.02] active:scale-[0.98]" className="cta-panel__btn mt-8 inline-flex h-12 items-center justify-center rounded-full px-10 text-sm font-bold shadow-xl transition-transform hover:scale-[1.02] active:scale-[0.98]"
> >
Open {ebookMeta.title} Open {ebookMeta.title}
</Link> </Link>

View File

@@ -13,7 +13,7 @@ export const metadata = {
export default function ReadPage() { export default function ReadPage() {
return ( return (
<div className="relative min-h-screen"> <div className="relative min-h-screen bg-[var(--background)] text-[var(--foreground)]">
<SiteHeader ctaLabel="Home" ctaHref="/" showBack /> <SiteHeader ctaLabel="Home" ctaHref="/" showBack />
<ReadingProgress /> <ReadingProgress />
@@ -60,7 +60,7 @@ export default function ReadPage() {
))} ))}
</article> </article>
<footer className="mt-16 rounded-2xl border border-[var(--border)] bg-[var(--surface)] px-6 py-10 text-center backdrop-blur-md sm:mt-20"> <footer className="glass-card mt-16 px-6 py-10 text-center sm:mt-20">
<p className="font-display text-lg font-bold text-[var(--foreground)]"> <p className="font-display text-lg font-bold text-[var(--foreground)]">
You&apos;ve finished the trailhead You&apos;ve finished the trailhead
</p> </p>

View File

@@ -6,43 +6,31 @@ export function BookCover({ className = "" }: { className?: string }) {
className={`cover-float relative w-full max-w-[min(100%,300px)] sm:max-w-[320px] ${className}`} className={`cover-float relative w-full max-w-[min(100%,300px)] sm:max-w-[320px] ${className}`}
> >
<div <div
className="absolute -inset-6 rounded-[2rem] bg-gradient-to-br from-teal-500/50 via-amber-400/40 to-sky-500/50 opacity-60 blur-2xl" className="cover-glow absolute -inset-6 rounded-[2rem] opacity-50 blur-2xl"
aria-hidden
/>
<div className="relative aspect-[3/4] overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/25">
<div
className="absolute inset-0 bg-gradient-to-br from-teal-800 via-teal-700 to-sky-900"
aria-hidden
/>
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"repeating-linear-gradient(0deg, transparent, transparent 23px, rgba(255,255,255,0.06) 24px)",
}}
aria-hidden aria-hidden
/> />
<div className="cover-card relative aspect-[3/4] overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/15">
<div <div
className="absolute inset-0 bg-[radial-gradient(ellipse_at_30%_12%,rgba(255,255,255,0.22),transparent_55%)]" className="absolute inset-0 bg-[radial-gradient(ellipse_at_30%_12%,rgba(255,255,255,0.22),transparent_55%)]"
aria-hidden aria-hidden
/> />
<div <div
className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-black/50 to-transparent" className="absolute inset-0 bg-[linear-gradient(160deg,transparent_35%,rgba(0,0,0,0.4))]"
aria-hidden aria-hidden
/> />
<div <div
className="absolute left-0 top-0 h-full w-4 bg-gradient-to-r from-black/35 to-transparent" className="absolute left-0 top-0 h-full w-4 bg-gradient-to-r from-black/35 to-transparent"
aria-hidden aria-hidden
/> />
<div className="absolute right-5 top-6 opacity-20" aria-hidden> <div className="absolute right-5 top-6 opacity-15" aria-hidden>
<GlobeMark className="h-24 w-24 text-white" /> <GlobeMark className="h-24 w-24 text-white" />
</div> </div>
<div className="relative flex h-full flex-col justify-between p-7 text-white sm:p-8"> <div className="relative flex h-full flex-col justify-between p-7 text-white sm:p-8">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<p className="text-[10px] font-bold uppercase tracking-[0.28em] text-teal-200/90"> <p className="text-[10px] font-bold uppercase tracking-[0.28em] text-white/75">
Digital nomad Digital nomad
</p> </p>
<span className="rounded-full border border-white/20 bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/80"> <span className="rounded-full border border-white/25 bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/90">
EN EN
</span> </span>
</div> </div>
@@ -50,12 +38,12 @@ export function BookCover({ className = "" }: { className?: string }) {
<h2 className="font-serif text-[1.7rem] font-medium leading-[1.1] tracking-tight sm:text-[1.9rem]"> <h2 className="font-serif text-[1.7rem] font-medium leading-[1.1] tracking-tight sm:text-[1.9rem]">
{ebookMeta.title} {ebookMeta.title}
</h2> </h2>
<p className="mt-3 text-xs leading-relaxed text-teal-100/85 sm:text-sm"> <p className="mt-3 text-xs leading-relaxed text-white/80 sm:text-sm">
{ebookMeta.subtitle} {ebookMeta.subtitle}
</p> </p>
</div> </div>
<div> <div>
<p className="font-mono text-[10px] tracking-wider text-white/50"> <p className="font-mono text-[10px] tracking-wider text-white/55">
48.8566° N · Work from anywhere 48.8566° N · Work from anywhere
</p> </p>
<p className="mt-2 text-xs font-medium text-white/70"> <p className="mt-2 text-xs font-medium text-white/70">
@@ -70,9 +58,15 @@ export function BookCover({ className = "" }: { className?: string }) {
function GlobeMark({ className }: { className?: string }) { function GlobeMark({ className }: { className?: string }) {
return ( return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="0.75"> <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="0.75"
>
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" /> <path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 014-10z" />
</svg> </svg>
); );
} }

View File

@@ -96,7 +96,7 @@ export function ReadSidebar() {
<button <button
type="button" type="button"
onClick={() => setMobileOpen(true)} onClick={() => setMobileOpen(true)}
className="flex h-12 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface-solid)] px-4 text-sm font-semibold text-[var(--foreground)] shadow-lg" className="flex h-12 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 text-sm font-semibold text-[var(--foreground)] shadow-lg"
aria-expanded={mobileOpen} aria-expanded={mobileOpen}
aria-controls="mobile-toc-panel" aria-controls="mobile-toc-panel"
> >
@@ -120,7 +120,7 @@ export function ReadSidebar() {
/> />
<div <div
id="mobile-toc-panel" id="mobile-toc-panel"
className="absolute bottom-0 left-0 right-0 max-h-[80vh] overflow-hidden rounded-t-3xl border border-[var(--border)] bg-[var(--surface-solid)] shadow-2xl" className="absolute bottom-0 left-0 right-0 max-h-[80vh] overflow-hidden rounded-t-3xl border border-[var(--border)] bg-[var(--surface)] shadow-2xl dark:bg-[var(--surface-elevated)]"
> >
<div className="flex items-center justify-between border-b border-[var(--border)] px-5 py-4"> <div className="flex items-center justify-between border-b border-[var(--border)] px-5 py-4">
<p <p

View File

@@ -14,7 +14,7 @@ export function SiteHeader({
showBack = false, showBack = false,
}: SiteHeaderProps) { }: SiteHeaderProps) {
return ( return (
<header className="sticky top-0 z-50 h-[var(--header-h)] border-b border-[var(--border)]/70 bg-[var(--background)]/80 backdrop-blur-xl"> <header className="sticky top-0 z-50 h-[var(--header-h)] border-b border-[var(--border)] bg-[var(--surface)]/95 backdrop-blur-xl dark:bg-[var(--background)]/90">
<div className="mx-auto flex h-full max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8"> <div className="mx-auto flex h-full max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
{showBack ? ( {showBack ? (
<Link <Link
@@ -29,7 +29,7 @@ export function SiteHeader({
className={`group flex min-w-0 items-center gap-2.5 ${showBack ? "hidden sm:flex" : "flex"}`} className={`group flex min-w-0 items-center gap-2.5 ${showBack ? "hidden sm:flex" : "flex"}`}
> >
<span <span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] text-white shadow-lg shadow-teal-500/20" className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] text-white shadow-md"
aria-hidden aria-hidden
> >
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -51,7 +51,7 @@ export function SiteHeader({
<ThemeToggle /> <ThemeToggle />
<Link <Link
href={ctaHref} href={ctaHref}
className="hidden h-10 items-center justify-center rounded-full bg-gradient-to-r from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] px-5 text-sm font-semibold text-white shadow-md shadow-teal-500/20 transition-transform hover:scale-[1.02] active:scale-[0.98] sm:inline-flex" className="hidden h-10 items-center justify-center rounded-full bg-gradient-to-r from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] px-5 text-sm font-semibold text-white shadow-[var(--glow-sm)] transition-transform hover:scale-[1.02] active:scale-[0.98] sm:inline-flex"
> >
{ctaLabel} {ctaLabel}
</Link> </Link>

View File

@@ -1,97 +1,27 @@
"use client"; /** Theme click handled by /public/theme.js — works without React hydration */
import { useEffect, useState } from "react";
type Theme = "light" | "dark";
function getPreferredTheme(): Theme {
if (typeof window === "undefined") return "light";
const stored = localStorage.getItem("theme");
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(theme: Theme) {
document.documentElement.classList.toggle("dark", theme === "dark");
localStorage.setItem("theme", theme);
}
export function ThemeToggle() { export function ThemeToggle() {
const [theme, setTheme] = useState<Theme>("light");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setTheme(getPreferredTheme());
setMounted(true);
}, []);
function toggle() {
const next: Theme = theme === "dark" ? "light" : "dark";
setTheme(next);
applyTheme(next);
}
return ( return (
<button <button
type="button" type="button"
onClick={toggle} data-theme-toggle
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"} aria-pressed="false"
className="theme-toggle group relative flex h-10 w-10 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface)]/80 text-[var(--foreground)] shadow-sm backdrop-blur-md transition-all hover:border-[var(--accent)]/40 hover:shadow-[var(--glow-sm)]" aria-label="Switch to dark mode"
title="Dark mode"
className="inline-flex h-10 shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full border-2 border-[var(--border-strong)] bg-[var(--surface)] px-3 text-[var(--foreground)] shadow-sm transition-[background-color,border-color,color] duration-200 hover:border-[var(--accent)]"
> >
<span className="sr-only"> <span data-theme-icon="sun" hidden aria-hidden>
{mounted <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
? theme === "dark" <path d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0-16v2m0 16v2M4.22 4.22l1.42 1.42m12.72 12.72 1.42 1.42M2 12h2m16 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
? "Light mode" </svg>
: "Dark mode" </span>
: "Toggle theme"} <span data-theme-icon="moon" aria-hidden>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3 7 7 0 1 0 21 14.5z" />
</svg>
</span>
<span data-theme-label className="text-xs font-bold tracking-wide">
Dark
</span> </span>
<SunIcon
className={`absolute h-[18px] w-[18px] transition-all duration-300 ${
mounted && theme === "dark"
? "rotate-0 scale-100 opacity-100"
: "rotate-90 scale-0 opacity-0"
}`}
/>
<MoonIcon
className={`absolute h-[18px] w-[18px] transition-all duration-300 ${
mounted && theme !== "dark"
? "rotate-0 scale-100 opacity-100"
: "-rotate-90 scale-0 opacity-0"
}`}
/>
</button> </button>
); );
} }
function SunIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
aria-hidden
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</svg>
);
}
function MoonIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
aria-hidden
>
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3 7 7 0 1 0 21 14.5z" />
</svg>
);
}

29
lib/theme.ts Normal file
View File

@@ -0,0 +1,29 @@
export type Theme = "light" | "dark";
export const THEME_STORAGE_KEY = "theme";
declare global {
interface Window {
NomadRoamTheme?: {
apply: (theme: Theme) => void;
toggle: () => void;
read: () => Theme;
};
}
}
export function applyTheme(theme: Theme) {
if (typeof window !== "undefined" && window.NomadRoamTheme) {
window.NomadRoamTheme.apply(theme);
}
}
export function readThemeFromDom(): Theme {
if (typeof window !== "undefined" && window.NomadRoamTheme) {
return window.NomadRoamTheme.read();
}
if (typeof document === "undefined") return "light";
return document.documentElement.getAttribute("data-theme") === "dark"
? "dark"
: "light";
}

View File

@@ -1,7 +1,8 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ // Allow phone/other devices on LAN to use dev HMR (e.g. http://192.168.x.x:3000)
allowedDevOrigins: ["192.168.2.203", "localhost", "127.0.0.1"],
}; };
export default nextConfig; export default nextConfig;

3
nomadroen.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
export PORT=4007
exec pnpm run start

View File

@@ -9,6 +9,9 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/plus-jakarta-sans": "^5.2.8",
"@fontsource/source-serif-4": "^5.2.9",
"next": "16.2.6", "next": "16.2.6",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4"

24
pnpm-lock.yaml generated
View File

@@ -8,6 +8,15 @@ importers:
.: .:
dependencies: dependencies:
'@fontsource/dm-sans':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/plus-jakarta-sans':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/source-serif-4':
specifier: ^5.2.9
version: 5.2.9
next: next:
specifier: 16.2.6 specifier: 16.2.6
version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -163,6 +172,15 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fontsource/dm-sans@5.2.8':
resolution: {integrity: sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==}
'@fontsource/plus-jakarta-sans@5.2.8':
resolution: {integrity: sha512-P5qE49fqdeD+7DXH1KBxmMPlB17LTz1zvBhFH0tFzfnYTKVJVyb0pR6plh0ZGXxcB+Oayb54FZZw3V42/DawTw==}
'@fontsource/source-serif-4@5.2.9':
resolution: {integrity: sha512-er/Pym9emsEVJNf947umJ4kXarXfsiN6CN7kTYinefKRaHLwiquiiHOZvKvxWgkV8JMCf3pV3g0NcsPFpVCH9w==}
'@humanfs/core@0.19.2': '@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@@ -2084,6 +2102,12 @@ snapshots:
'@eslint/core': 0.17.0 '@eslint/core': 0.17.0
levn: 0.4.1 levn: 0.4.1
'@fontsource/dm-sans@5.2.8': {}
'@fontsource/plus-jakarta-sans@5.2.8': {}
'@fontsource/source-serif-4@5.2.9': {}
'@humanfs/core@0.19.2': '@humanfs/core@0.19.2':
dependencies: dependencies:
'@humanfs/types': 0.15.0 '@humanfs/types': 0.15.0

162
public/theme.js Normal file
View File

@@ -0,0 +1,162 @@
(function () {
var STORAGE_KEY = "theme";
var TOKENS = {
light: {
"--background": "#faf8f5",
"--foreground": "#1a1614",
"--muted": "#6b6560",
"--accent": "#4f46e5",
"--accent-secondary": "#ea580c",
"--accent-tertiary": "#db2777",
"--surface": "#ffffff",
"--surface-elevated": "#ffffff",
"--border": "#e8e4df",
"--border-strong": "#d6d0c8",
"--gradient-1": "#4f46e5",
"--gradient-2": "#ea580c",
"--gradient-3": "#db2777",
"--glow-sm": "0 8px 30px rgba(79, 70, 229, 0.12)",
"--glow-lg": "0 20px 50px rgba(234, 88, 12, 0.14)",
"--mesh-1": "rgba(79, 70, 229, 0.14)",
"--mesh-2": "rgba(234, 88, 12, 0.1)",
"--mesh-3": "rgba(219, 39, 119, 0.08)",
"--prose": "#44403c",
"--card-shadow":
"0 1px 2px rgba(26, 22, 20, 0.04), 0 8px 24px rgba(26, 22, 20, 0.06)",
"--callout-bg": "rgba(79, 70, 229, 0.06)",
"--callout-border": "rgba(79, 70, 229, 0.18)",
"--cover-from": "#3730a3",
"--cover-via": "#7c2d12",
"--cover-to": "#831843",
"--cover-glow": "rgba(79, 70, 229, 0.35)",
"--cta-from": "#4338ca",
"--cta-via": "#c2410c",
"--cta-to": "#9d174d",
"--cta-text": "#faf8f5",
"--cta-btn-bg": "#faf8f5",
"--cta-btn-text": "#3730a3",
},
dark: {
"--background": "#0f0e0d",
"--foreground": "#f5f2ed",
"--muted": "#a39e97",
"--accent": "#a5b4fc",
"--accent-secondary": "#fdba74",
"--accent-tertiary": "#f9a8d4",
"--surface": "#1a1816",
"--surface-elevated": "#242120",
"--border": "#2e2b28",
"--border-strong": "#3d3935",
"--gradient-1": "#818cf8",
"--gradient-2": "#fb923c",
"--gradient-3": "#f472b6",
"--glow-sm": "0 8px 32px rgba(129, 140, 248, 0.18)",
"--glow-lg": "0 20px 56px rgba(251, 146, 60, 0.12)",
"--mesh-1": "rgba(129, 140, 248, 0.12)",
"--mesh-2": "rgba(251, 146, 60, 0.1)",
"--mesh-3": "rgba(244, 114, 182, 0.08)",
"--prose": "#c9c2ba",
"--card-shadow":
"0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 32px rgba(0, 0, 0, 0.35)",
"--callout-bg": "rgba(129, 140, 248, 0.08)",
"--callout-border": "rgba(165, 180, 252, 0.22)",
"--cover-from": "#312e81",
"--cover-via": "#7c2d12",
"--cover-to": "#701a3a",
"--cover-glow": "rgba(129, 140, 248, 0.4)",
"--cta-from": "#1e1b4b",
"--cta-via": "#431407",
"--cta-to": "#500724",
"--cta-text": "#f5f2ed",
"--cta-btn-bg": "#f5f2ed",
"--cta-btn-text": "#312e81",
},
};
function readTheme() {
var attr = document.documentElement.getAttribute("data-theme");
if (attr === "dark" || attr === "light") return attr;
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}
function applyTheme(theme) {
var root = document.documentElement;
var tokens = TOKENS[theme];
if (!tokens) return;
if (theme === "dark") root.classList.add("dark");
else root.classList.remove("dark");
root.setAttribute("data-theme", theme);
root.style.colorScheme = theme;
for (var key in tokens) {
if (Object.prototype.hasOwnProperty.call(tokens, key)) {
root.style.setProperty(key, tokens[key]);
}
}
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch (e) {}
updateToggleUi(theme);
}
function updateToggleUi(theme) {
var isDark = theme === "dark";
var buttons = document.querySelectorAll("[data-theme-toggle]");
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
btn.setAttribute("aria-pressed", isDark ? "true" : "false");
btn.setAttribute(
"aria-label",
isDark ? "Switch to light mode" : "Switch to dark mode"
);
btn.title = isDark ? "Light mode" : "Dark mode";
var label = btn.querySelector("[data-theme-label]");
if (label) label.textContent = isDark ? "Light" : "Dark";
var sun = btn.querySelector("[data-theme-icon='sun']");
var moon = btn.querySelector("[data-theme-icon='moon']");
if (sun) sun.hidden = !isDark;
if (moon) moon.hidden = isDark;
}
}
function resolveInitial() {
try {
var stored = localStorage.getItem(STORAGE_KEY);
if (stored === "dark" || stored === "light") return stored;
} catch (e) {}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function toggleTheme() {
applyTheme(readTheme() === "dark" ? "light" : "dark");
}
window.NomadRoamTheme = {
apply: applyTheme,
toggle: toggleTheme,
read: readTheme,
};
applyTheme(resolveInitial());
document.addEventListener(
"click",
function (event) {
var target = event.target;
if (!target || !target.closest) return;
var btn = target.closest("[data-theme-toggle]");
if (!btn) return;
event.preventDefault();
event.stopPropagation();
toggleTheme();
},
true
);
})();