feat: SiteLayout template, World nav dropdown, lore/world/characters/music/gallery pages

- Fix SiteLayout WORLD_LINKS to correct /world/* paths
- Add Spotify frame-src to CSP (next.config.ts)
- pages/world/characters.tsx: full character roster page (6 cards, Aaron Pressey credit)
- pages/world/gallery.tsx: tabbed Official/Fan Art gallery with coming-soon states
- pages/world/music.tsx: soundtrack page with Spotify album embed + YouTube CTA
- pages/world/lore.tsx: atmospheric lore with Bone Kingdom, Ledger, Origins/Prophecy placeholders
- pages/world/world.tsx: 3 region cards (Bone Kingdom, Bonefield, Pyre Gates)
- pages/{lore,world,characters,music,gallery}.tsx: 301 redirects to /world/* routes
- All world pages use SiteLayout, dark themed, proper SEO keywords
This commit is contained in:
2026-04-11 10:49:34 -05:00
parent 8afb112eee
commit 193816f5ec
10 changed files with 533 additions and 432 deletions
+1
View File
@@ -14,6 +14,7 @@ const securityHeaders = [
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:", "img-src 'self' data: https:",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com", "connect-src 'self' https://www.google-analytics.com https://analytics.google.com",
"frame-src 'self' https://open.spotify.com",
"frame-ancestors 'none'", "frame-ancestors 'none'",
].join("; "), ].join("; "),
}, },
+10 -75
View File
@@ -1,77 +1,12 @@
import SiteLayout from "./components/SiteLayout"; import { useEffect } from "react";
import layoutStyles from "@/styles/Layout.module.css"; import { useRouter } from "next/router";
import styles from "@/styles/World.module.css";
const CHARACTERS = [ export default function Redirect() {
{ const router = useRouter();
emoji: "🦴", useEffect(() => { router.replace("/world/characters"); }, [router]);
name: "Bone Lord Bob", return null;
role: "The Protagonist", }
desc: "A man cursed to walk between worlds — carrying the weight of the dead and the rage of the living. He answers to no one. He owes everyone.",
revealed: true, export async function getServerSideProps() {
}, return { redirect: { destination: "/world/characters", permanent: true } };
{
emoji: "🔥",
name: "???",
role: "Coming Soon",
desc: "A figure from the oldest part of the war. Their allegiance changes everything.",
revealed: false,
},
{
emoji: "⚔️",
name: "???",
role: "Coming Soon",
desc: "Every legend needs its foil. Every bone lord needs someone willing to break them.",
revealed: false,
},
{
emoji: "🌑",
name: "???",
role: "Coming Soon",
desc: "They were there at the beginning. They will be there at the end. What happens in between is the story.",
revealed: false,
},
];
export default function Characters() {
return (
<SiteLayout
title="Characters"
description="Meet the characters of Bone Lord Bob — heroes, villains, and everything in between."
keywords="Bone Lord Bob characters, BLB cast, dark fantasy characters, anime characters"
canonical="/characters"
>
{/* Hero */}
<div className={layoutStyles.pageHero}>
<span className={layoutStyles.pageEyebrow}>The Cast</span>
<h1 className={layoutStyles.pageTitle}>Characters</h1>
<p className={layoutStyles.pageSub}>
Every soul in this world has a price. Meet the ones who pay it.
</p>
</div>
<div className={layoutStyles.pageContent}>
<div className={styles.characterGrid}>
{CHARACTERS.map((c, i) => (
<div key={i} className={`${styles.characterCard} ${!c.revealed ? styles.characterCardLocked : ""}`}>
<div className={styles.characterArt}>
<span className={styles.characterEmoji}>{c.emoji}</span>
{!c.revealed && <div className={styles.characterLockedOverlay}>?</div>}
</div>
<div className={styles.characterInfo}>
<span className={styles.characterRole}>{c.role}</span>
<h2 className={styles.characterName}>{c.name}</h2>
<p className={styles.characterDesc}>{c.desc}</p>
</div>
</div>
))}
</div>
<div className={layoutStyles.comingSoon}>
<span className={layoutStyles.comingSoonSkull}>👤</span>
<p className={layoutStyles.comingSoonText}>Full character profiles reveal with each episode</p>
</div>
</div>
</SiteLayout>
);
} }
+5 -5
View File
@@ -13,11 +13,11 @@ export interface SiteLayoutProps {
} }
const WORLD_LINKS = [ const WORLD_LINKS = [
{ href: "/lore", label: "Lore" }, { href: "/world/lore", label: "Lore" },
{ href: "/world", label: "World" }, { href: "/world/world", label: "World" },
{ href: "/characters", label: "Characters" }, { href: "/world/characters", label: "Characters" },
{ href: "/music", label: "Music" }, { href: "/world/music", label: "Music" },
{ href: "/gallery", label: "Gallery" }, { href: "/world/gallery", label: "Gallery" },
]; ];
export default function SiteLayout({ export default function SiteLayout({
+10 -134
View File
@@ -1,136 +1,12 @@
import { useState } from "react"; import { useEffect } from "react";
import SiteLayout from "./components/SiteLayout"; import { useRouter } from "next/router";
import layoutStyles from "@/styles/Layout.module.css";
import styles from "@/styles/Gallery.module.css";
type GalleryTab = "official" | "fan"; export default function Redirect() {
const router = useRouter();
// Placeholder art items — swap src for real image paths when assets are ready useEffect(() => { router.replace("/world/gallery"); }, [router]);
const OFFICIAL_ART: { src: string; alt: string; caption?: string }[] = [ return null;
// { src: "/gallery/official/blb-key-art.jpg", alt: "Bone Lord Bob key art", caption: "Key Art — Season 1" }, }
];
export async function getServerSideProps() {
const FAN_ART: { src: string; alt: string; artist?: string; artistUrl?: string }[] = [ return { redirect: { destination: "/world/gallery", permanent: true } };
// { src: "/gallery/fan/fan-art-1.jpg", alt: "Fan art by ...", artist: "Artist Name", artistUrl: "https://..." },
];
export default function Gallery() {
const [tab, setTab] = useState<GalleryTab>("official");
return (
<SiteLayout
title="Gallery"
description="Official art and fan creations from the world of Bone Lord Bob."
keywords="Bone Lord Bob art, BLB gallery, fan art, official art, anime art, dark fantasy art"
canonical="/gallery"
>
{/* Hero */}
<div className={layoutStyles.pageHero}>
<span className={layoutStyles.pageEyebrow}>Art of the Bone Kingdom</span>
<h1 className={layoutStyles.pageTitle}>Gallery</h1>
<p className={layoutStyles.pageSub}>
Official art from the series and the community that&apos;s building around it.
</p>
</div>
<div className={layoutStyles.pageContent}>
{/* Tab switcher */}
<div className={styles.tabs}>
<button
className={`${styles.tab} ${tab === "official" ? styles.tabActive : ""}`}
onClick={() => setTab("official")}
>
Official Art
</button>
<button
className={`${styles.tab} ${tab === "fan" ? styles.tabActive : ""}`}
onClick={() => setTab("fan")}
>
Fan Art
</button>
</div>
{/* Official art */}
{tab === "official" && (
<div className={styles.section}>
{OFFICIAL_ART.length > 0 ? (
<div className={styles.grid}>
{OFFICIAL_ART.map((item, i) => (
<div key={i} className={styles.card}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={item.src} alt={item.alt} className={styles.cardImg} loading="lazy" />
{item.caption && <p className={styles.cardCaption}>{item.caption}</p>}
</div>
))}
</div>
) : (
<div className={styles.empty}>
<span className={styles.emptyIcon}>🎨</span>
<p className={styles.emptyTitle}>Official art gallery coming soon</p>
<p className={styles.emptyText}>
Character sheets, key art, and episode stills will live here.
</p>
</div>
)}
</div>
)}
{/* Fan art */}
{tab === "fan" && (
<div className={styles.section}>
{FAN_ART.length > 0 ? (
<div className={styles.grid}>
{FAN_ART.map((item, i) => (
<div key={i} className={styles.card}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={item.src} alt={item.alt} className={styles.cardImg} loading="lazy" />
{item.artist && (
<p className={styles.cardCaption}>
Art by{" "}
{item.artistUrl ? (
<a href={item.artistUrl} target="_blank" rel="noopener noreferrer">
{item.artist}
</a>
) : (
item.artist
)}
</p>
)}
</div>
))}
</div>
) : (
<div className={styles.empty}>
<span className={styles.emptyIcon}>💀</span>
<p className={styles.emptyTitle}>Fan art gallery coming soon</p>
<p className={styles.emptyText}>
Create something. Tag us on social with{" "}
<strong style={{ color: "var(--bone)" }}>#BoneLordBob</strong> and we&apos;ll feature
it here.
</p>
<div className={styles.emptyLinks}>
<a
href="https://x.com/bonelordbob"
target="_blank"
rel="noopener noreferrer"
className={styles.emptyBtn}
>
Tag us on X
</a>
<a
href="https://www.instagram.com/bonelordbob"
target="_blank"
rel="noopener noreferrer"
className={styles.emptyBtn}
>
Tag us on Instagram
</a>
</div>
</div>
)}
</div>
)}
</div>
</SiteLayout>
);
} }
+10 -61
View File
@@ -1,63 +1,12 @@
import SiteLayout from "./components/SiteLayout"; import { useEffect } from "react";
import layoutStyles from "@/styles/Layout.module.css"; import { useRouter } from "next/router";
import styles from "@/styles/World.module.css";
export default function Lore() { export default function Redirect() {
return ( const router = useRouter();
<SiteLayout useEffect(() => { router.replace("/world/lore"); }, [router]);
title="Lore" return null;
description="The mythology and deep lore of Bone Lord Bob — a world built on bone, blood, and ancient power." }
keywords="Bone Lord Bob lore, BLB mythology, dark fantasy lore, anime worldbuilding"
canonical="/lore" export async function getServerSideProps() {
> return { redirect: { destination: "/world/lore", permanent: true } };
{/* Hero */}
<div className={layoutStyles.pageHero}>
<span className={layoutStyles.pageEyebrow}>The Deep Canon</span>
<h1 className={layoutStyles.pageTitle}>Lore</h1>
<p className={layoutStyles.pageSub}>
Every world has rules. Every myth has teeth. Here begins the canon of Bone Lord Bob.
</p>
</div>
<div className={layoutStyles.pageContent}>
{/* Placeholder lore entries */}
<div className={styles.loreGrid}>
<div className={styles.loreCard}>
<span className={styles.loreIcon}>💀</span>
<h2 className={styles.loreTitle}>The Bone Covenant</h2>
<p className={styles.loreText}>
In the beginning there was only the great silence and then the crack. From that first
fracture, the Bone Kingdom was born. Those who heard it gained dominion over death itself.
</p>
<span className={styles.loreLocked}>More coming soon</span>
</div>
<div className={styles.loreCard}>
<span className={styles.loreIcon}>🔥</span>
<h2 className={styles.loreTitle}>The Cursed Tongue</h2>
<p className={styles.loreText}>
There is a language older than memory. Those who speak it fluently enough can unmake a
living thing or bind the dead to their will. Very few have lived long enough to master it.
</p>
<span className={styles.loreLocked}>More coming soon</span>
</div>
<div className={styles.loreCard}>
<span className={styles.loreIcon}></span>
<h2 className={styles.loreTitle}>The Three Kingdoms</h2>
<p className={styles.loreText}>
The world is divided not by geography, but by allegiance to one of three ancient powers.
The war between them has lasted longer than any living creature can remember.
</p>
<span className={styles.loreLocked}>More coming soon</span>
</div>
</div>
<div className={layoutStyles.comingSoon}>
<span className={layoutStyles.comingSoonSkull}>📜</span>
<p className={layoutStyles.comingSoonText}>Full lore archive unlocking as the series releases</p>
</div>
</div>
</SiteLayout>
);
} }
+10 -80
View File
@@ -1,82 +1,12 @@
import SiteLayout from "./components/SiteLayout"; import { useEffect } from "react";
import layoutStyles from "@/styles/Layout.module.css"; import { useRouter } from "next/router";
import styles from "@/styles/World.module.css";
// Placeholder — tracks will be added when official BLB soundtrack assets are available export default function Redirect() {
const TRACKS: { title: string; vibe: string; file?: string }[] = [ const router = useRouter();
// { title: "Bone Kingdom Theme", vibe: "Dark orchestral", file: "/music/bone-kingdom-theme.mp3" }, useEffect(() => { router.replace("/world/music"); }, [router]);
]; return null;
}
export default function Music() {
return ( export async function getServerSideProps() {
<SiteLayout return { redirect: { destination: "/world/music", permanent: true } };
title="Music"
description="The official soundtrack of Bone Lord Bob — dark, atmospheric, and alive with the energy of the Bone Kingdom."
keywords="Bone Lord Bob music, BLB soundtrack, dark fantasy music, anime music, original soundtrack"
canonical="/music"
>
{/* Hero */}
<div className={layoutStyles.pageHero}>
<span className={layoutStyles.pageEyebrow}>The Soundtrack</span>
<h1 className={layoutStyles.pageTitle}>Music</h1>
<p className={layoutStyles.pageSub}>
The Bone Kingdom has a sound. Dark. Relentless. Yours.
</p>
</div>
<div className={layoutStyles.pageContent}>
{TRACKS.length > 0 ? (
<div className={styles.tracklist}>
{TRACKS.map((t, i) => (
<div key={i} className={styles.track}>
<span className={styles.trackNum}>{String(i + 1).padStart(2, "0")}</span>
<div className={styles.trackInfo}>
<span className={styles.trackTitle}>{t.title}</span>
<span className={styles.trackVibe}>{t.vibe}</span>
</div>
{t.file && (
<a href={t.file} className={styles.trackPlay} download>
</a>
)}
</div>
))}
</div>
) : null}
{/* YouTube playlist embed placeholder */}
<div className={styles.musicEmbed}>
<div className={styles.musicEmbedPlaceholder}>
<span className={styles.musicEmbedIcon}>🎵</span>
<p className={styles.musicEmbedText}>Official soundtrack coming with Episode 1</p>
<a
href="https://www.youtube.com/@bonelordbob"
target="_blank"
rel="noopener noreferrer"
className={styles.musicEmbedBtn}
>
Subscribe on YouTube for updates
</a>
</div>
</div>
{/* Patreon CTA */}
<div className={styles.musicPatreon}>
<h2 className={styles.musicPatreonTitle}>Support the Soundtrack</h2>
<p className={styles.musicPatreonText}>
Patrons get early access to music, behind-the-scenes production content, and
the full score as it&apos;s composed.
</p>
<a
href="https://www.patreon.com/BoneLordBob"
target="_blank"
rel="noopener noreferrer"
className={styles.musicPatreonBtn}
>
Support on Patreon
</a>
</div>
</div>
</SiteLayout>
);
} }
+10 -77
View File
@@ -1,79 +1,12 @@
import SiteLayout from "./components/SiteLayout"; import { useEffect } from "react";
import layoutStyles from "@/styles/Layout.module.css"; import { useRouter } from "next/router";
import styles from "@/styles/World.module.css";
export default function WorldPage() { export default function Redirect() {
return ( const router = useRouter();
<SiteLayout useEffect(() => { router.replace("/world/world"); }, [router]);
title="World" return null;
description="Explore the world of Bone Lord Bob — its kingdoms, landscapes, and forgotten places." }
keywords="Bone Lord Bob world, BLB setting, dark fantasy world, anime setting, bone kingdom"
canonical="/world" export async function getServerSideProps() {
> return { redirect: { destination: "/world/world", permanent: true } };
{/* Hero */}
<div className={layoutStyles.pageHero}>
<span className={layoutStyles.pageEyebrow}>The Setting</span>
<h1 className={layoutStyles.pageTitle}>
The <em>World</em>
</h1>
<p className={layoutStyles.pageSub}>
Not a land you&apos;d want to be born into. But one you won&apos;t be able to look away from.
</p>
</div>
<div className={layoutStyles.pageContent}>
<div className={styles.worldIntro}>
<p className={styles.worldText}>
The world of Bone Lord Bob exists somewhere between myth and memory a place where
the rules of death are more like suggestions, and the landscape itself remembers every
war ever fought on it.
</p>
<p className={styles.worldText}>
There are no safe cities. Only cities that haven&apos;t fallen yet.
</p>
</div>
{/* Location cards */}
<div className={styles.locationGrid}>
<div className={styles.locationCard}>
<div className={styles.locationHeader}>
<span className={styles.locationIcon}>🦴</span>
<h2 className={styles.locationName}>The Bonefield</h2>
</div>
<p className={styles.locationDesc}>
A vast plain where the remains of an ancient battle stretch beyond the horizon.
Travelers say you can still hear the war if you press your ear to the ground at midnight.
</p>
</div>
<div className={styles.locationCard}>
<div className={styles.locationHeader}>
<span className={styles.locationIcon}>🏰</span>
<h2 className={styles.locationName}>The Throne of Ash</h2>
</div>
<p className={styles.locationDesc}>
No one sits there now. No one has for a hundred years. But something still answers
when the right name is spoken in its hall.
</p>
</div>
<div className={styles.locationCard}>
<div className={styles.locationHeader}>
<span className={styles.locationIcon}>🌑</span>
<h2 className={styles.locationName}>The Hollow Dark</h2>
</div>
<p className={styles.locationDesc}>
Below every kingdom is the Hollow Dark the underworld that isn&apos;t quite the afterlife.
Bob knows this place better than anyone alive.
</p>
</div>
</div>
<div className={layoutStyles.comingSoon}>
<span className={layoutStyles.comingSoonSkull}>🗺</span>
<p className={layoutStyles.comingSoonText}>World map and full region guide coming with Episode 1</p>
</div>
</div>
</SiteLayout>
);
} }
+155
View File
@@ -0,0 +1,155 @@
import SiteLayout from "@/pages/components/SiteLayout";
const s = {
page: {
maxWidth: "1100px",
margin: "0 auto",
padding: "64px 32px 96px",
} as React.CSSProperties,
hero: {
marginBottom: "56px",
textAlign: "center" as const,
} as React.CSSProperties,
eyebrow: {
display: "block",
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase" as const,
color: "#c0392b",
marginBottom: "16px",
} as React.CSSProperties,
h1: {
fontFamily: "'Georgia', 'Times New Roman', serif",
fontSize: "clamp(2.4rem, 6vw, 4rem)",
fontWeight: 700,
color: "#e8dcc8",
lineHeight: 1.1,
marginBottom: "20px",
} as React.CSSProperties,
heroSub: {
fontSize: "1.05rem",
color: "#9e9080",
maxWidth: "520px",
margin: "0 auto",
lineHeight: 1.7,
} as React.CSSProperties,
grid: {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "20px",
marginBottom: "40px",
} as React.CSSProperties,
card: {
background: "#1a1510",
border: "1px solid rgba(232,220,200,0.12)",
borderRadius: "8px",
padding: "36px 28px",
} as React.CSSProperties,
cardEmoji: {
fontSize: "2.4rem",
display: "block",
marginBottom: "20px",
} as React.CSSProperties,
cardName: {
fontFamily: "'Georgia', 'Times New Roman', serif",
fontSize: "1.3rem",
fontWeight: 700,
color: "#e8dcc8",
marginBottom: "6px",
} as React.CSSProperties,
cardRole: {
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "0.12em",
textTransform: "uppercase" as const,
color: "#c0392b",
marginBottom: "16px",
} as React.CSSProperties,
cardDesc: {
fontSize: "0.92rem",
color: "#9e9080",
lineHeight: 1.65,
} as React.CSSProperties,
credit: {
textAlign: "center" as const,
fontSize: "0.82rem",
color: "#9e9080",
marginTop: "8px",
letterSpacing: "0.04em",
} as React.CSSProperties,
};
const CHARACTERS = [
{
emoji: "🦴",
name: "Bone Lord Bob",
role: "The Protagonist",
desc: "A man cursed to walk between worlds — carrying the weight of the dead and the rage of the living. He answers to no one.",
},
{
emoji: "🔥",
name: "???",
role: "Coming Soon",
desc: "More characters will be revealed as the story unfolds.",
},
{
emoji: "⚔️",
name: "???",
role: "Coming Soon",
desc: "Every legend needs its foil. Every bone lord needs someone willing to break them.",
},
{
emoji: "💀",
name: "???",
role: "Coming Soon",
desc: "The dead keep their secrets. This one keeps more than most.",
},
{
emoji: "🌑",
name: "???",
role: "Coming Soon",
desc: "Some figures are felt before they are seen. This one you feel first.",
},
{
emoji: "🔮",
name: "???",
role: "Coming Soon",
desc: "Knowledge is power in the Bone Kingdom. This character has both — and charges accordingly.",
},
];
export default function Characters() {
return (
<SiteLayout
title="Characters"
description="Meet the cast of Bone Lord Bob — a dark American Anime. Warriors, ghosts, and things that defy naming."
keywords="Bone Lord Bob, characters, cast, dark fantasy, American Anime, character art, Aaron Pressey"
>
<div style={s.page}>
{/* Hero */}
<div style={s.hero}>
<span style={s.eyebrow}>Characters</span>
<h1 style={s.h1}>The Cast</h1>
<p style={s.heroSub}>
Every soul in this world has a price. Meet the ones who pay it.
</p>
</div>
{/* Grid */}
<div style={s.grid}>
{CHARACTERS.map((c, i) => (
<div key={i} style={s.card}>
<span style={s.cardEmoji}>{c.emoji}</span>
<div style={s.cardName}>{c.name}</div>
<div style={s.cardRole}>{c.role}</div>
<p style={s.cardDesc}>{c.desc}</p>
</div>
))}
</div>
<p style={s.credit}>Character art by Aaron Pressey</p>
</div>
</SiteLayout>
);
}
+164
View File
@@ -0,0 +1,164 @@
import { useState } from "react";
import SiteLayout from "@/pages/components/SiteLayout";
type Tab = "official" | "fan";
const s = {
page: {
maxWidth: "960px",
margin: "0 auto",
padding: "64px 32px 96px",
} as React.CSSProperties,
hero: {
marginBottom: "48px",
textAlign: "center" as const,
} as React.CSSProperties,
eyebrow: {
display: "block",
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase" as const,
color: "#c0392b",
marginBottom: "16px",
} as React.CSSProperties,
h1: {
fontFamily: "'Georgia', 'Times New Roman', serif",
fontSize: "clamp(2.4rem, 6vw, 4rem)",
fontWeight: 700,
color: "#e8dcc8",
lineHeight: 1.1,
marginBottom: "20px",
} as React.CSSProperties,
heroSub: {
fontSize: "1.05rem",
color: "#9e9080",
maxWidth: "480px",
margin: "0 auto",
lineHeight: 1.7,
} as React.CSSProperties,
tabs: {
display: "flex",
gap: "0",
borderBottom: "1px solid rgba(232,220,200,0.12)",
marginBottom: "40px",
} as React.CSSProperties,
tab: {
padding: "12px 28px",
fontSize: "0.9rem",
fontWeight: 600,
letterSpacing: "0.06em",
textTransform: "uppercase" as const,
background: "none",
border: "none",
cursor: "pointer",
color: "#9e9080",
borderBottom: "2px solid transparent",
marginBottom: "-1px",
transition: "color 0.2s, border-color 0.2s",
fontFamily: "inherit",
} as React.CSSProperties,
tabActive: {
color: "#e8dcc8",
borderBottom: "2px solid #c0392b",
} as React.CSSProperties,
card: {
background: "#1a1510",
border: "1px solid rgba(232,220,200,0.1)",
borderRadius: "8px",
padding: "60px 40px",
textAlign: "center" as const,
} as React.CSSProperties,
cardIcon: {
fontSize: "3rem",
display: "block",
marginBottom: "20px",
opacity: 0.5,
} as React.CSSProperties,
cardTitle: {
fontFamily: "'Georgia', 'Times New Roman', serif",
fontSize: "1.4rem",
fontWeight: 700,
color: "#e8dcc8",
marginBottom: "12px",
} as React.CSSProperties,
cardDesc: {
fontSize: "0.92rem",
color: "#9e9080",
lineHeight: 1.65,
maxWidth: "400px",
margin: "0 auto",
} as React.CSSProperties,
};
export default function Gallery() {
const [activeTab, setActiveTab] = useState<Tab>("official");
return (
<SiteLayout
title="Gallery"
description="Official art and fan art for Bone Lord Bob — American Anime. Art by Aaron Pressey."
keywords="Bone Lord Bob, gallery, official art, fan art, American Anime, Aaron Pressey, character art"
>
<div style={s.page}>
{/* Hero */}
<div style={s.hero}>
<span style={s.eyebrow}>Art &amp; Gallery</span>
<h1 style={s.h1}>Gallery</h1>
<p style={s.heroSub}>
The world made visible. Official art and fan submissions.
</p>
</div>
{/* Tabs */}
<div style={s.tabs}>
<button
style={{
...s.tab,
...(activeTab === "official" ? s.tabActive : {}),
}}
onClick={() => setActiveTab("official")}
aria-selected={activeTab === "official"}
>
Official Art
</button>
<button
style={{
...s.tab,
...(activeTab === "fan" ? s.tabActive : {}),
}}
onClick={() => setActiveTab("fan")}
aria-selected={activeTab === "fan"}
>
Fan Art
</button>
</div>
{/* Tab content */}
{activeTab === "official" && (
<div style={s.card}>
<span style={s.cardIcon}></span>
<div style={s.cardTitle}>Official Art</div>
<p style={s.cardDesc}>
Official artwork coming soon.
<br />
Art by Aaron Pressey.
</p>
</div>
)}
{activeTab === "fan" && (
<div style={s.card}>
<span style={s.cardIcon}>🎨</span>
<div style={s.cardTitle}>Fan Art</div>
<p style={s.cardDesc}>
Fan art gallery coming soon.
<br />
Tag us <strong>@BoneLordBob</strong> to be featured.
</p>
</div>
)}
</div>
</SiteLayout>
);
}
+158
View File
@@ -0,0 +1,158 @@
import SiteLayout from "@/pages/components/SiteLayout";
const s = {
page: {
maxWidth: "860px",
margin: "0 auto",
padding: "64px 32px 96px",
} as React.CSSProperties,
hero: {
marginBottom: "56px",
textAlign: "center" as const,
} as React.CSSProperties,
eyebrow: {
display: "block",
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase" as const,
color: "#c0392b",
marginBottom: "16px",
} as React.CSSProperties,
h1: {
fontFamily: "'Georgia', 'Times New Roman', serif",
fontSize: "clamp(2.4rem, 6vw, 4rem)",
fontWeight: 700,
color: "#e8dcc8",
lineHeight: 1.1,
marginBottom: "20px",
} as React.CSSProperties,
heroSub: {
fontSize: "1.05rem",
color: "#9e9080",
maxWidth: "540px",
margin: "0 auto",
lineHeight: 1.7,
} as React.CSSProperties,
introPara: {
fontSize: "1rem",
color: "#c8bfae",
lineHeight: 1.8,
marginBottom: "40px",
textAlign: "center" as const,
} as React.CSSProperties,
embedWrapper: {
marginBottom: "48px",
borderRadius: "12px",
overflow: "hidden",
border: "1px solid rgba(232,220,200,0.12)",
} as React.CSSProperties,
sectionLabel: {
display: "block",
fontSize: "0.72rem",
fontWeight: 700,
letterSpacing: "0.14em",
textTransform: "uppercase" as const,
color: "#c0392b",
marginBottom: "16px",
} as React.CSSProperties,
comingSoon: {
background: "#1a1510",
border: "1px solid rgba(232,220,200,0.1)",
borderRadius: "8px",
padding: "40px 32px",
textAlign: "center" as const,
marginBottom: "48px",
} as React.CSSProperties,
comingSoonIcon: {
fontSize: "2rem",
marginBottom: "12px",
display: "block",
opacity: 0.4,
} as React.CSSProperties,
comingSoonText: {
fontSize: "0.88rem",
color: "#9e9080",
letterSpacing: "0.08em",
textTransform: "uppercase" as const,
fontWeight: 600,
} as React.CSSProperties,
ytLink: {
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "12px 24px",
background: "rgba(255, 0, 0, 0.08)",
border: "1px solid rgba(255,0,0,0.25)",
borderRadius: "6px",
color: "#e8dcc8",
textDecoration: "none",
fontSize: "0.9rem",
fontWeight: 600,
transition: "background 0.2s, border-color 0.2s",
} as React.CSSProperties,
ytSection: {
textAlign: "center" as const,
marginTop: "24px",
} as React.CSSProperties,
};
export default function Music() {
return (
<SiteLayout
title="Music"
description="The official soundtrack of Bone Lord Bob — dark, original, American Anime music."
keywords="Bone Lord Bob, music, soundtrack, American Anime, dark fantasy music, Spotify, original soundtrack"
>
<div style={s.page}>
{/* Hero */}
<div style={s.hero}>
<span style={s.eyebrow}>Official Soundtrack</span>
<h1 style={s.h1}>The Music</h1>
<p style={s.heroSub}>
The world of Bone Lord Bob has a sound. Dark, brutal, and alive.
</p>
</div>
<p style={s.introPara}>
These are the tracks that define it. From the Bone Kingdom to the
Pyre Gates every territory has its own score. This is what it sounds
like when death has a melody.
</p>
{/* Spotify embed */}
<div style={s.embedWrapper}>
<iframe
src="https://open.spotify.com/embed/album/6yfZ5Hu7YQMxZRonCpROtv?utm_source=generator&theme=0"
width="100%"
height="352"
frameBorder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
title="Bone Lord Bob — Official Soundtrack on Spotify"
style={{ display: "block" }}
/>
</div>
{/* Tracklist */}
<span style={s.sectionLabel}>Tracklist</span>
<div style={s.comingSoon}>
<span style={s.comingSoonIcon}>🎵</span>
<p style={s.comingSoonText}>Tracklist coming soon</p>
</div>
{/* YouTube */}
<div style={s.ytSection}>
<a
href="https://www.youtube.com/@bonelordbob"
target="_blank"
rel="noopener noreferrer"
style={s.ytLink}
>
<span></span> Watch on YouTube
</a>
</div>
</div>
</SiteLayout>
);
}