# CYDM Per-Tenant Branding & Theming System

## Design Philosophy

**Constraint**: Blue (primary) + Orange (accent) only — no other colors.
**Rationale**: 
- Blue = trust, stability, finance (universal for banking)
- Orange = energy, accessibility, warmth (community focus)
- Consistent brand recognition across all CYDM tenants
- Simplifies design system, reduces decision fatigue
- Meets accessibility (WCAG AA) with proper contrast ratios

---

## Theme Architecture

### 1. Global Design Tokens (Fixed)
```css
/* These NEVER change per tenant - core SHAD UI semantics */
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: var(--primary);           /* Tracks primary */
  --radius: 0.625rem;               /* Base radius - tenant configurable */
}
```

### 2. Tenant-Customizable Tokens (Runtime)
```typescript
// Tenant branding JSON schema (stored in tenants.branding)
interface TenantBranding {
  // Primary: Must be blue family (hue 220-260)
  primary_color: string;      // OKLCH format: "0.45 0.22 258"
  primary_foreground: string; // Auto-computed for contrast
  
  // Accent: Must be orange family (hue 30-50)  
  accent_color: string;       // OKLCH format: "0.70 0.18 45"
  accent_foreground: string;  // Auto-computed
  
  // Radius: 0.25rem - 1.0rem (4px - 16px)
  border_radius: number;      // 0.5 = 8px default
  
  // Visual Assets
  logo_light: string;         // URL or base64 (SVG preferred)
  logo_dark: string;          // URL or base64
  favicon: string;            // URL
  
  // Strings
  institution_name: string;   // "Akiba Yetu SACCOS"
  short_name: string;         // "AYS"
  tagline: string;            // "Your Financial Partner"
  support_phone: string;      // "+255 7XX XXX XXX"
  support_email: string;      // "support@ays.co.tz"
  
  // Locale
  default_language: 'sw' | 'en';
  date_format: string;        // "DD/MM/YYYY"
  currency_symbol: string;    // "TZS"
}
```

### 3. Color Validation (Enforced at API Level)
```php
// app/Services/Tenancy/TenantBrandingService.php
class TenantBrandingService {
    public function validateAndNormalize(array $input): array {
        $primary = $this->parseOklch($input['primary_color']);
        $accent = $this->parseOklch($input['accent_color']);
        
        // Enforce blue primary (hue 220-260)
        if ($primary->h < 220 || $primary->h > 260) {
            throw new \InvalidArgumentException('Primary color must be in blue family (hue 220-260)');
        }
        
        // Enforce orange accent (hue 30-50)
        if ($accent->h < 30 || $accent->h > 50) {
            throw new \InvalidArgumentException('Accent color must be in orange family (hue 30-50)');
        }
        
        // Ensure accessibility (WCAG AA: 4.5:1 for text, 3:1 for UI)
        $primaryFg = $this->computeForeground($primary);
        $accentFg = $this->computeForeground($accent);
        
        return [
            'primary_color' => $primary->toOklchString(),
            'primary_foreground' => $primaryFg,
            'accent_color' => $accent->toOklchString(),
            'accent_foreground' => $accentFg,
            'border_radius' => clamp($input['border_radius'] ?? 0.625, 0.25, 1.0),
            // ... other fields
        ];
    }
}
```

### 4. Approved Color Palettes (Pre-defined Options)
```typescript
// Tenants choose from curated palettes - no free-form color picker
const BLUE_PALETTES = [
  { name: 'Deep Trust', primary: '0.38 0.20 258', description: 'Conservative, established' },
  { name: 'Professional Blue', primary: '0.45 0.22 258', description: 'Balanced, modern' },  // DEFAULT
  { name: 'Bright Confidence', primary: '0.52 0.24 255', description: 'Youthful, digital-first' },
  { name: 'Navy Authority', primary: '0.32 0.18 260', description: 'Traditional, serious' },
];

const ORANGE_PALETTES = [
  { name: 'Warm Community', accent: '0.68 0.16 42', description: 'Approachable, friendly' },
  { name: 'Energetic Orange', accent: '0.70 0.18 45', description: 'Dynamic, action-oriented' }, // DEFAULT
  { name: 'Amber Warmth', accent: '0.72 0.15 38', description: 'Trustworthy, established' },
  { name: 'Sunrise Energy', accent: '0.75 0.20 48', description: 'Optimistic, growing' },
];

const RADIUS_OPTIONS = [
  { value: 0.25, label: 'Sharp (4px)', description: 'Modern, technical' },
  { value: 0.375, label: 'Subtle (6px)', description: 'Clean, professional' },
  { value: 0.5, label: 'Balanced (8px)', description: 'Friendly, accessible' }, // DEFAULT
  { value: 0.625, label: 'Rounded (10px)', description: 'Soft, approachable' },
  { value: 0.75, label: 'Pill (12px)', description: 'Playful, mobile-first' },
];
```

---

## Runtime Theme Application

### CSS Variable Injection (Client-Side)
```typescript
// resources/js/hooks/useTenantTheme.ts
export function useTenantTheme() {
  const { tenant } = usePage().props;
  const branding = tenant?.branding;
  
  useEffect(() => {
    if (!branding) return;
    
    const root = document.documentElement;
    const updates: Record<string, string> = {};
    
    // Primary (Blue) - maps to SHAD UI --primary, --ring
    if (branding.primary_color) {
      updates['--primary'] = branding.primary_color;
      updates['--ring'] = branding.primary_color;
      updates['--primary-foreground'] = branding.primary_foreground;
      updates['--sidebar-primary'] = branding.primary_color;
      updates['--sidebar-primary-foreground'] = branding.primary_foreground;
    }
    
    // Accent (Orange) - maps to SHAD UI --accent
    if (branding.accent_color) {
      updates['--accent'] = branding.accent_color;
      updates['--accent-foreground'] = branding.accent_foreground;
      updates['--sidebar-accent'] = branding.accent_color;
      updates['--sidebar-accent-foreground'] = branding.accent_foreground;
    }
    
    // Border Radius
    if (branding.border_radius) {
      updates['--radius'] = `${branding.border_radius}rem`;
    }
    
    // Apply all at once (single reflow)
    Object.entries(updates).forEach(([prop, value]) => {
      root.style.setProperty(prop, value);
    });
    
    // Update meta theme-color for mobile browsers
    const metaThemeColor = document.querySelector('meta[name="theme-color"]');
    if (metaThemeColor && branding.primary_color) {
      metaThemeColor.setAttribute('content', oklchToHex(branding.primary_color));
    }
  }, [branding]);
}
```

### Server-Side Rendering (SSR) Support
```php
// app/Http/Middleware/InjectTenantTheme.php
class InjectTenantTheme {
    public function handle(Request $request, Closure $next) {
        $tenant = $request->attributes->get('tenant');
        
        if ($tenant && $tenant->branding) {
            // Share with Inertia for initial render
            Inertia::share('tenantBranding', $tenant->branding);
            
            // Add inline styles for zero-flash SSR
            $styles = $this->generateInlineStyles($tenant->branding);
            Inertia::share('tenantThemeStyles', $styles);
        }
        
        return $next($request);
    }
    
    private function generateInlineStyles(array $branding): string {
        $css = ':root {';
        if (!empty($branding['primary_color'])) {
            $css .= "--primary: {$branding['primary_color']};";
            $css .= "--ring: {$branding['primary_color']};";
            $css .= "--primary-foreground: {$branding['primary_foreground']};";
        }
        if (!empty($branding['accent_color'])) {
            $css .= "--accent: {$branding['accent_color']};";
            $css .= "--accent-foreground: {$branding['accent_foreground']};";
        }
        if (!empty($branding['border_radius'])) {
            $css .= "--radius: {$branding['border_radius']}rem;";
        }
        $css .= '}';
        return $css;
    }
}
```

---

## Logo & Asset Management

### Upload & Processing
```php
// app/Services/Tenancy/TenantBrandingService.php (continued)
public function uploadLogo(Tenant $tenant, UploadedFile $file, string $variant): string {
    // Validate: SVG, PNG, WebP only; max 500KB; max 400x400px
    $this->validateLogo($file);
    
    // Process: Optimize, generate variants
    $path = "tenants/{$tenant->id}/branding/{$variant}-{$file->hashName()}";
    
    if ($file->getMimeType() === 'image/svg+xml') {
        $optimized = $this->optimizeSvg($file);
    } else {
        $optimized = Image::read($file)
            ->cover(400, 400)
            ->toWebp(85);
    }
    
    Storage::disk('s3')->put($path, $optimized);
    
    // Update tenant branding JSON
    $branding = $tenant->branding ?? [];
    $branding["logo_{$variant}"] = Storage::disk('s3')->url($path);
    $tenant->update(['branding' => $branding]);
    
    return $branding["logo_{$variant}"];
}
```

### Logo Usage in Components
```tsx
// resources/js/Components/layout/BrandLogo.tsx
export function BrandLogo({ variant = 'light', className }: { 
  variant?: 'light' | 'dark'; 
  className?: string;
}) {
  const { tenant } = usePage().props;
  const logoUrl = tenant?.branding?.[`logo_${variant}`];
  const fallbackName = tenant?.branding?.short_name ?? 'CYDM';
  
  return (
    <div className={cn('flex items-center gap-2', className)}>
      {logoUrl ? (
        <img 
          src={logoUrl} 
          alt={`${tenant.branding.institution_name} logo`} 
          className="h-8 w-auto" 
        />
      ) : (
        <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground font-bold text-lg">
          {fallbackName.charAt(0)}
        </div>
      )}
      <span className="font-semibold text-foreground">
        {tenant?.branding?.institution_name}
      </span>
    </div>
  );
}
```

---

## White-Label Configuration (Beyond Colors)

### Customizable Strings
```typescript
interface TenantStrings {
  // Navigation
  nav_dashboard: string;      // "Dashboard" / "Dashibodi"
  nav_members: string;        // "Members" / "Wanachama"
  nav_loans: string;          // "Loans" / "Mikopo"
  nav_savings: string;        // "Savings" / "Akiba"
  nav_shares: string;         // "Shares" / "Hisa"
  nav_accounting: string;     // "Accounting" / "Uhasibu"
  nav_reports: string;        // "Reports" / "Ripoti"
  nav_settings: string;       // "Settings" / "Mipangilio"
  
  // Actions
  btn_apply_loan: string;     // "Apply for Loan" / "Omba Mkopo"
  btn_deposit: string;        // "Deposit" / "Wekeza"
  btn_withdraw: string;       // "Withdraw" / "Ongeza"
  btn_repay: string;          // "Repay" / "Lipa"
  
  // Status
  status_pending: string;     // "Pending" / "Inasubiri"
  status_approved: string;    // "Approved" / "Imeidhinishwa"
  status_active: string;      // "Active" / "Hai"
  status_overdue: string;     // "Overdue" / "Uliochelewa"
  
  // Messages
  msg_welcome: string;        // "Welcome to {institution}"
  msg_loan_approved: string;  // "Your loan of {amount} has been approved"
  msg_payment_received: string; // "Payment of {amount} received. Thank you!"
}
```

### Document Templates (Per Tenant)
```php
// Stored in tenant.settings['document_templates']
$defaultTemplates = [
    'loan_agreement' => [
        'header_html' => '<div class="letterhead">...</div>',
        'footer_html' => '<div class="footer">...</div>',
        'clauses' => [...], // Customizable terms
    ],
    'receipt' => [...],
    'statement' => [...],
    'welcome_letter' => [...],
    'overdue_notice' => [...],
];
```

---

## Onboarding Branding Wizard (Step 3 of 5)

```tsx
// resources/js/Pages/Onboarding/BrandingStep.tsx
export function BrandingStep({ tenant, onUpdate }: BrandingStepProps) {
  const [step, setStep] = useState<'colors' | 'logo' | 'strings' | 'preview'>('colors');
  
  return (
    <div className="space-y-6">
      {/* Progress */}
      <div className="flex items-center gap-4">
        {['colors', 'logo', 'strings', 'preview'].map((s, i) => (
          <div key={s} className="flex items-center gap-2">
            <div className={cn(
              'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium',
              i < currentStepIndex ? 'bg-primary text-primary-foreground' :
              i === currentStepIndex ? 'bg-accent text-accent-foreground' :
              'bg-muted text-muted-foreground'
            )}>
              {i + 1}
            </div>
            <span className="hidden sm:block capitalize">{s}</span>
          </div>
        ))}
      </div>
      
      {/* Color Selection */}
      {step === 'colors' && (
        <ColorPaletteSelector
          currentPrimary={tenant.branding?.primary_color}
          currentAccent={tenant.branding?.accent_color}
          currentRadius={tenant.branding?.border_radius}
          onChange={(updates) => onUpdate({ branding: { ...tenant.branding, ...updates } })}
        />
      )}
      
      {/* Logo Upload */}
      {step === 'logo' && (
        <LogoUploader
          currentLight={tenant.branding?.logo_light}
          currentDark={tenant.branding?.logo_dark}
          onUpload={(variant, url) => onUpdate({ 
            branding: { ...tenant.branding, [`logo_${variant}`]: url } 
          })}
        />
      )}
      
      {/* String Customization */}
      {step === 'strings' && (
        <StringEditor
          strings={tenant.branding?.strings ?? defaultStrings}
          onChange={(strings) => onUpdate({ branding: { ...tenant.branding, strings } })}
        />
      )}
      
      {/* Live Preview */}
      {step === 'preview' && (
        <ThemePreview tenant={tenant} />
      )}
    </div>
  );
}
```

---

## Accessibility Compliance

| Requirement | Implementation |
|-------------|----------------|
| **Contrast Ratio** | Auto-compute foreground for 4.5:1 (text) / 3:1 (UI) |
| **Focus Indicators** | `--ring` tracks primary, visible focus-visible styles |
| **Color Blindness** | Blue/orange are deuteranopia/protanopia safe; no red/green status solely by color |
| **Reduced Motion** | `@media (prefers-reduced-motion)` disables transitions |
| **Dark Mode** | All tokens defined for `.dark`, auto-switch or manual toggle |
| **Font Scaling** | `rem` units throughout, respects browser zoom |

---

## Testing Checklist

- [ ] All 4 blue palettes render correctly in light/dark mode
- [ ] All 4 orange accents meet contrast on primary backgrounds
- [ ] Radius options (4px-12px) don't break layout
- [ ] Logo upload: SVG, PNG, WebP; rejected: JPG, GIF, >500KB
- [ ] Theme applies instantly on tenant switch (no refresh)
- [ ] SSR renders correct theme on first paint (no flash)
- [ ] Custom strings appear in all UI components
- [ ] PDF generation uses tenant templates/colors
- [ ] Email templates inherit branding
- [ ] Mobile browser theme-color meta tag updates