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:
@@ -14,6 +14,7 @@ const securityHeaders = [
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com",
|
||||
"frame-src 'self' https://open.spotify.com",
|
||||
"frame-ancestors 'none'",
|
||||
].join("; "),
|
||||
},
|
||||
|
||||
+10
-75
@@ -1,77 +1,12 @@
|
||||
import SiteLayout from "./components/SiteLayout";
|
||||
import layoutStyles from "@/styles/Layout.module.css";
|
||||
import styles from "@/styles/World.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
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. He owes everyone.",
|
||||
revealed: 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>
|
||||
);
|
||||
export default function Redirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => { router.replace("/world/characters"); }, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { destination: "/world/characters", permanent: true } };
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ export interface SiteLayoutProps {
|
||||
}
|
||||
|
||||
const WORLD_LINKS = [
|
||||
{ href: "/lore", label: "Lore" },
|
||||
{ href: "/world", label: "World" },
|
||||
{ href: "/characters", label: "Characters" },
|
||||
{ href: "/music", label: "Music" },
|
||||
{ href: "/gallery", label: "Gallery" },
|
||||
{ href: "/world/lore", label: "Lore" },
|
||||
{ href: "/world/world", label: "World" },
|
||||
{ href: "/world/characters", label: "Characters" },
|
||||
{ href: "/world/music", label: "Music" },
|
||||
{ href: "/world/gallery", label: "Gallery" },
|
||||
];
|
||||
|
||||
export default function SiteLayout({
|
||||
|
||||
+10
-134
@@ -1,136 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import SiteLayout from "./components/SiteLayout";
|
||||
import layoutStyles from "@/styles/Layout.module.css";
|
||||
import styles from "@/styles/Gallery.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type GalleryTab = "official" | "fan";
|
||||
|
||||
// Placeholder art items — swap src for real image paths when assets are ready
|
||||
const OFFICIAL_ART: { src: string; alt: string; caption?: string }[] = [
|
||||
// { src: "/gallery/official/blb-key-art.jpg", alt: "Bone Lord Bob key art", caption: "Key Art — Season 1" },
|
||||
];
|
||||
|
||||
const FAN_ART: { src: string; alt: string; artist?: string; artistUrl?: string }[] = [
|
||||
// { 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'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'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>
|
||||
);
|
||||
export default function Redirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => { router.replace("/world/gallery"); }, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { destination: "/world/gallery", permanent: true } };
|
||||
}
|
||||
|
||||
+10
-61
@@ -1,63 +1,12 @@
|
||||
import SiteLayout from "./components/SiteLayout";
|
||||
import layoutStyles from "@/styles/Layout.module.css";
|
||||
import styles from "@/styles/World.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Lore() {
|
||||
return (
|
||||
<SiteLayout
|
||||
title="Lore"
|
||||
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"
|
||||
>
|
||||
{/* 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>
|
||||
);
|
||||
export default function Redirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => { router.replace("/world/lore"); }, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { destination: "/world/lore", permanent: true } };
|
||||
}
|
||||
|
||||
+10
-80
@@ -1,82 +1,12 @@
|
||||
import SiteLayout from "./components/SiteLayout";
|
||||
import layoutStyles from "@/styles/Layout.module.css";
|
||||
import styles from "@/styles/World.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// Placeholder — tracks will be added when official BLB soundtrack assets are available
|
||||
const TRACKS: { title: string; vibe: string; file?: string }[] = [
|
||||
// { title: "Bone Kingdom Theme", vibe: "Dark orchestral", file: "/music/bone-kingdom-theme.mp3" },
|
||||
];
|
||||
|
||||
export default function Music() {
|
||||
return (
|
||||
<SiteLayout
|
||||
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'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>
|
||||
);
|
||||
export default function Redirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => { router.replace("/world/music"); }, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { destination: "/world/music", permanent: true } };
|
||||
}
|
||||
|
||||
+10
-77
@@ -1,79 +1,12 @@
|
||||
import SiteLayout from "./components/SiteLayout";
|
||||
import layoutStyles from "@/styles/Layout.module.css";
|
||||
import styles from "@/styles/World.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function WorldPage() {
|
||||
return (
|
||||
<SiteLayout
|
||||
title="World"
|
||||
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"
|
||||
>
|
||||
{/* 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'd want to be born into. But one you won'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'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'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>
|
||||
);
|
||||
export default function Redirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => { router.replace("/world/world"); }, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { destination: "/world/world", permanent: true } };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user