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'",
|
"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
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-61
@@ -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
@@ -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'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
@@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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