Predictu
Casino Integration

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.

Zero code changes required. Branding is purely data-driven. Operators configure everything through the dashboard UI. The iframe app picks up branding changes automatically on every new session - no redeployment needed.

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

FieldCSS Variable(s)ControlsDefault
primaryColor--primary, --ring, --greenPrimary brand color. Used for buttons, links, active states, focus rings, badges, and the "YES" accent throughout the UI.120 100% 43%
primaryForeground--primary-foregroundText color on primary-colored backgrounds. Must have sufficient contrast against primaryColor.120 30% 5%
accentColor--accent, --accent-foregroundSecondary accent color. Used for hover states, secondary badges, and accent borders. Often the same as primaryColor.120 100% 43%

Surface Colors

FieldCSS Variable(s)ControlsDefault
backgroundColor--backgroundPage background color. The base layer behind all content.120 18% 5%
foregroundColor--foreground, --card-foreground, --popover-foregroundPrimary text color. Used for headings, body text, and card content.100 8% 82%
cardColor--card, --popoverCard and popover background color. Used for market cards, trade tickets, modals, and dropdown menus.120 14% 11%
cardHoverColor--card-hoverCard background on hover. Provides visual feedback when users mouse over interactive cards.120 14% 14%
borderColor--borderBorder color for cards, dividers, table rows, and input fields.120 10% 17%
inputColor--input, --secondary, --mutedInput field background, secondary surface color, and muted background. Used for text inputs, search bars, toggle groups, and tab bars.120 12% 10%
mutedColor--mutedMuted background surface. Used for subtle section backgrounds and disabled states. If set, overrides the --muted mapping from inputColor.120 10% 9%
mutedForeground--muted-foregroundSecondary/muted text color. Used for labels, descriptions, timestamps, and placeholder text.120 6% 44%

Button Colors

FieldCSS Variable(s)ControlsDefault
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

FieldControlsFormatDefault
logoUrlOperator 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)
bannerUrlHero 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)
faviconUrlBrowser 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

FieldCSS VariableControlsDefault
borderRadius--radiusGlobal 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

ComponentRangeDescriptionExample
H (Hue)0–360Color 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

BrandHSL ValueHex EquivalentVisual
Casino Green120 100% 43%#00db00Bright green
Royal Purple270 60% 50%#7733ccRich purple
Casino Gold43 96% 56%#f5a623Warm gold
Ocean Blue210 80% 50%#1a8cccDeep blue
Crimson Red0 72% 51%#dc2626Bold red
Midnight Black0 0% 8%#141414Near-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

1
Branding loaded from Supabase. When the iframe receives predictu:init, the casino store fetches the operator's branding from the operators table and stores it in Zustand.
2
BrandingInjector reacts to store change. The component subscribes to useCasinoStore((s) => s.operatorBranding). When branding data appears, the useEffect fires.
3
brandingToCssVars() converts tokens. The utility function maps each OperatorBranding field to one or more CSS variable names. Some fields map to multiple variables (e.g. primaryColor maps to --primary, --ring, and --green).
4
CSS properties injected on <html>. The component iterates over the resulting key-value pairs and calls document.documentElement.style.setProperty(key, value) for each one.
5
UI repaints instantly. Since all Tailwind classes and component styles reference these CSS custom properties (e.g. bg-primary resolves to hsl(var(--primary))), the entire UI repaints with the new colors without any re-renders.
6
Cleanup on unmount. When the component unmounts (or branding changes), the 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 FieldCSS 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 VariableDerived FromComputation
--up-bgbuttonYesColor{buttonYesColor} / 0.15 - 15% opacity version for subtle YES backgrounds.
--down-bgbuttonNoColor{buttonNoColor} / 0.15 - 15% opacity version for subtle NO backgrounds.
--secondaryinputColor or backgroundColorUses inputColor if set, falls back to backgroundColor.
--secondary-foregroundforegroundColorDirect copy of foregroundColor.
--accent-foregroundprimaryForeground or backgroundColorUses primaryForeground if set, falls back to backgroundColor.
--muted (override)mutedColorIf 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:

1
Log into the Operator Dashboard at predictu-operator.vercel.app with your operator admin credentials.
2
Navigate to the Branding tab. The editor shows a split view: configuration form on the left, live preview on the right.
3
Edit color tokens. Each color field has a color picker and a text input for the HSL value. Changes are reflected in the live preview immediately.
4
Upload images. Drag and drop or click to upload your logo, banner, and favicon. Images are stored in Supabase Storage and served via CDN.
5
Adjust border radius. Use the slider or type a value like 0.75rem. The preview updates cards, buttons, and inputs in real time.
6
Save. The branding is persisted to the 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:

GET/api/operator/branding

Returns the current branding configuration for the authenticated operator.

PATCH/api/operator/branding

Update 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

Accessibility. Ensure sufficient contrast between text and background colors. The WCAG 2.1 AA standard requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text. Dark themes are particularly sensitive to this.
  • foregroundColor must be readable against backgroundColor (4.5:1 ratio).
  • primaryForeground must be readable against primaryColor (4.5:1 ratio).
  • mutedForeground must be readable against cardColor (3:1 ratio minimum).

Dark Theme Guidelines

Most casino sites use dark themes. Follow these guidelines for dark branding:

  • Background lightness: Keep backgroundColor between 3% and 10% lightness for a dark theme that doesn't feel washed out.
  • Card elevation: cardColor should be 3–6% lighter than backgroundColor to create visual hierarchy.
  • Border subtlety: borderColor should be 5–12% lighter than cardColor - visible but not harsh.
  • Primary brightness: Keep primaryColor lightness above 40% so it pops against the dark background.
  • Button contrast: buttonYesColor and buttonNoColor should be instantly distinguishable. Green and red with similar saturation/lightness work well.

Testing Your Branding

After configuring branding, test these scenarios:

ScenarioWhat to Check
Market discovery pageCard colors, text readability, primary color on badges and buttons
Trade ticket modalYES/NO button colors, input field backgrounds, border visibility
Portfolio pageProfit (green) and loss (red) amounts are clear, position cards readable
Mobile layoutBottom nav bar colors, touch targets have sufficient contrast
Loading statesSkeleton placeholders match your card/background colors
Dropdown menusPopover backgrounds (uses cardColor) are distinguishable from the page