Operator Branding
Every visual element of the Predictu iframe app is themeable through the OperatorBranding interface. Operators configure 17 color tokens, 3 image URLs, and a border radius value through the Operator Dashboard. The BrandingInjector component converts these tokens to CSS custom properties and injects them on <html>, instantly reskinning the entire UI to match the operator's casino brand.
OperatorBranding Interface
The branding schema is stored as JSONB in the operators.branding column and maps 1:1 to CSS custom properties. All color values use the HSL format without the hsl() wrapper, e.g. "120 100% 45%".
TypeScript Definition
interface OperatorBranding {
// Core colors (HSL strings)
primaryColor: string;
primaryForeground: string;
accentColor: string;
// Surface colors (HSL strings)
backgroundColor: string;
foregroundColor: string;
cardColor: string;
cardHoverColor: string;
borderColor: string;
inputColor: string;
mutedColor: string;
mutedForeground: string;
// Button colors (HSL strings)
buttonYesColor: string;
buttonNoColor: string;
// Image URLs
logoUrl: string | null;
bannerUrl: string | null;
faviconUrl: string | null;
// Layout
borderRadius: string; // e.g. "0.75rem"
}Field Reference
Complete reference for every field in the OperatorBranding interface, including what it controls, the CSS variables it maps to, and the default value.
Core Colors
| Field | CSS Variable(s) | Controls | Default |
|---|---|---|---|
primaryColor | --primary, --ring, --green | Primary brand color. Used for buttons, links, active states, focus rings, badges, and the "YES" accent throughout the UI. | 120 100% 43% |
primaryForeground | --primary-foreground | Text color on primary-colored backgrounds. Must have sufficient contrast against primaryColor. | 120 30% 5% |
accentColor | --accent, --accent-foreground | Secondary accent color. Used for hover states, secondary badges, and accent borders. Often the same as primaryColor. | 120 100% 43% |
Surface Colors
| Field | CSS Variable(s) | Controls | Default |
|---|---|---|---|
backgroundColor | --background | Page background color. The base layer behind all content. | 120 18% 5% |
foregroundColor | --foreground, --card-foreground, --popover-foreground | Primary text color. Used for headings, body text, and card content. | 100 8% 82% |
cardColor | --card, --popover | Card and popover background color. Used for market cards, trade tickets, modals, and dropdown menus. | 120 14% 11% |
cardHoverColor | --card-hover | Card background on hover. Provides visual feedback when users mouse over interactive cards. | 120 14% 14% |
borderColor | --border | Border color for cards, dividers, table rows, and input fields. | 120 10% 17% |
inputColor | --input, --secondary, --muted | Input field background, secondary surface color, and muted background. Used for text inputs, search bars, toggle groups, and tab bars. | 120 12% 10% |
mutedColor | --muted | Muted background surface. Used for subtle section backgrounds and disabled states. If set, overrides the --muted mapping from inputColor. | 120 10% 9% |
mutedForeground | --muted-foreground | Secondary/muted text color. Used for labels, descriptions, timestamps, and placeholder text. | 120 6% 44% |
Button Colors
| Field | CSS Variable(s) | Controls | Default |
|---|---|---|---|
buttonYesColor | --up, --up-bg (derived at 15% opacity) | Color for YES/Buy buttons, positive indicators, profit amounts, and the green side of the orderbook. The --up-bg is auto-derived at 15% opacity for subtle backgrounds. | 120 100% 43% |
buttonNoColor | --down, --down-bg (derived at 15% opacity) | Color for NO/Sell buttons, negative indicators, loss amounts, and the red side of the orderbook. The --down-bg is auto-derived at 15% opacity for subtle backgrounds. | 0 72% 51% |
Image URLs
| Field | Controls | Format | Default |
|---|---|---|---|
logoUrl | Operator logo displayed in the header/nav area. Replaces the default Predictu logo in embed mode. | URL to PNG, SVG, or WebP. Recommended: 200x50px, transparent background. | null (Predictu logo) |
bannerUrl | Hero banner image on the market discovery page. If set, displayed as a full-width banner above the market list. | URL to PNG, JPG, or WebP. Recommended: 1200x300px. | null (no banner) |
faviconUrl | Browser favicon for the iframe app. In embed mode, this is less important since the browser shows the parent page's favicon. | URL to ICO, PNG, or SVG. Recommended: 32x32px. | null (Predictu favicon) |
Layout
| Field | CSS Variable | Controls | Default |
|---|---|---|---|
borderRadius | --radius | Global border radius applied to cards, buttons, inputs, modals, and badges. Set to "0" for sharp corners (sportsbook-style) or "1rem" for rounded (casual-style). | 0.75rem |
HSL Color Format
All color values in the branding schema use the HSL format without the hsl() function wrapper. This is because the values are injected as CSS custom properties and used inside Tailwind's hsl() function:
/* In your branding config (stored in Supabase) */
primaryColor: "120 100% 43%"
/* Injected as CSS custom property */
--primary: 120 100% 43%;
/* Used by Tailwind and component styles */
.button {
background: hsl(var(--primary));
/* Resolves to: hsl(120 100% 43%) */
}
/* With opacity modifier */
.badge {
background: hsl(var(--primary) / 0.15);
/* Resolves to: hsl(120 100% 43% / 0.15) */
}HSL Anatomy
| Component | Range | Description | Example |
|---|---|---|---|
| H (Hue) | 0–360 | Color wheel angle. 0/360 = red, 120 = green, 240 = blue. | 120 (green) |
| S (Saturation) | 0%–100% | Color intensity. 0% = grey, 100% = full color. | 100% (fully saturated) |
| L (Lightness) | 0%–100% | Brightness. 0% = black, 50% = pure color, 100% = white. | 43% (medium) |
Common Casino Color Examples
| Brand | HSL Value | Hex Equivalent | Visual |
|---|---|---|---|
| Casino Green | 120 100% 43% | #00db00 | Bright green |
| Royal Purple | 270 60% 50% | #7733cc | Rich purple |
| Casino Gold | 43 96% 56% | #f5a623 | Warm gold |
| Ocean Blue | 210 80% 50% | #1a8ccc | Deep blue |
| Crimson Red | 0 72% 51% | #dc2626 | Bold red |
| Midnight Black | 0 0% 8% | #141414 | Near-black |
BrandingInjector Component
The BrandingInjector is a client component that watches the casino store for operator branding changes and injects CSS custom properties on the <html> element. It renders no visible UI (return null).
How It Works
predictu:init, the casino store fetches the operator's branding from the operators table and stores it in Zustand.useCasinoStore((s) => s.operatorBranding). When branding data appears, the useEffect fires.OperatorBranding field to one or more CSS variable names. Some fields map to multiple variables (e.g. primaryColor maps to --primary, --ring, and --green).document.documentElement.style.setProperty(key, value) for each one.bg-primary resolves to hsl(var(--primary))), the entire UI repaints with the new colors without any re-renders.useEffect cleanup function removes all injected properties, restoring the default theme.Component Source
"use client";
import { useEffect } from "react";
import { useCasinoStore } from "@/stores/casino-store";
import { brandingToCssVars } from "@/lib/branding";
export function BrandingInjector() {
const branding = useCasinoStore((s) => s.operatorBranding);
useEffect(() => {
if (!branding) return;
const vars = brandingToCssVars(branding);
const html = document.documentElement;
for (const [key, value] of Object.entries(vars)) {
if (value) {
html.style.setProperty(key, value);
}
}
return () => {
for (const key of Object.keys(vars)) {
html.style.removeProperty(key);
}
};
}, [branding]);
return null;
}CSS Variable Mapping
The brandingToCssVars() function maps branding fields to CSS variables. Some fields produce multiple variables, and some derived variables are computed automatically.
Direct Mappings
| Branding Field | CSS Variable(s) |
|---|---|
primaryColor | --primary, --ring, --green |
primaryForeground | --primary-foreground |
accentColor | --accent, --accent-foreground |
backgroundColor | --background |
foregroundColor | --foreground, --card-foreground, --popover-foreground |
cardColor | --card, --popover |
cardHoverColor | --card-hover |
borderColor | --border |
inputColor | --input, --secondary, --muted |
mutedForeground | --muted-foreground |
buttonYesColor | --up |
buttonNoColor | --down |
borderRadius | --radius |
Derived Variables
These variables are automatically computed from the base branding values:
| Derived Variable | Derived From | Computation |
|---|---|---|
--up-bg | buttonYesColor | {buttonYesColor} / 0.15 - 15% opacity version for subtle YES backgrounds. |
--down-bg | buttonNoColor | {buttonNoColor} / 0.15 - 15% opacity version for subtle NO backgrounds. |
--secondary | inputColor or backgroundColor | Uses inputColor if set, falls back to backgroundColor. |
--secondary-foreground | foregroundColor | Direct copy of foregroundColor. |
--accent-foreground | primaryForeground or backgroundColor | Uses primaryForeground if set, falls back to backgroundColor. |
--muted (override) | mutedColor | If mutedColor is explicitly set, overrides the --muted mapping from inputColor. |
Default Casino Theme
The default branding uses a dark green casino theme. If no custom branding is configured for an operator, these values are used:
const DEFAULT_BRANDING: OperatorBranding = {
// Core colors
primaryColor: "120 100% 43%", // Bright green
primaryForeground: "120 30% 5%", // Near-black on green
accentColor: "120 100% 43%", // Same as primary
// Surfaces (dark green tones)
backgroundColor: "120 18% 5%", // Very dark green-black
foregroundColor: "100 8% 82%", // Light grey-green text
cardColor: "120 14% 11%", // Dark green card
cardHoverColor: "120 14% 14%", // Slightly lighter on hover
borderColor: "120 10% 17%", // Subtle green border
inputColor: "120 12% 10%", // Input background
mutedColor: "120 10% 9%", // Muted surface
mutedForeground: "120 6% 44%", // Muted text
// Buttons
buttonYesColor: "120 100% 43%", // Green for YES/Buy
buttonNoColor: "0 72% 51%", // Red for NO/Sell
// Images
logoUrl: null,
bannerUrl: null,
faviconUrl: null,
// Layout
borderRadius: "0.75rem",
};Configuring via Operator Dashboard
Operators configure branding through the Operator Dashboard's branding editor. The process:
predictu-operator.vercel.app with your operator admin credentials.0.75rem. The preview updates cards, buttons, and inputs in real time.operators.branding JSONB column. All new iframe sessions will pick up the changes immediately. Existing sessions will update on the next page load.Programmatic Branding (API)
Branding can also be configured programmatically via the Operator API:
/api/operator/brandingReturns the current branding configuration for the authenticated operator.
/api/operator/brandingUpdate branding fields. Partial updates are supported - only include the fields you want to change.
// Example: Update primary color and border radius
const response = await fetch("/api/operator/branding", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk_live_...",
},
body: JSON.stringify({
primaryColor: "270 60% 50%", // Purple theme
accentColor: "270 60% 50%",
buttonYesColor: "142 71% 45%", // Green for YES
borderRadius: "0.5rem",
}),
});
const result = await response.json();
// { success: true, branding: { ...updated fields... } }Theming Best Practices
Contrast Requirements
foregroundColormust be readable againstbackgroundColor(4.5:1 ratio).primaryForegroundmust be readable againstprimaryColor(4.5:1 ratio).mutedForegroundmust be readable againstcardColor(3:1 ratio minimum).
Dark Theme Guidelines
Most casino sites use dark themes. Follow these guidelines for dark branding:
- Background lightness: Keep
backgroundColorbetween 3% and 10% lightness for a dark theme that doesn't feel washed out. - Card elevation:
cardColorshould be 3–6% lighter thanbackgroundColorto create visual hierarchy. - Border subtlety:
borderColorshould be 5–12% lighter thancardColor- visible but not harsh. - Primary brightness: Keep
primaryColorlightness above 40% so it pops against the dark background. - Button contrast:
buttonYesColorandbuttonNoColorshould be instantly distinguishable. Green and red with similar saturation/lightness work well.
Testing Your Branding
After configuring branding, test these scenarios:
| Scenario | What to Check |
|---|---|
| Market discovery page | Card colors, text readability, primary color on badges and buttons |
| Trade ticket modal | YES/NO button colors, input field backgrounds, border visibility |
| Portfolio page | Profit (green) and loss (red) amounts are clear, position cards readable |
| Mobile layout | Bottom nav bar colors, touch targets have sufficient contrast |
| Loading states | Skeleton placeholders match your card/background colors |
| Dropdown menus | Popover backgrounds (uses cardColor) are distinguishable from the page |
