1596 lines
45 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibe Ads — AI Ad Creative Engine</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
/* ═══════════════════════════════════════════════════════
CSS RESET & VARIABLES
═══════════════════════════════════════════════════════ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a2e;
--glass-bg: rgba(255, 255, 255, 0.04);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-hover: rgba(255, 255, 255, 0.08);
--blue: #00d4ff;
--blue-glow: rgba(0, 212, 255, 0.3);
--coral: #ff6b6b;
--coral-glow: rgba(255, 107, 107, 0.3);
--purple: #a855f7;
--green: #34d399;
--yellow: #fbbf24;
--text-primary: #f0f0f0;
--text-secondary: rgba(255, 255, 255, 0.6);
--text-muted: rgba(255, 255, 255, 0.35);
--radius: 16px;
--radius-sm: 10px;
--radius-xs: 6px;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, #0d1117 100%);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* ═══════════════════════════════════════════════════════
AMBIENT BACKGROUND
═══════════════════════════════════════════════════════ */
.ambient-bg {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.ambient-orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.15;
animation: orbFloat 20s ease-in-out infinite;
}
.ambient-orb:nth-child(1) {
width: 600px; height: 600px;
background: var(--blue);
top: -200px; left: -100px;
animation-delay: 0s;
}
.ambient-orb:nth-child(2) {
width: 500px; height: 500px;
background: var(--coral);
bottom: -200px; right: -100px;
animation-delay: -7s;
}
.ambient-orb:nth-child(3) {
width: 400px; height: 400px;
background: var(--purple);
top: 50%; left: 50%;
transform: translate(-50%, -50%);
animation-delay: -14s;
opacity: 0.08;
}
@keyframes orbFloat {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(30px, -40px) scale(1.05); }
50% { transform: translate(-20px, 30px) scale(0.95); }
75% { transform: translate(40px, 20px) scale(1.02); }
}
/* ═══════════════════════════════════════════════════════
LAYOUT
═══════════════════════════════════════════════════════ */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
position: relative;
z-index: 1;
}
/* ═══════════════════════════════════════════════════════
HEADER / HERO
═══════════════════════════════════════════════════════ */
.hero {
padding: 80px 0 40px;
text-align: center;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 100px;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 24px;
backdrop-filter: blur(20px);
}
.hero-badge .dot {
width: 6px; height: 6px;
background: var(--green);
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 6px rgba(52, 211, 153, 0); }
}
.hero h1 {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 900;
line-height: 1.05;
letter-spacing: -0.03em;
margin-bottom: 16px;
}
.hero h1 .gradient-text {
background: linear-gradient(135deg, var(--blue) 0%, var(--purple) 50%, var(--coral) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero p {
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto 48px;
line-height: 1.6;
}
/* ═══════════════════════════════════════════════════════
INPUT SECTION
═══════════════════════════════════════════════════════ */
.input-section {
max-width: 640px;
margin: 0 auto 60px;
}
.input-wrapper {
position: relative;
display: flex;
gap: 12px;
padding: 8px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
backdrop-filter: blur(20px);
transition: border-color 0.3s, box-shadow 0.3s;
}
.input-wrapper:focus-within {
border-color: rgba(0, 212, 255, 0.3);
box-shadow: 0 0 0 4px rgba(0, 212, 255, 0.08), 0 8px 32px rgba(0, 0, 0, 0.3);
}
.input-wrapper input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--text-primary);
font-size: 16px;
font-family: inherit;
padding: 12px 16px;
}
.input-wrapper input::placeholder {
color: var(--text-muted);
}
.btn-generate {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
background: linear-gradient(135deg, var(--blue), #0099cc);
color: #000;
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
.btn-generate:hover {
transform: translateY(-1px);
box-shadow: 0 4px 20px var(--blue-glow);
}
.btn-generate:active { transform: translateY(0); }
.btn-generate:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-generate svg {
width: 18px; height: 18px;
fill: currentColor;
}
.demo-hint {
text-align: center;
margin-top: 12px;
font-size: 13px;
color: var(--text-muted);
}
.demo-hint a {
color: var(--blue);
cursor: pointer;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
.demo-hint a:hover { border-bottom-color: var(--blue); }
/* ═══════════════════════════════════════════════════════
PIPELINE / STEPS INDICATOR
═══════════════════════════════════════════════════════ */
.pipeline {
display: none;
justify-content: center;
gap: 8px;
margin-bottom: 48px;
flex-wrap: wrap;
}
.pipeline.active { display: flex; }
.pipeline-step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 100px;
font-size: 13px;
color: var(--text-muted);
transition: all 0.5s;
}
.pipeline-step.active {
color: var(--blue);
border-color: rgba(0, 212, 255, 0.3);
background: rgba(0, 212, 255, 0.06);
}
.pipeline-step.done {
color: var(--green);
border-color: rgba(52, 211, 153, 0.3);
background: rgba(52, 211, 153, 0.06);
}
.pipeline-step .step-icon {
width: 20px; height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
border: 1.5px solid currentColor;
transition: all 0.5s;
}
.pipeline-step.done .step-icon {
background: var(--green);
border-color: var(--green);
color: #000;
}
.pipeline-step.active .step-icon {
border-color: var(--blue);
animation: spinStep 1.5s linear infinite;
}
@keyframes spinStep {
0% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.4); }
50% { box-shadow: 0 0 0 4px rgba(0, 212, 255, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.4); }
}
.pipeline-connector {
display: flex;
align-items: center;
color: var(--text-muted);
font-size: 16px;
}
/* ═══════════════════════════════════════════════════════
LOADING STATE
═══════════════════════════════════════════════════════ */
.loading-section {
display: none;
text-align: center;
padding: 80px 0;
}
.loading-section.active { display: block; }
.loader-ring {
width: 64px; height: 64px;
margin: 0 auto 24px;
border: 3px solid var(--glass-border);
border-top-color: var(--blue);
border-right-color: var(--coral);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text {
font-size: 18px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
.loading-subtext {
font-size: 14px;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
/* ═══════════════════════════════════════════════════════
ERROR STATE
═══════════════════════════════════════════════════════ */
.error-banner {
display: none;
max-width: 640px;
margin: 0 auto 32px;
padding: 16px 20px;
background: rgba(255, 107, 107, 0.08);
border: 1px solid rgba(255, 107, 107, 0.2);
border-radius: var(--radius-sm);
color: var(--coral);
font-size: 14px;
text-align: center;
}
.error-banner.active { display: block; }
/* ═══════════════════════════════════════════════════════
RESULTS SECTION
═══════════════════════════════════════════════════════ */
.results-section {
display: none;
padding-bottom: 100px;
}
.results-section.active { display: block; }
/* ═══════════════════════════════════════════════════════
BRAND PERSONA CARD
═══════════════════════════════════════════════════════ */
.brand-section {
margin-bottom: 64px;
opacity: 0;
transform: translateY(30px);
animation: fadeUp 0.7s ease-out forwards;
}
@keyframes fadeUp {
to { opacity: 1; transform: translateY(0); }
}
.section-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-bottom: 20px;
}
.section-label::after {
content: '';
flex: 1;
height: 1px;
background: var(--glass-border);
}
.brand-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
backdrop-filter: blur(20px);
overflow: hidden;
}
.brand-header {
display: flex;
align-items: center;
gap: 20px;
padding: 32px 32px 24px;
border-bottom: 1px solid var(--glass-border);
}
.brand-avatar {
width: 72px; height: 72px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 900;
color: #000;
flex-shrink: 0;
}
.brand-header-text h2 {
font-size: 24px;
font-weight: 800;
margin-bottom: 4px;
}
.brand-header-text .brand-tagline {
font-size: 15px;
color: var(--text-secondary);
font-style: italic;
}
.brand-body {
padding: 24px 32px 32px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 24px;
}
.brand-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.brand-meta-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.brand-meta-value {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.brand-colors {
display: flex;
gap: 8px;
margin-top: 4px;
}
.brand-color-swatch {
width: 28px; height: 28px;
border-radius: 8px;
border: 2px solid rgba(255,255,255,0.1);
}
.brand-benefits {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.brand-benefit-tag {
padding: 4px 10px;
background: rgba(0, 212, 255, 0.08);
border: 1px solid rgba(0, 212, 255, 0.15);
border-radius: 100px;
font-size: 12px;
color: var(--blue);
}
/* ═══════════════════════════════════════════════════════
AD GALLERY
═══════════════════════════════════════════════════════ */
.ads-section {
margin-bottom: 64px;
}
.ads-section .section-label { margin-bottom: 28px; }
.ads-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 24px;
}
.ad-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
overflow: hidden;
backdrop-filter: blur(20px);
opacity: 0;
transform: translateY(30px);
transition: transform 0.3s, box-shadow 0.3s;
}
.ad-card:hover {
transform: translateY(-4px) !important;
box-shadow: 0 12px 40px rgba(0,0,0,0.4);
}
.ad-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--glass-border);
}
.ad-format-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.ad-format-label .format-icon {
font-size: 16px;
}
.ad-format-tag {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ad-card-body { padding: 0; }
/* ─── MEME FORMAT ─── */
.meme-mockup {
position: relative;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
min-height: 280px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 24px 20px;
text-align: center;
}
.meme-mockup::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") repeat;
opacity: 0.5;
}
.meme-top, .meme-bottom {
position: relative;
z-index: 1;
font-size: 22px;
font-weight: 900;
text-transform: uppercase;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8), -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
line-height: 1.2;
}
.meme-center-emoji {
position: relative;
z-index: 1;
font-size: 64px;
margin: 16px 0;
}
/* ─── IMESSAGE FORMAT ─── */
.imessage-mockup {
background: #000;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.imessage-bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 18px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
}
.imessage-bubble.received {
align-self: flex-start;
background: #26252a;
color: #fff;
border-bottom-left-radius: 4px;
}
.imessage-bubble.sent {
align-self: flex-end;
background: #0b84fe;
color: #fff;
border-bottom-right-radius: 4px;
}
.imessage-header {
text-align: center;
padding: 4px 0 12px;
}
.imessage-header .contact-name {
font-size: 13px;
font-weight: 600;
color: rgba(255,255,255,0.9);
}
.imessage-header .contact-label {
font-size: 10px;
color: rgba(255,255,255,0.35);
}
.imessage-time {
text-align: center;
font-size: 11px;
color: rgba(255,255,255,0.25);
margin-bottom: 4px;
}
/* ─── TWEET FORMAT ─── */
.tweet-mockup {
padding: 20px;
background: #000;
}
.tweet-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.tweet-avatar {
width: 44px; height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 18px;
color: #000;
flex-shrink: 0;
}
.tweet-names {
display: flex;
flex-direction: column;
}
.tweet-display-name {
font-size: 15px;
font-weight: 700;
color: #e7e9ea;
display: flex;
align-items: center;
gap: 4px;
}
.tweet-verified {
width: 18px; height: 18px;
background: #1d9bf0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
}
.tweet-handle {
font-size: 14px;
color: #71767b;
}
.tweet-text {
font-size: 16px;
line-height: 1.5;
color: #e7e9ea;
margin-bottom: 16px;
}
.tweet-time {
font-size: 13px;
color: #71767b;
padding-bottom: 12px;
border-bottom: 1px solid #2f3336;
margin-bottom: 12px;
}
.tweet-stats {
display: flex;
gap: 24px;
}
.tweet-stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #71767b;
}
.tweet-stat .stat-icon {
font-size: 16px;
opacity: 0.7;
}
/* ─── STAT CARD FORMAT ─── */
.stat-mockup {
padding: 40px 24px;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-mockup::before {
content: '';
position: absolute;
top: -50%; left: -50%;
width: 200%; height: 200%;
background: radial-gradient(circle at center, rgba(0, 212, 255, 0.08) 0%, transparent 50%);
animation: statPulse 4s ease-in-out infinite;
}
@keyframes statPulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 1; }
}
.stat-big-number {
position: relative;
font-size: 72px;
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1;
margin-bottom: 12px;
background: linear-gradient(135deg, var(--blue), var(--purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
position: relative;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.stat-subtext {
position: relative;
font-size: 14px;
color: var(--text-secondary);
max-width: 300px;
margin: 0 auto 16px;
}
.stat-source {
position: relative;
font-size: 11px;
color: var(--text-muted);
font-style: italic;
}
/* ─── UGC / REVIEW FORMAT ─── */
.ugc-mockup {
padding: 24px 20px;
}
.ugc-stars {
display: flex;
gap: 2px;
margin-bottom: 12px;
}
.ugc-star {
font-size: 18px;
color: #fbbf24;
}
.ugc-star.empty { color: #333; }
.ugc-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-primary);
}
.ugc-body {
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
margin-bottom: 16px;
}
.ugc-meta {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
border-top: 1px solid var(--glass-border);
}
.ugc-reviewer {
display: flex;
align-items: center;
gap: 8px;
}
.ugc-reviewer-avatar {
width: 32px; height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, var(--coral), var(--purple));
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #fff;
}
.ugc-reviewer-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.ugc-verified {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--green);
font-weight: 500;
}
.ugc-platform {
font-size: 12px;
color: var(--text-muted);
}
/* ─── BILLBOARD FORMAT ─── */
.billboard-mockup {
position: relative;
min-height: 300px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 40px 32px;
overflow: hidden;
}
.billboard-headline {
position: relative;
z-index: 1;
font-size: clamp(28px, 4vw, 40px);
font-weight: 900;
text-transform: uppercase;
letter-spacing: -0.02em;
line-height: 1.1;
margin-bottom: 16px;
max-width: 500px;
}
.billboard-subline {
position: relative;
z-index: 1;
font-size: 16px;
color: rgba(255,255,255,0.7);
margin-bottom: 24px;
max-width: 400px;
}
.billboard-cta {
position: relative;
z-index: 1;
display: inline-flex;
padding: 12px 32px;
background: #fff;
color: #000;
font-size: 14px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 100px;
}
/* ═══════════════════════════════════════════════════════
FOOTER
═══════════════════════════════════════════════════════ */
.footer {
text-align: center;
padding: 40px 0 60px;
border-top: 1px solid var(--glass-border);
}
.footer-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 100px;
font-size: 12px;
color: var(--text-muted);
backdrop-filter: blur(20px);
}
.footer-badge .cc-icon {
width: 16px;
height: 16px;
background: linear-gradient(135deg, var(--blue), var(--purple));
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 900;
color: #fff;
}
/* ═══════════════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════════════ */
@media (max-width: 768px) {
.hero { padding: 48px 0 24px; }
.brand-header { flex-direction: column; text-align: center; }
.brand-body { grid-template-columns: 1fr; }
.ads-grid { grid-template-columns: 1fr; }
.input-wrapper { flex-direction: column; }
.btn-generate { justify-content: center; }
.pipeline-step span:not(.step-icon) { display: none; }
.brand-header { padding: 24px; }
.brand-body { padding: 20px; }
}
/* ═══════════════════════════════════════════════════════
ANIMATIONS
═══════════════════════════════════════════════════════ */
.ad-card.animate {
animation: fadeUp 0.6s ease-out forwards;
}
.ad-card.animate:nth-child(2) { animation-delay: 0.1s; }
.ad-card.animate:nth-child(3) { animation-delay: 0.2s; }
.ad-card.animate:nth-child(4) { animation-delay: 0.3s; }
.ad-card.animate:nth-child(5) { animation-delay: 0.4s; }
.ad-card.animate:nth-child(6) { animation-delay: 0.5s; }
/* Typing animation for iMessage */
.typing-indicator {
display: flex;
gap: 4px;
padding: 10px 14px;
align-self: flex-start;
background: #26252a;
border-radius: 18px;
border-bottom-left-radius: 4px;
}
.typing-dot {
width: 7px; height: 7px;
background: rgba(255,255,255,0.3);
border-radius: 50%;
animation: typingBounce 1.4s ease-in-out infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
</style>
</head>
<body>
<!-- Ambient background -->
<div class="ambient-bg">
<div class="ambient-orb"></div>
<div class="ambient-orb"></div>
<div class="ambient-orb"></div>
</div>
<div class="container">
<!-- Hero -->
<section class="hero">
<div class="hero-badge">
<span class="dot"></span>
AI-Powered Creative Engine
</div>
<h1>
Generate <span class="gradient-text">Scroll-Stopping</span><br>
Ad Creative in Seconds
</h1>
<p>
Paste any website URL. Our AI analyzes the brand, extracts their DNA, and generates
ad concepts across 6 high-converting formats — instantly.
</p>
</section>
<!-- Input -->
<section class="input-section">
<div class="input-wrapper">
<input
type="url"
id="urlInput"
placeholder="Paste a website URL — e.g., https://thevibemarketer.com"
autocomplete="off"
spellcheck="false"
>
<button class="btn-generate" id="generateBtn" onclick="handleGenerate()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
Generate
</button>
</div>
<div class="demo-hint">
or <a onclick="loadDemo()">try the demo</a> with pre-generated results
</div>
</section>
<!-- Pipeline Steps -->
<div class="pipeline" id="pipeline">
<div class="pipeline-step" id="step-fetch">
<span class="step-icon">1</span>
<span>Fetching Site</span>
</div>
<div class="pipeline-connector"></div>
<div class="pipeline-step" id="step-analyze">
<span class="step-icon">2</span>
<span>Analyzing Brand</span>
</div>
<div class="pipeline-connector"></div>
<div class="pipeline-step" id="step-generate">
<span class="step-icon">3</span>
<span>Generating Ads</span>
</div>
<div class="pipeline-connector"></div>
<div class="pipeline-step" id="step-render">
<span class="step-icon">4</span>
<span>Rendering</span>
</div>
</div>
<!-- Loading -->
<section class="loading-section" id="loadingSection">
<div class="loader-ring"></div>
<div class="loading-text" id="loadingText">Analyzing brand identity...</div>
<div class="loading-subtext" id="loadingSubtext">Extracting colors, voice, and positioning</div>
</section>
<!-- Error -->
<div class="error-banner" id="errorBanner"></div>
<!-- Results -->
<section class="results-section" id="resultsSection">
<!-- Brand Persona Card -->
<div class="brand-section" id="brandSection"></div>
<!-- Ad Gallery -->
<div class="ads-section" id="adsSection"></div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="footer-badge">
<span class="cc-icon"></span>
Powered by Claude Code × Anthropic
</div>
</footer>
</div>
<script>
/* ═══════════════════════════════════════════════════════
MAIN APPLICATION LOGIC
═══════════════════════════════════════════════════════ */
const API_BASE = window.location.origin;
// ─── Pipeline Step Management ───
function setStep(stepId, state) {
const el = document.getElementById(stepId);
if (!el) return;
el.classList.remove('active', 'done');
if (state) el.classList.add(state);
if (state === 'done') {
el.querySelector('.step-icon').textContent = '✓';
}
}
function showPipeline() {
const p = document.getElementById('pipeline');
p.classList.add('active');
// Reset all steps
['step-fetch', 'step-analyze', 'step-generate', 'step-render'].forEach(id => {
setStep(id, null);
const el = document.getElementById(id);
if (el) {
const num = id.split('-')[1] === 'fetch' ? '1' : id.split('-')[1] === 'analyze' ? '2' : id.split('-')[1] === 'generate' ? '3' : '4';
el.querySelector('.step-icon').textContent = num;
}
});
}
function setLoading(text, subtext) {
document.getElementById('loadingText').textContent = text;
document.getElementById('loadingSubtext').textContent = subtext;
}
// ─── Main Generate Handler ───
async function handleGenerate() {
const urlInput = document.getElementById('urlInput');
let url = urlInput.value.trim();
if (!url) {
url = 'https://thevibemarketer.com';
urlInput.value = url;
}
// Ensure protocol
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
urlInput.value = url;
}
const btn = document.getElementById('generateBtn');
btn.disabled = true;
// Reset UI
document.getElementById('errorBanner').classList.remove('active');
document.getElementById('resultsSection').classList.remove('active');
document.getElementById('loadingSection').classList.add('active');
showPipeline();
try {
// Step 1: Fetch website
setStep('step-fetch', 'active');
setLoading('Fetching website content...', `Downloading ${new URL(url).hostname}`);
let websiteContent = '';
try {
const fetchRes = await fetch(`${API_BASE}/api/fetch-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const fetchData = await fetchRes.json();
if (fetchData.success) {
websiteContent = fetchData.content;
}
} catch (e) {
console.warn('URL fetch failed, continuing with URL only:', e);
}
setStep('step-fetch', 'done');
// Step 2: Analyze with Claude
setStep('step-analyze', 'active');
setLoading('AI analyzing brand identity...', 'Extracting colors, voice, tone, and positioning');
await sleep(300); // Brief pause for visual effect
setStep('step-analyze', 'done');
setStep('step-generate', 'active');
setLoading('Generating ad creative concepts...', 'Creating 6 unique formats with tailored copy');
const genRes = await fetch(`${API_BASE}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, websiteContent: websiteContent || `Website URL: ${url}. Unable to fetch content directly, please analyze based on the URL and domain name.` })
});
const genData = await genRes.json();
if (!genData.success) {
throw new Error(genData.error || 'Generation failed');
}
setStep('step-generate', 'done');
// Step 3: Render results
setStep('step-render', 'active');
setLoading('Rendering ad mockups...', 'Building visual layouts');
await sleep(500);
setStep('step-render', 'done');
document.getElementById('loadingSection').classList.remove('active');
renderResults(genData.data);
} catch (err) {
console.error('Error:', err);
document.getElementById('loadingSection').classList.remove('active');
// Show error and fall back to demo
const errorBanner = document.getElementById('errorBanner');
errorBanner.textContent = `⚠️ ${err.message} — Loading demo results instead...`;
errorBanner.classList.add('active');
setTimeout(() => loadDemo(), 1500);
} finally {
btn.disabled = false;
}
}
// ─── Render Results ───
function renderResults(data) {
const { brand, ads } = data;
const resultsSection = document.getElementById('resultsSection');
// Brand Persona Card
document.getElementById('brandSection').innerHTML = `
<div class="section-label">🎯 Brand Persona Analysis</div>
<div class="brand-card">
<div class="brand-header">
<div class="brand-avatar" style="background: linear-gradient(135deg, ${brand.primaryColor || '#00d4ff'}, ${brand.secondaryColor || '#a855f7'})">
${(brand.name || 'B')[0]}
</div>
<div class="brand-header-text">
<h2>${escHtml(brand.name)}</h2>
<div class="brand-tagline">"${escHtml(brand.tagline)}"</div>
</div>
</div>
<div class="brand-body">
<div class="brand-meta">
<div class="brand-meta-label">Brand Voice</div>
<div class="brand-meta-value">${escHtml(brand.voice)}</div>
</div>
<div class="brand-meta">
<div class="brand-meta-label">Positioning</div>
<div class="brand-meta-value">${escHtml(brand.positioning)}</div>
</div>
<div class="brand-meta">
<div class="brand-meta-label">Industry</div>
<div class="brand-meta-value">${escHtml(brand.industry)}</div>
</div>
<div class="brand-meta">
<div class="brand-meta-label">Target Audience</div>
<div class="brand-meta-value">${escHtml(brand.targetAudience)}</div>
</div>
<div class="brand-meta">
<div class="brand-meta-label">Emotional Tone</div>
<div class="brand-meta-value">${escHtml(brand.emotionalTone)}</div>
</div>
<div class="brand-meta">
<div class="brand-meta-label">Brand Colors</div>
<div class="brand-colors">
<div class="brand-color-swatch" style="background: ${brand.primaryColor}" title="${brand.primaryColor}"></div>
<div class="brand-color-swatch" style="background: ${brand.secondaryColor}" title="${brand.secondaryColor}"></div>
<div class="brand-color-swatch" style="background: ${brand.accentColor}" title="${brand.accentColor}"></div>
</div>
</div>
<div class="brand-meta" style="grid-column: 1 / -1;">
<div class="brand-meta-label">Key Benefits</div>
<div class="brand-benefits">
${(brand.keyBenefits || []).map(b => `<span class="brand-benefit-tag">${escHtml(b)}</span>`).join('')}
</div>
</div>
</div>
</div>
`;
// Ad Gallery
const adsHtml = `
<div class="section-label">🎨 Generated Ad Concepts</div>
<div class="ads-grid">
${renderMemeCard(ads.meme, brand)}
${renderImessageCard(ads.iMessage, brand)}
${renderTweetCard(ads.tweet, brand)}
${renderStatCard(ads.statCard)}
${renderUgcCard(ads.ugc)}
${renderBillboardCard(ads.billboard, brand)}
</div>
`;
document.getElementById('adsSection').innerHTML = adsHtml;
resultsSection.classList.add('active');
// Animate cards in
setTimeout(() => {
document.querySelectorAll('.ad-card').forEach(card => card.classList.add('animate'));
}, 100);
// Scroll to results
document.getElementById('brandSection').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ─── Ad Card Renderers ───
function renderMemeCard(meme, brand) {
const emojis = ['😤', '💀', '🤯', '😂', '🔥', '👀', '💪', '🧠'];
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">🖼️</span> Meme</div>
<span class="ad-format-tag" style="background: rgba(251,191,36,0.15); color: #fbbf24;">Social</span>
</div>
<div class="ad-card-body">
<div class="meme-mockup">
<div class="meme-top">${escHtml(meme.topText)}</div>
<div class="meme-center-emoji">${emoji}</div>
<div class="meme-bottom">${escHtml(meme.bottomText)}</div>
</div>
</div>
</div>
`;
}
function renderImessageCard(im, brand) {
const messages = im.messages || [];
const bubbles = messages.map(m => {
const cls = m.sender === 'user' ? 'sent' : 'received';
return `<div class="imessage-bubble ${cls}">${escHtml(m.text)}</div>`;
}).join('');
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">💬</span> iMessage</div>
<span class="ad-format-tag" style="background: rgba(11,132,254,0.15); color: #0b84fe;">Conversational</span>
</div>
<div class="ad-card-body">
<div class="imessage-mockup">
<div class="imessage-header">
<div class="contact-name">bestie 🫶</div>
<div class="contact-label">iMessage</div>
</div>
<div class="imessage-time">Today ${new Date().getHours() % 12 || 12}:${String(new Date().getMinutes()).padStart(2, '0')} ${new Date().getHours() >= 12 ? 'PM' : 'AM'}</div>
${bubbles}
</div>
</div>
</div>
`;
}
function renderTweetCard(tweet, brand) {
const now = new Date();
const timeStr = `${now.getHours() % 12 || 12}:${String(now.getMinutes()).padStart(2, '0')} ${now.getHours() >= 12 ? 'PM' : 'AM'} · ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">𝕏</span> Tweet</div>
<span class="ad-format-tag" style="background: rgba(29,155,240,0.15); color: #1d9bf0;">Viral</span>
</div>
<div class="ad-card-body">
<div class="tweet-mockup">
<div class="tweet-header">
<div class="tweet-avatar" style="background: linear-gradient(135deg, ${brand.primaryColor || '#00d4ff'}, ${brand.secondaryColor || '#a855f7'})">${(tweet.displayName || 'B')[0]}</div>
<div class="tweet-names">
<div class="tweet-display-name">
${escHtml(tweet.displayName)}
<div class="tweet-verified">✓</div>
</div>
<div class="tweet-handle">${escHtml(tweet.handle)}</div>
</div>
</div>
<div class="tweet-text">${escHtml(tweet.text)}</div>
<div class="tweet-time">${timeStr}</div>
<div class="tweet-stats">
<div class="tweet-stat"><span class="stat-icon">💬</span> ${escHtml(tweet.replies)}</div>
<div class="tweet-stat"><span class="stat-icon">🔁</span> ${escHtml(tweet.retweets)}</div>
<div class="tweet-stat"><span class="stat-icon">❤️</span> ${escHtml(tweet.likes)}</div>
<div class="tweet-stat"><span class="stat-icon">📊</span> ${escHtml(tweet.views)}</div>
</div>
</div>
</div>
</div>
`;
}
function renderStatCard(stat) {
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">📊</span> Stat Card</div>
<span class="ad-format-tag" style="background: rgba(0,212,255,0.15); color: #00d4ff;">Data</span>
</div>
<div class="ad-card-body">
<div class="stat-mockup">
<div class="stat-big-number">${escHtml(stat.bigNumber)}</div>
<div class="stat-label">${escHtml(stat.label)}</div>
<div class="stat-subtext">${escHtml(stat.subtext)}</div>
<div class="stat-source">${escHtml(stat.source)}</div>
</div>
</div>
</div>
`;
}
function renderUgcCard(ugc) {
const stars = Array.from({ length: 5 }, (_, i) =>
`<span class="ugc-star ${i < ugc.rating ? '' : 'empty'}">★</span>`
).join('');
const initials = (ugc.reviewerName || 'U').split(' ').map(n => n[0]).join('');
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">⭐</span> UGC Review</div>
<span class="ad-format-tag" style="background: rgba(255,107,107,0.15); color: #ff6b6b;">Social Proof</span>
</div>
<div class="ad-card-body">
<div class="ugc-mockup">
<div class="ugc-stars">${stars}</div>
<div class="ugc-title">${escHtml(ugc.title)}</div>
<div class="ugc-body">${escHtml(ugc.body)}</div>
<div class="ugc-meta">
<div class="ugc-reviewer">
<div class="ugc-reviewer-avatar">${initials}</div>
<div>
<div class="ugc-reviewer-name">${escHtml(ugc.reviewerName)}</div>
<div class="ugc-platform">${escHtml(ugc.platform)}</div>
</div>
</div>
${ugc.verified ? '<div class="ugc-verified">✓ Verified Purchase</div>' : ''}
</div>
</div>
</div>
</div>
`;
}
function renderBillboardCard(billboard, brand) {
return `
<div class="ad-card">
<div class="ad-card-header">
<div class="ad-format-label"><span class="format-icon">🏗️</span> Billboard</div>
<span class="ad-format-tag" style="background: rgba(168,85,247,0.15); color: #a855f7;">Impact</span>
</div>
<div class="ad-card-body">
<div class="billboard-mockup" style="background: linear-gradient(135deg, ${brand.primaryColor || '#1a1a2e'}, ${brand.secondaryColor || '#0a0a0a'}, ${brand.accentColor || '#1a1a2e'})">
<div class="billboard-headline">${escHtml(billboard.headline)}</div>
<div class="billboard-subline">${escHtml(billboard.subline)}</div>
<div class="billboard-cta">${escHtml(billboard.cta)}</div>
</div>
</div>
</div>
`;
}
// ─── Utility ───
function escHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ─── Demo / Fallback Data ───
function loadDemo() {
document.getElementById('urlInput').value = 'https://thevibemarketer.com';
document.getElementById('errorBanner').classList.remove('active');
document.getElementById('loadingSection').classList.remove('active');
document.getElementById('pipeline').classList.remove('active');
renderResults(DEMO_DATA);
}
const DEMO_DATA = {
brand: {
name: "The Vibe Marketer",
tagline: "AI-powered marketing that actually vibes with your audience",
voice: "Confident yet approachable. Speaks in the language of modern creators — casual but backed by serious strategy. Avoids corporate jargon, embraces cultural fluency.",
positioning: "The AI marketing platform for brands that want to feel native on every platform, not just present. Where data meets cultural relevance.",
primaryColor: "#6C5CE7",
secondaryColor: "#00CEC9",
accentColor: "#FD79A8",
industry: "AI Marketing / AdTech",
targetAudience: "DTC founders, growth marketers, and creative directors at brands doing $1M-50M who need scroll-stopping creative at scale",
keyBenefits: ["AI-generated ad creative in seconds", "Platform-native formats", "Brand voice consistency", "10x content velocity", "Data-driven creative decisions"],
emotionalTone: "Energetic, empowering, and slightly irreverent — makes you feel like you're in on the future"
},
ads: {
meme: {
topText: "MY DESIGNER WHEN I ASK FOR 47 AD VARIATIONS BY TOMORROW",
bottomText: "THE VIBE MARKETER AI: 'SAY LESS FAM'",
context: "Stressed person transforming into calm person meme format"
},
iMessage: {
messages: [
{ sender: "friend", text: "how are you pumping out so much content lately?? your ads are everywhere 👀" },
{ sender: "user", text: "lol you're gonna think I'm lying" },
{ sender: "friend", text: "try me" },
{ sender: "user", text: "AI generates all my ad creative now. thevibemarketer.com — it literally analyzes my brand and creates scroll-stoppers in like 30 seconds" },
{ sender: "friend", text: "BRB canceling my agency retainer 😭" }
]
},
tweet: {
handle: "@thevibemarketer",
displayName: "The Vibe Marketer",
text: "Hot take: your brand doesn't have a content problem.\n\nIt has a creative velocity problem. 🚀\n\nWe just helped a DTC brand go from 4 ad variants/week to 200+.\n\nSame brand voice. Same quality. 50x the output.\n\nThe future of marketing isn't more people. It's better AI.",
likes: "4.2K",
retweets: "1.1K",
replies: "847",
views: "892K"
},
statCard: {
bigNumber: "50×",
label: "Creative Output Increase",
subtext: "Average increase in ad variants produced per week after switching to AI-powered creative generation",
source: "The Vibe Marketer — 2024 Customer Data"
},
ugc: {
reviewerName: "Sarah M.",
rating: 5,
title: "Replaced our entire creative team's bottleneck",
body: "We were spending $15K/month on a creative agency and waiting 2 weeks for ad variants. Now I paste our landing page URL, and in 30 seconds I have more creative options than my agency delivered in a month. The brand voice matching is scary accurate — my team couldn't tell the difference.",
platform: "G2 Reviews",
verified: true
},
billboard: {
headline: "YOUR BRAND DESERVES BETTER THAN GENERIC ADS",
subline: "AI creative that actually understands your vibe",
cta: "Start Creating Free →"
}
}
};
// ─── Keyboard shortcut ───
document.getElementById('urlInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleGenerate();
});
</script>
</body>
</html>