diff --git a/app/(pages)/titles/[id]/page.tsx b/app/(pages)/titles/[id]/page.tsx
index d8ba672..33e6ec9 100644
--- a/app/(pages)/titles/[id]/page.tsx
+++ b/app/(pages)/titles/[id]/page.tsx
@@ -10,6 +10,7 @@ import { titles } from "@/lib/db/schema";
import { getTitleWithChildren, importTitle } from "@/lib/services/metadata";
import { getUserTitleInfo } from "@/lib/services/tracking";
import { tmdbImageUrl } from "@/lib/tmdb/image";
+import { getTitleThemeStyle } from "@/lib/utils/title-theme";
import { TitleActions } from "./_components/title-actions";
import { TitleAvailability } from "./_components/title-availability";
import { TitleHero } from "./_components/title-hero";
@@ -72,8 +73,10 @@ export default async function TitleDetailPage({
const { title, seasons, availability } = result;
+ const themeStyle = getTitleThemeStyle(title.colorPalette);
+
return (
-
+
{});
+ // Lazy color extraction — await so the palette is available for theming
+ let palette = parseColorPalette(title.colorPalette);
+ if (!palette && title.posterPath) {
+ palette =
+ (await extractAndStoreColors(title.id, title.posterPath).catch(
+ () => null,
+ )) ?? null;
}
const resolvedTitle: ResolvedTitle = {
@@ -586,7 +590,7 @@ export async function getTitleWithChildren(id: string): Promise<{
voteAverage: title.voteAverage,
voteCount: title.voteCount,
status: title.status,
- colorPalette: parseColorPalette(title.colorPalette),
+ colorPalette: palette,
};
return { title: resolvedTitle, seasons: titleSeasons, availability };
diff --git a/lib/utils/title-theme.ts b/lib/utils/title-theme.ts
new file mode 100644
index 0000000..f2382e7
--- /dev/null
+++ b/lib/utils/title-theme.ts
@@ -0,0 +1,30 @@
+import type { ColorPalette } from "@/lib/types/title";
+
+function hexToRelativeLuminance(hex: string): number {
+ const r = Number.parseInt(hex.slice(1, 3), 16) / 255;
+ const g = Number.parseInt(hex.slice(3, 5), 16) / 255;
+ const b = Number.parseInt(hex.slice(5, 7), 16) / 255;
+ const toLinear = (c: number) =>
+ c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
+}
+
+export function getTitleThemeStyle(
+ palette: ColorPalette | null,
+): React.CSSProperties {
+ const color = palette?.vibrant;
+ if (!color) return {};
+
+ const luminance = hexToRelativeLuminance(color);
+ const foreground =
+ luminance > 0.3
+ ? "oklch(0.13 0.006 55)" // dark (current --primary-foreground)
+ : "oklch(0.93 0.015 80)"; // light (current --foreground)
+
+ return {
+ "--primary": color,
+ "--ring": color,
+ "--primary-foreground": foreground,
+ "--status-watching": color,
+ } as React.CSSProperties;
+}