/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:
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"]orimg[src*="brand"] - Favicon: metadata
faviconkey - Colors:
background-colorinline styles, hex values in style blocks
Always take Playwright screenshots at 375/768/1440:
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):
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
widthin 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):
background: radial-gradient(ellipse at 50% 38%,
{brand-mid-dark} 0%, {brand-dark} 55%, {near-black} 100%);
Gold border frame (standard casino finish):
.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:
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-shadowon key headlines
Font rendering notes:
font-family: 'Cinzel', Georgia, serif— primary display font for most casino brands$character in Cinzel renders poorly (looks like8) — usefont-family: Arial, sans-seriffor deposit strip text containing currency symbols- All caps via
text-transform: uppercasefor CTA letter-spacing: 0.8pxon CTAs
Stage 4 — Headless Chrome Render
"/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:
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:
- Read the 3 specific fixes from critique output
- Apply the highest-impact fix first (usually Depth: add hero image or glow)
- Re-render → re-critique
- Max 4 iterations before escalating to user
Stage 6 — i18n Fork (FR variant)
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 ( ) - Fractions:
1 $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:
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-imagefade at bottom 20% - Font: Libre Caslon Text + Caveat (handwritten) for preheading
- Hero value:
$1,000,000EN /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: é=é, ô=ô, 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 (no8artifact) - 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-harvestand/banner-critiqueare standalone skills — copy~/.claude/skills/{skill}/SKILL.mdto the target environment
Remote agent (VPS / CI):
- Chrome headless works headlessly — no display required
- Ensure
FC_API_KEYenv var is present - Google Fonts CDN requires outbound HTTP on port 443
- If offline: pre-download Cinzel + Libre Caslon WOFF2 files and use
@font-facewithurl()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}/