# Casino Banner Production

> Produce LP-anchored casino affiliate display banners from a landing page URL and bonus copy. Runs brand harvest, HTML compositor authoring, headless Chrome render, visual critique, iteration, i18n variants, and final JPG delivery for Rewards Affiliates / Casino Rewards style campaigns. Use when asked to make casino banners, display creatives, IAB 250x250/500x500 casino assets, or brand-faithful casino affiliate creatives. ANTI-PATTERN: Do not use for unrelated visual design, generic research, or VOX SMS+ unless explicitly requested as an 800x418 no-CTA variant.

- Skill: `cryptopafi/casino-banner-production` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cryptopafi/casino-banner-production`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cryptopafi/casino-banner-production/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: cryptopafi (https://skillmd.com/u/cryptopafi)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/cryptopafi/casino-banner-production

---


# /casino-banner-production — LP-Anchored Casino Banner Production Pipeline

End-to-end pipeline: affiliate LP URL + bonus copy → brand harvest → HTML compositor → headless render → 5-axis critique → iterate to PASS → ship EN+FR deliverables. Portable to any Claude Code harness, IDE, or remote agent.

## §1. Identity

**Purpose:** Produce IAB (Interactive Advertising Bureau) display banners for casino affiliate networks (e.g. Casino Rewards / Rewards Affiliates) anchored to the brand's own landing page (LP) visual identity. Replaces manual design work with a reproducible, critique-gated pipeline.

**Triggers:** `banner <brand>`, `/casino-banner-production`, `make banners for <brand>`, `build display creatives for <affiliate>`.

**DO NOT use for:** VOX SMS+ creatives unless explicitly requested as an `800x418` no-CTA variant, Google Display Network animated formats, or brands without a publicly accessible LP.

**Session origin:** Derived from SMSads/Casino Rewards pilot (Elevate Marketing LLC), May 2026. Three brands: Luxury Casino (LXC), Zodiac Casino (ZC), Casino Kingdom (CK). All patterns validated through iteration to PASS PRO critique scores.

---

## §2. Inputs

| Input | Type | Required | Description |
|---|---|---|---|
| `brand_name` | string | YES | Human name, e.g. "Casino Kingdom" |
| `brand_slug` | string | YES | Short code, e.g. `ck`, `lxc`, `zc` |
| `lp_url` | string | YES | Brand LP URL, e.g. `https://www.casinokingdom.eu/en/` |
| `bonus_en` | string | YES | Offer copy in English, verbatim from bonus file |
| `bonus_fr` | string | NO | French offer copy. Skip if EN-only campaign |
| `formats` | array | NO | Default: `["250x250", "500x500"]`. Also supports explicit `800x418` no-CTA variants |
| `output_dir` | path | NO | Default: `~/Desktop/Elevate Marketing /Visuals/deliverables/{brand_slug}/` |
| `affiliate_id` | string | NO | Affiliate ID for redirect URL tagging (e.g. `aff111147`) |
| `fc_api_key` | env | COND | `$FC_API_KEY` — required for Firecrawl brand harvest |

**Bonus file location:** `~/.nexus/projects/smsads/casino-rewards/recon/bonuses-verbatim.md`

---

## §3. Outputs

```
{output_dir}/
  {brand_slug}-en-500x500-vNN.jpg     ← Final EN large
  {brand_slug}-en-250x250-vNN.jpg     ← Final EN IAB square
  {brand_slug}-fr-500x500-vNN.jpg     ← Final FR large (if FR requested)
  {brand_slug}-fr-250x250-vNN.jpg     ← Final FR IAB square

{output_dir}/_build/
  compositor-250x250-vNN.html         ← EN compositor source (reusable)
  compositor-250x250-vNN-fr.html      ← FR compositor source
  logo.png                            ← Staged brand logo (RGBA)
  vault-hero-vN.png                   ← Cropped LP hero element
  raw-500x500-vNN.png                 ← 2× DPR Chrome render
  iteration-NN-critique.md            ← Brief rationale, critique summary, fixes applied

~/.nexus/projects/brand-harvest/{domain}/
  brand-kit.json                      ← W3C Design Tokens
  logos/ck_logo.png
  screenshots/desktop-{375,768,1440}.png
```

---

## §4. Dependencies

| Dep | Install | Required |
|---|---|---|
| Python 3 + Pillow | `pip install Pillow` | YES |
| Google Chrome (headless) | `/Applications/Google Chrome.app` (macOS) | YES |
| `firecrawl-py` | `pip install firecrawl-py` | YES (Stage 1) |
| `FC_API_KEY` env | `~/.zshrc` export | YES (Firecrawl) |
| `/brand-harvest` skill | `~/.claude/skills/brand-harvest/` | YES (Stage 1) |
| `/banner-critique` skill | `~/.claude/skills/banner-critique/` | YES (Stage 5) |
| Google Fonts CDN | Internet access at render time | YES (fonts) |
| Telegram bot token | `~/.nexus/workspace/active/vps2-secrets.env` | NO (delivery) |

**Chrome headless path (macOS):**
```
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
```
Never use bare `google-chrome` — not on macOS PATH.

---

## §5. Algorithm

### Stage 1 — Brand Harvest (run `/brand-harvest {lp_url}`)

Extract design system from the LP:

```python
from firecrawl import FirecrawlApp
import os, json

app = FirecrawlApp(api_key=os.environ['FC_API_KEY'])
result = app.scrape(url, formats=['markdown', 'rawHtml'])
# branding field is often null — fall through to HTML parse
```

**Fallback if Firecrawl branding null:** Parse `rawHtml` for:
- Logo: `img[src*="logo"]` or `img[src*="brand"]`
- Favicon: metadata `favicon` key
- Colors: `background-color` inline styles, hex values in style blocks

**Always take Playwright screenshots at 375/768/1440:**
```bash
npx playwright screenshot --full-page --viewport-size "1440,900" {url} desktop-1440.png
```

**Output:** `brand-kit.json` with `colors`, `typography`, `logos`, `brand_voice` keys.

**Logo check (HARD — from BANNER-LOGO-CROSSREF rule):**
- Download all logo variants (square, wide, transparent, dark-bg, light-bg)
- Check alpha channel: `Image.open(logo).mode == 'RGBA'` and corner pixels alpha=0
- Match logo aspect to banner size before placing
- Light-bg logos need bg-removal for dark composites

---

### Stage 2 — LP Visual Signature Extraction

From the 1440px LP screenshot, crop the **hero element** (vault, jackpot wheel, medallion, or jackpot amount display):

```python
from PIL import Image
img = Image.open('screenshots/desktop-1440.png')
w, h = img.size
# Crop: adjust x/y bounds by visual inspection of screenshot
hero = img.crop((x1, y1, x2, y2))
hero.save('_build/hero-element.png')
```

**Rules:**
- Exclude people/arms from edge of crop — crop tight to the circular/visual element
- Exclude baked UI text (nav bars, cookie banners) from the top of crop
- The hero element IS the brand's visual signature — it must appear in the banner
- At small size (130–160px in compositor), baked text inside hero becomes atmospheric/decorative rather than competing copy — this is acceptable and desirable

---

### Stage 3 — HTML Compositor Authoring

Write `_build/compositor-250x250.html` using this structure (all measurements in CSS px at 250×250 canvas):

```
Canvas: 250×250px CSS, rendered at 2× DPR = 500×500px actual
Font stack: Google Fonts (Cinzel/Libre Caslon for casino; load via @import)
Layout zones (top → bottom):
  1. Logo zone:       top 4–8px, width 60–72px, centered
  2. Hero zone:       top 55–65px, width 130–165px, centered
  3. Deposit strip:   ~186px, height 18–22px, pill with border
  4. CTA button:      bottom 8–14px, height 34–40px
```

**Logo placement rules:**
- Logo `width` in CSS = target render width ÷ 2 (because 2× DPR)
- Always use `position: absolute; left: 50%; transform: translateX(-50%)` for centering
- Add `filter: drop-shadow(0 1px 8px rgba(228,180,50,0.55))` for casino gold glow

**Background pattern (dark casino):**
```css
background: radial-gradient(ellipse at 50% 38%,
  {brand-mid-dark} 0%, {brand-dark} 55%, {near-black} 100%);
```

**Gold border frame (standard casino finish):**
```css
.banner::before { border: 2px solid #9a7420; z-index: 10; }
.banner::after  { border: 1px solid rgba(228,180,50,0.18); inset: 2px; z-index: 10; }
```

**CTA button pattern:**
```css
background: linear-gradient(180deg, {gold-light} 0%, {gold-mid} 55%, {gold-dark} 100%);
border: 1px solid {gold-light};
box-shadow: 0 2px 16px rgba(228,180,50,0.50), 0 1px 4px rgba(0,0,0,0.9);
```

**Depth cues (required for Depth axis ≥ 4):**
- Atmospheric radial glow behind hero element
- `filter: drop-shadow(0 4px 18px rgba(0,0,0,0.9))` on hero image
- 3–8 sparkle dot particles (2–3px, `border-radius:50%`, gold, varying opacity)
- `text-shadow` on key headlines

**Font rendering notes:**
- `font-family: 'Cinzel', Georgia, serif` — primary display font for most casino brands
- `$` character in Cinzel renders poorly (looks like `8`) — use `font-family: Arial, sans-serif` for deposit strip text containing currency symbols
- All caps via `text-transform: uppercase` for CTA
- `letter-spacing: 0.8px` on CTAs

---

### Stage 4 — Headless Chrome Render

```bash
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless=new \
  --disable-gpu \
  --no-sandbox \
  --screenshot="{output}/raw-500x500-vNN.png" \
  --window-size=250,250 \
  --force-device-scale-factor=2 \
  "file://{output}/compositor-250x250.html"
```

Then Lanczos downscale:

```python
from PIL import Image
img = Image.open('raw-500x500-vNN.png')
img.convert('RGB').save('brand-en-500x500-vNN.jpg', 'JPEG', quality=92, optimize=True)
img.resize((250, 250), Image.LANCZOS).convert('RGB').save('brand-en-250x250-vNN.jpg', 'JPEG', quality=92, optimize=True)
```

**Why 2× DPR then downscale:** Eliminates sub-pixel aliasing on text. Never render directly at 250×250 — the result is blurry.

---

### Stage 5 — Banner Critique (run `/banner-critique`)

Invoke: `/banner-critique {path} --brand {brand_code} --format 250x250`

**5-axis scoring (each 1–5, PASS requires ALL ≥ 4):**

| Axis | What to check |
|---|---|
| Readability | Text contrast ≥ 7:1, no clipping, min 18pt at final render |
| Hierarchy | One dominant element; eye lands on it in 0.3s; flow obvious |
| Depth | Layered z-order, shadow/glow on hero, foreground/bg distinction |
| Brand fit | Palette ±15 hex of LP, font close-match, CTA verb matches LP |
| Polish | Looks agency-produced; no amateur CSS tells |

**Never pass generously to be polite.** If Depth=2 (flat CSS look), score it 2.

**If FAIL → iterate:**
1. Read the 3 specific fixes from critique output
2. Apply the highest-impact fix first (usually Depth: add hero image or glow)
3. Re-render → re-critique
4. Max 4 iterations before escalating to user

---

### Stage 6 — i18n Fork (FR variant)

```python
with open('compositor-250x250-vNN.html') as f:
    html = f.read()

# Replace EN strings with FR equivalents
html = html.replace(
    '<span class="deposit-text" ...>$1 Deposit → 40 Free Spins</span>',
    '<span class="deposit-text" style="font-family:Arial,sans-serif;">Dépôt 1$ → 40 Tours Gratuits</span>'
).replace(
    'CLAIM FREE SPIN',
    "PROFITER DE L'OFFRE"
)
```

**FR CTA rules (from session constraints):**
- ✅ `PROFITER DE L'OFFRE` — approved
- ❌ `JE PROFITE DE...` — banned by user
- Currency: `1 000 000 $` with non-breaking spaces (`&nbsp;`)
- Fractions: `1&nbsp;$` not `$1`

**Currency/symbol rendering:**
- Never use Cinzel for deposit strips with `$`, `€`, or `£` — use Arial/sans-serif inline
- Test FR render explicitly — accented chars (é, ô, à) may break in some fonts

---

### Stage 7 — Ship Deliverables

```
{output_dir}/
  en-default/
    {slug}-en-500x500-vNN.jpg
    {slug}-en-250x250-vNN.jpg
  fr-default/
    {slug}-fr-500x500-vNN.jpg
    {slug}-fr-250x250-vNN.jpg
```

**Optional Telegram delivery:**
```bash
set +x
BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' ~/.nexus/workspace/active/vps2-secrets.env | cut -d= -f2-)
CHAT_ID=623593648

curl -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendDocument" \
  -F chat_id="${CHAT_ID}" \
  -F document=@"/tmp/finals.zip"
```

**ZIP best practice:** Create finals-only zip (exclude `_build/`) — one zip per batch delivery.

---

## §6. Brand-Specific Patterns

### Casino Kingdom (CK)
- Background: `radial-gradient(ellipse at 50% 38%, #1c1408, #0d0904, #050403)`
- Gold: `#E8B84B` / CTA gradient: `#F5C842 → #D4960A → #A87108`
- Font: Cinzel 900 weight for headings
- Hero: LP vault/medallion circle crop — this IS the brand signature
- Crop tip: trim right ~15% of full vault crop to remove person's arm at door handle
- Logo: 180×180 RGBA transparent ✅ — use at 62–72px width in compositor

### Zodiac Casino (ZC)
- Three approved variants: C (purple-dark), C2 (red/magenta), C3 (true-black)
- C3 bg: `radial-gradient(ellipse at 50% 45%, #1a0d0d, #0e0e0e, #050505)` — user-preferred
- Jackpot asset: use LP jackpot crop with CSS `mask-image` fade at bottom 20%
- Font: Libre Caslon Text + Caveat (handwritten) for preheading
- Hero value: `$1,000,000` EN / `1 000 000 $` FR

### Luxury Casino (LXC)
- Background: dark navy/midnight blue
- Logo: 204×64 transparent PNG — wide aspect, use width 130–140px
- Font: Native LP font stack preferred

---

## §7. Layout Constraint Math

For 250px canvas (remember: CSS values = half of actual render pixels):

```
Logo height = logo_width × (logo_h_px / logo_w_px)
Logo bottom = logo_top + logo_height
Preheading must be ≥ 8px BELOW logo bottom (clearance)
Hero top = preheading_bottom + 4px
Hero height = hero_width × (hero_h_px / hero_w_px)
Hero bottom = hero_top + hero_height
Deposit strip top = hero_bottom + 4px  (must NOT be inside hero)
CTA top = 250 - cta_bottom_margin - cta_height
Verify: deposit_bottom < cta_top (no overlap)
```

**Common failure:** Vault/hero image taller than expected → deposit strip covered.
**Fix:** Reduce hero `width` by 10px increments until layout fits.

---

## §8. Error Handling

| Error | Action |
|---|---|
| Firecrawl branding null | Parse rawHtml for logo URLs; continue |
| Logo not transparent | Use on dark bg only; flag for bg-removal |
| Chrome render black/empty | Quote the `file://` URL fully; if Chrome still fails on spaces, render from a temporary no-space path |
| Exit code 137 (SIGKILL) | Render one file at a time; avoid parallel Chrome instances |
| `$` in Cinzel = `8` glyph | Switch deposit strip font to Arial inline |
| FR accents garbled | Use HTML entities: `&#233;`=é, `&#244;`=ô, or write FR HTML directly, not via sed |
| Deposit line hidden under hero | Reduce hero width; recalculate layout math |
| Google Fonts not loading offline | Base64 encode fonts or use system serif fallback |

---

## §9. Tests

- **Smoke:** `/casino-banner-production CK https://www.casinokingdom.eu/en/ "1 Free Spin Mega Money Wheel"` → produces 4 JPGs, critique PASS ≥ 4.0/5.0 mean
- **Regression:** Re-run on LXC with known v08 assets → output matches reference within ±5KB file size
- **i18n:** FR deposit strip with `$` renders correctly (no `8` artifact)
- **Layout math:** Logo + hero + deposit + CTA all within 250px, no overlap

---

## §10. Portability Notes (for other harnesses / IDEs)

**Cursor / Aider / plain Codex:**
- This skill is pure Python + HTML + shell — no NexusOS-specific tools required
- Replace `~/.nexus/projects/...` paths with your project root
- Replace Telegram delivery with any notification mechanism
- `/brand-harvest` and `/banner-critique` are standalone skills — copy `~/.claude/skills/{skill}/SKILL.md` to the target environment

**Remote agent (VPS / CI):**
- Chrome headless works headlessly — no display required
- Ensure `FC_API_KEY` env var is present
- Google Fonts CDN requires outbound HTTP on port 443
- If offline: pre-download Cinzel + Libre Caslon WOFF2 files and use `@font-face` with `url()` path

**Model routing:**
- Brand harvest + compositor authoring: Sonnet (standard)
- Critique scoring + design judgment: Sonnet or Opus
- FR copy translation: Sonnet
- Architecture decisions: Opus

---

## §11. Full Pipeline Summary (cheat sheet)

```
1. /brand-harvest {lp_url}          → brand-kit.json + logo + screenshots
2. LP screenshot → crop hero element → vault-hero.png
3. Write compositor HTML             → _build/compositor-250x250.html
4. Chrome headless render 2× DPR    → raw-500x500.png
5. Pillow Lanczos downscale          → brand-en-{500,250}.jpg
6. /banner-critique                  → score ≥ 4.0 all axes?
   └─ FAIL → fix top issue → goto 3
   └─ PASS → continue
7. i18n fork FR                      → compositor-fr.html → render → brand-fr-{500,250}.jpg
8. Ship to deliverables/{lang}/
9. Zip finals (exclude _build/)
10. Telegram sendDocument (optional)
```

**Expected wall time:** ~15–25 min per brand (3 brands = ~1h with iteration)
**Expected cost:** 2–5 Firecrawl credits/brand (free tier covers ~200 brands/mo)

---

## §12. Session Reference

**Origin:** SMSads/Casino Rewards pilot, May 2026 (Elevate Marketing LLC × VOX Solutions).
**Brands validated:** LXC v08, ZC v06 C/C2/C3, CK v03.
**Deliverables location:** `~/Desktop/Elevate Marketing /Visuals/deliverables/`
**Bonuses file:** `~/.nexus/projects/smsads/casino-rewards/recon/bonuses-verbatim.md`
**Brand-harvest output:** `~/.nexus/projects/brand-harvest/{domain}/`

