Stackless-Website/sandbox.html

2517 lines
95 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Stackless Sandbox — see what we'd build for you</title>
<meta name="description" content="A 60-second interactive preview. Pick your business shape, pick the headache, see the products that fix it — with real prices and live mini-mocks." />
<meta name="theme-color" content="#1DB897" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<link rel="manifest" href="manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="shared.css" />
<style>
/* ---------- sandbox-specific tokens ---------- */
:root {
--paper: #F4F2EC;
--surface: #FFFFFF;
--ink: #212A40;
--ink-2: #1C2439;
--muted: #6E7387;
--border: #E5E2D8;
--accent: #1DB897;
--accent-2: #F76857;
--ink-deep: #0F1525;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow-pop: 0 14px 40px -18px rgba(28, 36, 57, 0.28);
--shadow-card: 0 2px 4px rgba(28,36,57,0.04), 0 10px 30px -14px rgba(28,36,57,0.18);
}
.sand-wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
/* ---------- header ---------- */
.sand-header {
padding: 28px 0 18px;
display: flex; align-items: center; justify-content: space-between;
gap: 16px;
}
.sand-back {
font: 500 13.5px/1 var(--font-body);
color: var(--muted);
text-decoration: none;
padding: 8px 12px;
border-radius: 999px;
transition: background 0.15s, color 0.15s;
}
.sand-back:hover { background: rgba(33, 42, 64, 0.06); color: var(--ink-2); }
.sand-stepper {
display: flex; align-items: center; gap: 8px;
font: 500 12.5px/1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.sand-stepper .pip {
width: 28px; height: 4px; border-radius: 2px;
background: var(--border);
transition: background 0.2s;
}
.sand-stepper .pip.is-on { background: var(--ink-2); }
.sand-stepper .pip.is-done { background: var(--accent); }
.sand-stepper strong { color: var(--ink-2); font-weight: 600; }
/* ---------- main stage ---------- */
.sand-stage { padding: 24px 0 80px; min-height: calc(100vh - 120px); }
.sand-step { display: none; }
.sand-step.is-active { display: block; animation: sand-in 0.35s ease; }
@keyframes sand-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.sand-eyebrow {
font: 500 12.5px/1 var(--font-mono);
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 14px;
display: inline-flex; align-items: center; gap: 8px;
}
.sand-eyebrow::before { content: ""; width: 22px; height: 1px; background: var(--accent); }
.sand-h1 {
font: 600 clamp(34px, 4vw, 48px)/1.1 var(--font-display);
letter-spacing: -0.03em;
color: var(--ink-2);
max-width: 22ch;
}
.sand-h1 .accent { color: var(--accent); font-style: normal; }
.sand-sub {
margin-top: 16px;
font-size: 17px;
color: var(--muted);
max-width: 56ch;
line-height: 1.5;
}
/* ---------- archetype cards ---------- */
.arch-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 40px;
}
.arch-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 28px 26px;
cursor: pointer;
transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
text-align: left;
font: inherit;
color: inherit;
display: flex; flex-direction: column;
min-height: 280px;
}
.arch-card:hover { transform: translateY(-2px); border-color: rgba(33,42,64,0.24); box-shadow: var(--shadow-card); }
.arch-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.arch-card .arch-emoji {
width: 56px; height: 56px;
border-radius: 14px;
display: grid; place-items: center;
margin-bottom: 20px;
background: linear-gradient(135deg, rgba(29,184,151,0.10), rgba(247,104,87,0.06));
}
.arch-card .arch-emoji svg { width: 30px; height: 30px; color: var(--ink-2); }
.arch-card h3 {
font: 600 21px/1.2 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
margin-bottom: 8px;
}
.arch-card p { font-size: 14.5px; color: var(--muted); line-height: 1.5; flex: 1; }
.arch-card .arch-arrow {
margin-top: 18px;
font: 500 13.5px/1 var(--font-body);
color: var(--accent);
display: inline-flex; align-items: center; gap: 6px;
}
/* ---------- headache chips ---------- */
.head-grid {
display: grid;
grid-template-columns: 1fr;
gap: 14px;
margin-top: 36px;
max-width: 760px;
}
.head-card {
display: flex; align-items: flex-start; gap: 18px;
padding: 22px 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
cursor: pointer;
transition: border-color 0.15s, transform 0.15s;
text-align: left;
font: inherit;
color: inherit;
}
.head-card:hover { border-color: rgba(33,42,64,0.24); transform: translateX(2px); }
.head-card .head-num {
width: 32px; height: 32px; flex: none;
border-radius: 10px;
background: var(--paper);
color: var(--ink-2);
display: grid; place-items: center;
font: 600 13.5px/1 var(--font-mono);
letter-spacing: 0.04em;
}
.head-card h3 {
font: 600 17px/1.3 var(--font-display);
letter-spacing: -0.01em;
color: var(--ink-2);
margin-bottom: 4px;
}
.head-card p { font-size: 14.5px; color: var(--muted); line-height: 1.5; }
/* ---------- reveal stage ---------- */
.reveal-head {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 24px; flex-wrap: wrap;
margin-bottom: 32px;
}
.reveal-head .sand-h1 { max-width: 28ch; }
.price-summary {
display: flex; align-items: baseline; gap: 14px;
padding: 14px 22px;
background: var(--ink-2);
color: #F4F2EC;
border-radius: var(--r-md);
}
.price-summary__label {
font: 500 11.5px/1 var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(244,242,236,0.6);
}
.price-summary__amt {
font: 600 28px/1 var(--font-display);
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.price-summary__sub { font: 500 13px/1 var(--font-mono); color: rgba(244,242,236,0.6); }
.reveal-grid {
display: grid;
grid-template-columns: 1fr;
gap: 18px;
}
.reveal-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 0;
overflow: hidden;
display: grid;
grid-template-columns: 1.05fr 1fr;
min-height: 360px;
}
.reveal-text { padding: 32px 30px; display: flex; flex-direction: column; }
.reveal-text .kicker {
font: 500 11.5px/1 var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 12px;
}
.reveal-text h2 {
font: 600 26px/1.2 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
margin-bottom: 10px;
}
.reveal-text .blurb { font-size: 15px; color: var(--muted); line-height: 1.55; margin-bottom: 18px; }
.reveal-price {
display: flex; align-items: baseline; gap: 8px;
margin-bottom: 18px;
}
.reveal-price .amt {
font: 600 32px/1 var(--font-display);
letter-spacing: -0.025em;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.reveal-price .unit { font: 500 14px/1 var(--font-body); color: var(--muted); }
.reveal-text ul { list-style: none; margin: 0 0 22px; }
.reveal-text li {
padding: 6px 0 6px 22px;
position: relative;
font-size: 14px;
color: var(--ink);
line-height: 1.5;
}
.reveal-text li::before {
content: "";
position: absolute; left: 0; top: 13px;
width: 12px; height: 8px;
border-left: 2px solid var(--accent);
border-bottom: 2px solid var(--accent);
transform: rotate(-45deg);
}
.reveal-text .btn { align-self: flex-start; }
/* ---------- mini-mocks (right panel) ---------- */
.mock {
background: linear-gradient(160deg, #1C2439 0%, #0F1525 100%);
position: relative;
overflow: hidden;
display: grid; place-items: center;
padding: 28px;
}
.mock::after {
content: ""; position: absolute; inset: 0;
background-image:
radial-gradient(circle at 20% 20%, rgba(29,184,151,0.12), transparent 50%),
radial-gradient(circle at 80% 80%, rgba(247,104,87,0.10), transparent 50%);
pointer-events: none;
}
/* Phone mock — WhatsApp chat */
.phone {
width: 240px; height: 460px;
background: #0B0F1A;
border-radius: 32px;
border: 6px solid #1C2439;
box-shadow: 0 30px 60px -20px rgba(0,0,0,0.5);
overflow: hidden;
position: relative;
z-index: 1;
}
.phone__notch {
position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
width: 60px; height: 18px; background: #0B0F1A; border-radius: 0 0 14px 14px;
z-index: 3;
}
.phone__bar {
background: #1F2A3F;
padding: 14px 16px 10px;
display: flex; align-items: center; gap: 10px;
color: #F4F2EC;
}
.phone__bar .av {
width: 30px; height: 30px; border-radius: 50%;
background: var(--accent);
display: grid; place-items: center;
font: 600 13px/1 var(--font-display); color: var(--ink-2);
}
.phone__bar .name { font: 600 14px/1.2 var(--font-display); }
.phone__bar .sub { font: 500 11px/1 var(--font-mono); letter-spacing: 0.04em; color: rgba(255,255,255,0.6); margin-top: 2px; }
.phone__chat {
padding: 14px 12px;
background: #0F1525;
height: calc(100% - 56px);
overflow: hidden;
display: flex; flex-direction: column; gap: 8px;
}
.bub {
max-width: 80%;
padding: 8px 12px;
border-radius: 14px;
font-size: 12.5px; line-height: 1.4;
animation: bub-in 0.4s ease;
}
@keyframes bub-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.bub--in { background: #1F2A3F; color: #F4F2EC; align-self: flex-start; border-bottom-left-radius: 4px; }
.bub--out { background: #1DB897; color: #0B0F1A; align-self: flex-end; border-bottom-right-radius: 4px; font-weight: 500; }
.bub--typing { display: inline-flex; gap: 3px; padding: 12px 14px; }
.bub--typing span { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.4); animation: typing 1.2s infinite; }
.bub--typing span:nth-child(2) { animation-delay: 0.15s; }
.bub--typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes typing { 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-3px); } }
/* Workflow mock — node graph */
.flow {
width: 100%; max-width: 380px;
position: relative; z-index: 1;
}
.flow__row { display: flex; align-items: center; gap: 0; }
.flow__node {
flex: 1; padding: 12px 14px;
background: #1F2A3F;
border: 1px solid rgba(255,255,255,0.06);
border-radius: 12px;
color: #F4F2EC;
font: 500 12.5px/1.3 var(--font-body);
display: flex; align-items: center; gap: 10px;
animation: node-in 0.5s ease;
}
.flow__node .ico {
width: 28px; height: 28px; flex: none;
border-radius: 8px;
display: grid; place-items: center;
font: 600 11.5px/1 var(--font-mono);
letter-spacing: 0.04em;
}
.flow__node--lead .ico { background: rgba(29,184,151,0.18); color: var(--accent); }
.flow__node--crm .ico { background: rgba(247,104,87,0.18); color: var(--accent-2); }
.flow__node--email .ico { background: rgba(110,200,255,0.18); color: #6EC8FF; }
.flow__node--slack .ico { background: rgba(255,200,87,0.18); color: #FFC857; }
.flow__arrow {
width: 28px; flex: none;
display: grid; place-items: center;
color: rgba(255,255,255,0.4);
}
.flow__arrow svg { width: 18px; height: 18px; }
.flow__row + .flow__row { margin-top: 14px; }
.flow__pulse {
position: absolute;
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 0 rgba(29,184,151,0.6);
animation: pulse 1.6s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(29,184,151,0.6); }
70% { box-shadow: 0 0 0 12px rgba(29,184,151,0); }
100% { box-shadow: 0 0 0 0 rgba(29,184,151,0); }
}
@keyframes node-in { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: none; } }
.flow__label {
margin-top: 20px;
font: 500 11.5px/1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
text-align: center;
}
/* Instagram post mock */
.ig {
width: 260px;
background: #0B0F1A;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
overflow: hidden;
position: relative; z-index: 1;
box-shadow: 0 30px 60px -20px rgba(0,0,0,0.5);
}
.ig__head {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px;
}
.ig__av {
width: 30px; height: 30px; border-radius: 50%;
background: linear-gradient(135deg, #F76857, #FFC857);
display: grid; place-items: center;
font: 600 12.5px/1 var(--font-display); color: #fff;
}
.ig__name { font: 600 13px/1.2 var(--font-body); color: #F4F2EC; }
.ig__sub { font: 500 11px/1 var(--font-mono); color: rgba(255,255,255,0.5); margin-top: 2px; }
.ig__image {
aspect-ratio: 1 / 1;
background:
radial-gradient(circle at 30% 20%, rgba(29,184,151,0.45), transparent 50%),
radial-gradient(circle at 70% 80%, rgba(247,104,87,0.40), transparent 50%),
linear-gradient(160deg, #1C2439, #0F1525);
position: relative;
display: grid; place-items: center;
}
.ig__image .stamp {
font: 500 11px/1 var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255,255,255,0.6);
padding: 6px 10px;
background: rgba(0,0,0,0.3);
border-radius: 6px;
backdrop-filter: blur(4px);
}
.ig__caption {
padding: 12px 14px 14px;
font-size: 12.5px; line-height: 1.5;
color: rgba(255,255,255,0.85);
}
.ig__caption strong { color: #fff; font-weight: 600; }
.ig__sched {
margin: 0 14px 12px;
padding: 8px 10px;
background: rgba(29,184,151,0.10);
border: 1px solid rgba(29,184,151,0.25);
border-radius: 8px;
font: 500 11px/1.4 var(--font-mono);
letter-spacing: 0.04em;
color: var(--accent);
display: flex; align-items: center; gap: 6px;
}
/* Website / app / dashboard mocks — generic tile */
.tile-mock {
width: 100%; max-width: 380px;
background: #F4F2EC;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 20px 50px -20px rgba(0,0,0,0.5);
position: relative; z-index: 1;
color: var(--ink-2);
}
.tile-mock__bar {
background: #E5E2D8;
padding: 8px 12px;
display: flex; align-items: center; gap: 6px;
}
.tile-mock__bar span { width: 9px; height: 9px; border-radius: 50%; background: #C7C2B5; }
.tile-mock__bar span:first-child { background: #F76857; }
.tile-mock__bar span:nth-child(2) { background: #FFC857; }
.tile-mock__bar span:nth-child(3) { background: #1DB897; }
.tile-mock__body { padding: 22px 24px; min-height: 220px; }
.tile-mock h4 {
font: 600 18px/1.2 var(--font-display);
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.tile-mock p { font-size: 12.5px; color: var(--muted); line-height: 1.5; margin-bottom: 14px; }
.tile-mock__btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 14px; border-radius: 999px;
background: var(--accent-2); color: #fff;
font: 500 12.5px/1 var(--font-body);
}
.tile-mock__chart {
margin-top: 16px;
display: flex; align-items: flex-end; gap: 6px;
height: 60px;
}
.tile-mock__chart span {
flex: 1; background: var(--accent); border-radius: 3px 3px 0 0;
opacity: 0.5;
animation: bar 1.6s ease-in-out infinite;
}
.tile-mock__chart span:nth-child(1) { height: 40%; animation-delay: 0s; }
.tile-mock__chart span:nth-child(2) { height: 65%; animation-delay: 0.1s; }
.tile-mock__chart span:nth-child(3) { height: 50%; animation-delay: 0.2s; }
.tile-mock__chart span:nth-child(4) { height: 80%; animation-delay: 0.3s; }
.tile-mock__chart span:nth-child(5) { height: 70%; animation-delay: 0.4s; }
.tile-mock__chart span:nth-child(6) { height: 90%; animation-delay: 0.5s; }
.tile-mock__chart span:nth-child(7) { height: 60%; animation-delay: 0.6s; }
@keyframes bar { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
.tile-mock__rows { margin-top: 14px; }
.tile-mock__row {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border);
font-size: 12.5px;
}
.tile-mock__row:last-child { border-bottom: 0; }
.tile-mock__row .label { color: var(--ink-2); font-weight: 500; }
.tile-mock__row .val { color: var(--muted); font-variant-numeric: tabular-nums; }
.tile-mock__row .pill {
padding: 2px 8px;
border-radius: 999px;
font: 500 10.5px/1.4 var(--font-mono);
letter-spacing: 0.04em;
}
.tile-mock__row .pill--ok { background: rgba(29,184,151,0.14); color: #11876E; }
.tile-mock__row .pill--warn { background: rgba(255,200,87,0.18); color: #A87500; }
/* ============================================================
INTERACTIVE MOCK UPGRADES — every product gets a live, busy
demo: status badges, ticking numbers, animated state, etc.
============================================================ */
/* shared mock container override — keep dark gradient but
make room for the new busy components */
.mock { padding: 22px; }
.mock__label {
position: absolute; top: 14px; left: 18px;
font: 500 10.5px/1 var(--font-mono);
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255,255,255,0.45);
z-index: 2;
}
.mock__stamp {
position: absolute; top: 14px; right: 18px;
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 9px;
background: rgba(29,184,151,0.16);
border: 1px solid rgba(29,184,151,0.32);
border-radius: 999px;
font: 500 10.5px/1 var(--font-mono);
letter-spacing: 0.06em;
color: var(--accent);
z-index: 2;
}
.mock__stamp .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 0 rgba(29,184,151,0.6);
animation: livepulse 1.6s infinite;
}
@keyframes livepulse {
0% { box-shadow: 0 0 0 0 rgba(29,184,151,0.6); }
70% { box-shadow: 0 0 0 8px rgba(29,184,151,0); }
100% { box-shadow: 0 0 0 0 rgba(29,184,151,0); }
}
/* --- AI Worker: reply-time + escalation footer --- */
.phone__stamp {
position: absolute; top: 10px; right: 10px;
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 8px;
background: rgba(29,184,151,0.18);
border-radius: 999px;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.04em;
color: var(--accent);
z-index: 4;
}
.phone__badge {
align-self: center;
margin: 2px 0 4px;
padding: 4px 8px;
background: rgba(255,255,255,0.08);
border-radius: 999px;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.06em;
color: rgba(255,255,255,0.65);
animation: bub-in 0.4s ease;
}
.phone__escalate {
align-self: center;
margin-top: 6px;
padding: 5px 10px;
background: rgba(247,104,87,0.16);
border: 1px solid rgba(247,104,87,0.32);
border-radius: 8px;
font: 500 10px/1.3 var(--font-mono);
letter-spacing: 0.04em;
color: var(--accent-2);
animation: bub-in 0.4s ease;
}
.phone__escalate .arr { color: rgba(247,104,87,0.7); margin: 0 4px; }
/* --- Workflow: live canvas with travelling pulse --- */
.flow {
width: 100%; max-width: 360px;
position: relative; z-index: 1;
}
.flow__stamp {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 12px;
font: 500 10px/1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
}
.flow__stamp strong { color: var(--accent); font-weight: 600; }
.flow__node {
flex: 1; padding: 10px 12px;
background: #1F2A3F;
border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px;
color: #F4F2EC;
font: 500 11.5px/1.3 var(--font-body);
display: flex; align-items: center; gap: 9px;
transition: border-color 0.2s, background 0.2s, transform 0.2s;
}
.flow__node.is-active {
border-color: var(--accent);
background: #2A3A55;
transform: scale(1.02);
}
.flow__node.is-done {
border-color: rgba(29,184,151,0.32);
background: rgba(29,184,151,0.06);
}
.flow__node .ico {
width: 24px; height: 24px; flex: none;
border-radius: 6px;
display: grid; place-items: center;
font: 600 10.5px/1 var(--font-mono);
}
.flow__node--lead .ico { background: rgba(29,184,151,0.18); color: var(--accent); }
.flow__node--crm .ico { background: rgba(247,104,87,0.18); color: var(--accent-2); }
.flow__node--email .ico { background: rgba(110,200,255,0.18); color: #6EC8FF; }
.flow__node--slack .ico { background: rgba(255,200,87,0.18); color: #FFC857; }
.flow__node .check {
margin-left: auto;
width: 16px; height: 16px;
border-radius: 50%;
background: var(--accent);
color: var(--ink-2);
display: none;
align-items: center; justify-content: center;
}
.flow__node.is-done .check { display: inline-flex; }
.flow__node.is-done .check svg { width: 10px; height: 10px; }
.flow__arrow {
width: 24px; flex: none;
display: grid; place-items: center;
color: rgba(255,255,255,0.3);
}
.flow__arrow svg { width: 14px; height: 14px; }
.flow__arrow.is-active { color: var(--accent); }
.flow__row + .flow__row { margin-top: 8px; }
.flow__pulse {
position: absolute;
width: 10px; height: 10px; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 0 rgba(29,184,151,0.7);
pointer-events: none;
opacity: 0;
z-index: 3;
}
.flow__pulse.is-on {
opacity: 1;
animation: livepulse 1.4s infinite;
}
.flow__status {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,0.06);
display: flex; justify-content: space-between;
font: 500 10.5px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
}
.flow__status strong { color: var(--accent); font-weight: 600; }
/* --- Social: live composer --- */
.ig {
width: 280px;
background: #0B0F1A;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
overflow: hidden;
position: relative; z-index: 1;
box-shadow: 0 30px 60px -20px rgba(0,0,0,0.5);
}
.ig__head {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px 8px;
}
.ig__av {
width: 30px; height: 30px; border-radius: 50%;
background: linear-gradient(135deg, #F76857, #FFC857);
display: grid; place-items: center;
font: 600 12.5px/1 var(--font-display); color: #fff;
}
.ig__name { font: 600 12.5px/1.2 var(--font-body); color: #F4F2EC; }
.ig__sub { font: 500 10.5px/1 var(--font-mono); color: rgba(255,255,255,0.5); margin-top: 2px; }
.ig__status {
margin-left: auto;
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 8px;
background: rgba(29,184,151,0.16);
border-radius: 999px;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.04em;
color: var(--accent);
}
.ig__status .dot {
width: 5px; height: 5px; border-radius: 50%;
background: var(--accent);
animation: livepulse 1.4s infinite;
}
.ig__composer {
margin: 0 14px 8px;
background: #1F2A3F;
border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px;
padding: 10px 12px;
min-height: 60px;
}
.ig__composer-label {
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 6px;
}
.ig__caption {
font-size: 11.5px;
line-height: 1.45;
color: rgba(255,255,255,0.85);
min-height: 32px;
}
.ig__caption .cursor {
display: inline-block;
width: 1.5px; height: 12px;
background: var(--accent);
margin-left: 1px;
vertical-align: middle;
animation: blink 0.9s infinite;
}
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
.ig__image {
aspect-ratio: 1 / 1;
background:
radial-gradient(circle at 30% 20%, rgba(29,184,151,0.45), transparent 50%),
radial-gradient(circle at 70% 80%, rgba(247,104,87,0.40), transparent 50%),
linear-gradient(160deg, #1C2439, #0F1525);
position: relative;
display: grid; place-items: center;
}
.ig__image .stamp {
font: 500 11px/1 var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255,255,255,0.65);
padding: 6px 12px;
background: rgba(0,0,0,0.35);
border-radius: 8px;
backdrop-filter: blur(4px);
}
.ig__image .watermark {
position: absolute; bottom: 8px; right: 8px;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.08em;
color: rgba(255,255,255,0.5);
}
.ig__queue {
padding: 8px 14px 12px;
display: flex; gap: 6px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.ig__queue-item {
flex: 1;
aspect-ratio: 1 / 1;
border-radius: 6px;
background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.04);
display: grid; place-items: center;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.04em;
color: rgba(255,255,255,0.4);
position: relative;
}
.ig__queue-item.is-on {
border-color: var(--accent);
background: rgba(29,184,151,0.10);
color: var(--accent);
}
.ig__queue-item.is-on::after {
content: "NOW";
position: absolute; top: -6px; right: -6px;
padding: 2px 5px;
background: var(--accent);
color: var(--ink-2);
font: 700 8px/1 var(--font-mono);
border-radius: 4px;
}
/* --- Website: interactive landing preview --- */
.site {
width: 100%; max-width: 360px;
background: var(--surface);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 50px -20px rgba(0,0,0,0.5);
position: relative; z-index: 1;
color: var(--ink-2);
}
.site__chrome {
background: #E5E2D8;
padding: 8px 12px;
display: flex; align-items: center; gap: 6px;
}
.site__chrome span { width: 9px; height: 9px; border-radius: 50%; background: #C7C2B5; }
.site__chrome span:first-child { background: #F76857; }
.site__chrome span:nth-child(2) { background: #FFC857; }
.site__chrome span:nth-child(3) { background: #1DB897; }
.site__url {
margin-left: 8px; flex: 1;
font: 500 10px/1 var(--font-mono);
color: var(--muted);
background: #fff;
padding: 4px 8px;
border-radius: 4px;
}
.site__nav {
display: flex; gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
font: 500 10px/1 var(--font-mono);
color: var(--muted);
letter-spacing: 0.04em;
}
.site__nav .is-on { color: var(--ink-2); font-weight: 600; }
.site__tab {
flex: 1; text-align: center;
padding: 8px 0;
background: transparent;
border: 0;
font: 500 10px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.site__tab.is-on { color: var(--ink-2); border-bottom-color: var(--accent); }
.site__view {
padding: 18px 16px 16px;
min-height: 240px;
}
.site__view--hero h5 {
font: 600 14px/1.2 var(--font-display);
letter-spacing: -0.01em;
margin-bottom: 6px;
}
.site__view--hero p { font-size: 10.5px; color: var(--muted); line-height: 1.45; margin-bottom: 12px; }
.site__view--services { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.site__service {
padding: 10px;
background: var(--paper);
border-radius: 6px;
font-size: 10px;
}
.site__service strong {
display: block;
font: 600 10.5px/1.2 var(--font-display);
margin-bottom: 3px;
}
.site__service span { color: var(--muted); font-size: 9.5px; }
.site__view--contact label {
display: block;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 4px;
margin-top: 8px;
}
.site__view--contact label:first-child { margin-top: 0; }
.site__view--contact input,
.site__view--contact textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 5px;
font: 400 10.5px/1.4 var(--font-body);
color: var(--ink-2);
background: var(--surface);
}
.site__view--contact input { height: 26px; }
.site__view--contact textarea { height: 38px; resize: none; }
.site__cta {
width: 100%;
margin-top: 12px;
padding: 9px 14px;
border-radius: 999px;
background: var(--accent-2);
color: #fff;
font: 500 11px/1 var(--font-body);
border: 0;
cursor: pointer;
position: relative;
overflow: hidden;
}
.site__cta::after {
content: "";
position: absolute; inset: 0;
background: radial-gradient(circle at center, rgba(255,255,255,0.3), transparent 60%);
opacity: 0;
transition: opacity 0.2s;
}
.site__cta:hover::after { opacity: 1; }
.site__cta:hover { background: #e25645; }
/* --- App: live ops dashboard --- */
.ops {
width: 100%; max-width: 360px;
background: var(--surface);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 50px -20px rgba(0,0,0,0.5);
position: relative; z-index: 1;
color: var(--ink-2);
}
.ops__chrome {
background: #E5E2D8;
padding: 8px 12px;
display: flex; align-items: center; gap: 6px;
}
.ops__chrome span { width: 9px; height: 9px; border-radius: 50%; background: #C7C2B5; }
.ops__chrome span:first-child { background: #F76857; }
.ops__chrome span:nth-child(2) { background: #FFC857; }
.ops__chrome span:nth-child(3) { background: #1DB897; }
.ops__title {
margin-left: 8px;
font: 500 10px/1 var(--font-mono);
color: var(--muted);
}
.ops__view { padding: 14px 16px 16px; }
.ops__kpis {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
margin-bottom: 14px;
}
.ops__kpi {
padding: 8px 10px;
background: var(--paper);
border-radius: 6px;
}
.ops__kpi-label {
font: 500 9px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 4px;
}
.ops__kpi-val {
font: 600 16px/1 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.ops__kpi-delta {
font: 500 9.5px/1 var(--font-mono);
color: #11876E;
margin-top: 3px;
}
.ops__chart {
display: flex; align-items: flex-end; gap: 4px;
height: 60px;
padding: 8px;
background: var(--paper);
border-radius: 6px;
margin-bottom: 12px;
}
.ops__chart span {
flex: 1; background: var(--accent); border-radius: 2px 2px 0 0;
opacity: 0.55;
transition: height 0.6s ease, opacity 0.3s;
}
.ops__list { display: flex; flex-direction: column; gap: 4px; }
.ops__list-head {
display: flex; justify-content: space-between;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 4px;
}
.ops__row {
display: flex; align-items: center; gap: 8px;
padding: 6px 8px;
background: var(--paper);
border-radius: 5px;
font-size: 10.5px;
animation: row-in 0.4s ease;
}
@keyframes row-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.ops__row .id {
font: 600 10.5px/1 var(--font-mono);
color: var(--ink-2);
}
.ops__row .name { flex: 1; color: var(--muted); }
.ops__row .pill {
padding: 2px 6px;
border-radius: 999px;
font: 500 9px/1.3 var(--font-mono);
letter-spacing: 0.04em;
}
.pill--ok { background: rgba(29,184,151,0.14); color: #11876E; }
.pill--warn { background: rgba(255,200,87,0.18); color: #A87500; }
.pill--proc { background: rgba(110,200,255,0.18); color: #1D6FA8; }
/* --- Dashboard: live KPI snapshot with auto-refresh --- */
.kpi {
width: 100%; max-width: 360px;
background: var(--surface);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 50px -20px rgba(0,0,0,0.5);
position: relative; z-index: 1;
color: var(--ink-2);
}
.kpi__chrome {
background: #E5E2D8;
padding: 8px 12px;
display: flex; align-items: center; gap: 6px;
}
.kpi__chrome span { width: 9px; height: 9px; border-radius: 50%; background: #C7C2B5; }
.kpi__chrome span:first-child { background: #F76857; }
.kpi__chrome span:nth-child(2) { background: #FFC857; }
.kpi__chrome span:nth-child(3) { background: #1DB897; }
.kpi__title { margin-left: 8px; font: 500 10px/1 var(--font-mono); color: var(--muted); }
.kpi__view { padding: 16px; }
.kpi__head {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 14px;
}
.kpi__head h6 {
font: 600 13px/1.2 var(--font-display);
letter-spacing: -0.01em;
}
.kpi__refresh {
display: inline-flex; align-items: center; gap: 5px;
font: 500 9.5px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.kpi__refresh .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent);
animation: livepulse 1.6s infinite;
}
.kpi__refresh.is-flash { color: var(--accent); }
.kpi__row {
display: flex; justify-content: space-between; align-items: baseline;
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
.kpi__row:last-child { border-bottom: 0; }
.kpi__row .label {
font-size: 11.5px;
color: var(--muted);
}
.kpi__row .val {
font: 600 15px/1 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
transition: color 0.3s;
}
.kpi__row .val.is-flash { color: var(--accent); }
.kpi__row .pill {
padding: 2px 7px;
border-radius: 999px;
font: 500 9.5px/1.3 var(--font-mono);
letter-spacing: 0.04em;
}
.kpi__row .right { display: flex; align-items: center; gap: 8px; }
.kpi__row .arr { color: var(--muted); font-size: 11px; }
/* ---------- nav buttons ---------- */
.sand-nav {
display: flex; justify-content: space-between; align-items: center;
margin-top: 36px;
gap: 16px;
}
.sand-nav .left, .sand-nav .right { display: flex; gap: 10px; align-items: center; }
.btn-ghost {
background: transparent; color: var(--ink); border: 1px solid var(--ink);
padding: 10px 16px; border-radius: 999px;
font: 500 14px/1 var(--font-body);
cursor: pointer;
transition: background 0.15s, color 0.15s;
display: inline-flex; align-items: center; gap: 8px;
}
.btn-ghost:hover { background: var(--ink); color: #fff; }
.btn-ghost:disabled { opacity: 0.4; cursor: default; }
.btn-ghost:disabled:hover { background: transparent; color: var(--ink); }
.btn-solid {
background: var(--ink-2); color: #fff; border: 0;
padding: 12px 22px; border-radius: 999px;
font: 500 15px/1 var(--font-body);
cursor: pointer;
transition: background 0.15s, transform 0.12s;
display: inline-flex; align-items: center; gap: 10px;
}
.btn-solid:hover { background: #0F1525; }
.btn-solid:active { transform: translateY(1px); }
.btn-accent {
background: var(--accent); color: var(--ink-2); border: 0;
padding: 12px 22px; border-radius: 999px;
font: 500 15px/1 var(--font-body);
cursor: pointer;
transition: background 0.15s, transform 0.12s;
display: inline-flex; align-items: center; gap: 10px;
}
.btn-accent:hover { background: #1AA888; }
.btn-accent:active { transform: translateY(1px); }
/* ---------- email gate (overlay) ---------- */
.gate-backdrop {
position: fixed; inset: 0;
background: rgba(15, 21, 37, 0.72);
backdrop-filter: blur(8px);
display: none;
align-items: center; justify-content: center;
z-index: 100;
padding: 20px;
animation: fade-in 0.2s ease;
}
.gate-backdrop.is-open { display: flex; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.gate {
background: var(--surface);
border-radius: var(--r-lg);
padding: 40px 38px;
max-width: 460px;
width: 100%;
box-shadow: 0 30px 80px -20px rgba(0,0,0,0.5);
animation: gate-in 0.3s ease;
}
@keyframes gate-in { from { opacity: 0; transform: translateY(20px) scale(0.96); } to { opacity: 1; transform: none; } }
.gate h2 {
font: 600 24px/1.2 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
margin-bottom: 8px;
}
.gate p { font-size: 14.5px; color: var(--muted); line-height: 1.5; margin-bottom: 22px; }
.gate__field { margin-bottom: 12px; }
.gate__field label {
display: block; margin-bottom: 6px;
font: 500 12.5px/1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.gate__field input {
width: 100%; padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 10px;
font: 400 15px/1.4 var(--font-body);
color: var(--ink-2);
background: var(--surface);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.gate__field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(29,184,151,0.15); }
.gate__field input.is-invalid { border-color: var(--accent-2); }
.gate__err {
display: none;
margin-top: 8px;
padding: 10px 12px;
background: #FFF5F3;
border: 1px solid #F7C8BF;
border-radius: 8px;
font-size: 13px;
color: #A8341F;
}
.gate__err.is-on { display: block; }
.gate__actions {
display: flex; gap: 10px; align-items: center;
margin-top: 22px;
}
.gate__actions .btn-solid { flex: 1; justify-content: center; }
.gate__close {
position: absolute; top: 14px; right: 14px;
background: transparent; border: 0; padding: 6px;
cursor: pointer; color: var(--muted);
border-radius: 8px;
display: grid; place-items: center;
}
.gate__close:hover { background: var(--paper); color: var(--ink-2); }
.gate__close svg { width: 18px; height: 18px; }
.gate__small {
margin-top: 14px;
font-size: 12px;
color: var(--muted);
text-align: center;
}
.gate__success {
display: none;
text-align: center;
padding: 20px 0;
}
.gate__success.is-on { display: block; }
.gate__success .check {
width: 56px; height: 56px;
border-radius: 50%;
background: var(--accent);
color: var(--ink-2);
display: grid; place-items: center;
margin: 0 auto 18px;
}
.gate__success .check svg { width: 28px; height: 28px; }
.gate__success h3 {
font: 600 22px/1.2 var(--font-display);
letter-spacing: -0.02em;
color: var(--ink-2);
margin-bottom: 8px;
}
.gate__success p { color: var(--muted); margin-bottom: 20px; }
.gate__link {
display: flex; align-items: center; gap: 8px;
padding: 10px 12px;
background: var(--paper);
border: 1px solid var(--border);
border-radius: 10px;
font: 500 12.5px/1.2 var(--font-mono);
color: var(--ink-2);
word-break: break-all;
}
/* ---------- final share state ---------- */
.final-state {
display: none;
text-align: center;
padding: 40px 0;
}
.final-state.is-on { display: block; }
.final-state h2 {
font: 600 36px/1.1 var(--font-display);
letter-spacing: -0.025em;
color: var(--ink-2);
max-width: 18ch;
margin: 0 auto 14px;
}
.final-state p { font-size: 16px; color: var(--muted); max-width: 50ch; margin: 0 auto 28px; }
.final-state__actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* ---------- responsive ---------- */
@media (max-width: 880px) {
.arch-grid { grid-template-columns: 1fr; }
.reveal-card { grid-template-columns: 1fr; }
.mock { min-height: 360px; }
}
@media (max-width: 560px) {
.sand-stepper { display: none; }
.arch-card { min-height: auto; }
.gate { padding: 30px 24px; }
}
</style>
</head>
<body>
<header class="sand-header sand-wrap">
<a href="pricing.html" class="sand-back">← Back to pricing</a>
<div class="sand-stepper" id="stepper">
<span class="pip is-on" data-pip="0"></span>
<span class="pip" data-pip="1"></span>
<span class="pip" data-pip="2"></span>
<span class="pip" data-pip="3"></span>
<span>Step <strong id="stepNum">1</strong> of 4</span>
</div>
</header>
<main class="sand-stage">
<div class="sand-wrap">
<!-- ============= STEP 1: ARCHETYPE ============= -->
<section class="sand-step is-active" data-step="0">
<span class="sand-eyebrow">Sandbox · 60 seconds</span>
<h1 class="sand-h1">What does your business <span class="accent">look like</span>?</h1>
<p class="sand-sub">Pick the shape closest to yours. We'll show you which of our six products actually apply — with the real prices, not the brochure ones.</p>
<div class="arch-grid" id="archGrid"></div>
</section>
<!-- ============= STEP 2: HEADACHE ============= -->
<section class="sand-step" data-step="1">
<span class="sand-eyebrow">Sandbox · what's biting</span>
<h1 class="sand-h1">What's eating your week?</h1>
<p class="sand-sub" id="headSub">Pick the one that costs the most time. We'll show you what we'd build to kill it.</p>
<div class="head-grid" id="headGrid"></div>
<div class="sand-nav">
<button class="btn-ghost" data-go="prev">← Change business type</button>
</div>
</section>
<!-- ============= STEP 3: REVEAL ============= -->
<section class="sand-step" data-step="2">
<div class="reveal-head">
<div>
<span class="sand-eyebrow">Your shortlist</span>
<h1 class="sand-h1" id="revealH1">Here's what we'd build.</h1>
<p class="sand-sub" id="revealSub">Live mini-mocks below show each one doing its actual job. The total is your all-in monthly — no hidden setup fees, no "contact us" asterisks.</p>
</div>
<div class="price-summary">
<div>
<div class="price-summary__label">All-in monthly</div>
<div class="price-summary__amt" id="priceTotal">R0</div>
</div>
<div class="price-summary__sub" id="priceCycle">/ month</div>
</div>
</div>
<div class="reveal-grid" id="revealGrid"></div>
<div class="sand-nav">
<button class="btn-ghost" data-go="prev">← Different headache</button>
<button class="btn-accent" id="openGate">Save &amp; share this →</button>
</div>
</section>
<!-- ============= STEP 4: SAVED ============= -->
<section class="sand-step" data-step="3">
<div class="final-state is-on" id="finalState">
<span class="sand-eyebrow">Saved</span>
<h2>Your shortlist is on file.</h2>
<p>We've sent a copy to your inbox with the link, and you can share it with anyone who needs to be in the room when you make the call.</p>
<div class="final-state__actions">
<a class="btn-solid" id="finalLink" href="#" target="_blank" rel="noopener">Open my config</a>
<a class="btn-ghost" href="contact.html?topic=sandbox">Talk to a human →</a>
</div>
</div>
</section>
</div>
</main>
<!-- ============= EMAIL GATE ============= -->
<div class="gate-backdrop" id="gateBackdrop" role="dialog" aria-modal="true" aria-labelledby="gateTitle">
<div class="gate" style="position: relative;">
<button class="gate__close" id="gateClose" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div id="gateForm">
<h2 id="gateTitle">Send me my shortlist.</h2>
<p>One email, the link to your config, no follow-up sequence.</p>
<div class="gate__field">
<label for="gateName">Your name</label>
<input type="text" id="gateName" placeholder="Jane" autocomplete="given-name" required />
</div>
<div class="gate__field">
<label for="gateEmail">Email</label>
<input type="email" id="gateEmail" placeholder="jane@business.co.za" autocomplete="email" required />
</div>
<div class="gate__err" id="gateErr">Please enter a valid name and email.</div>
<div class="gate__actions">
<button class="btn-solid" id="gateSubmit">Send me the link</button>
</div>
<p class="gate__small">By submitting you agree to one email from us. No list, no sequence, no retargeting pixels.</p>
</div>
<div class="gate__success" id="gateSuccess">
<div class="check">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<h3>Sent. Here's your link now:</h3>
<p>Open it any time to come back to this config.</p>
<div class="gate__link" id="gateLinkText"></div>
<div class="gate__actions" style="margin-top: 18px;">
<button class="btn-solid" id="gateOpen">Open my config</button>
</div>
</div>
</div>
</div>
<script src="header.js"></script>
<script>
/* =============================================================
STACKLESS SANDBOX
4 steps: archetype → headache → reveal (with mini-mocks)
→ email gate → shareable URL
============================================================= */
(function () {
// -------- 1. ARCHETYPE + HEADACHE DATA -----------------------
var ARCH = {
trades: {
id: 'trades',
label: 'Trades &amp; field service',
blurb: 'Plumber, electrician, contractor, handyman — on the road, on the phone, leaving quotes at 10pm.',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.4 2.4-2-2 2.4-2.4z"/></svg>',
headaches: [
{
id: 'missed-enquiries',
title: 'Missed enquiries after 5pm',
desc: 'Customers WhatsApp at 8pm, you reply at 6am, they\'ve already booked someone else.',
products: ['ai-worker', 'website']
},
{
id: 'quote-followup',
title: 'Quotes that go cold',
desc: 'You send 10 quotes a week, 3 come back. The other 7 just… disappear.',
products: ['ai-worker', 'workflow']
},
{
id: 'paperwork',
title: 'Paperwork &amp; invoicing chaos',
desc: 'Job done on Saturday, invoice sent on Wednesday, paid in 60 days. Cash flow is a nightmare.',
products: ['workflow', 'dashboard']
}
]
},
creator: {
id: 'creator',
label: 'Content &amp; creator',
blurb: 'Influencer, freelancer, agency, photographer — making the work and running the business at the same time.',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M7 21l5-3 5 3"/></svg>',
headaches: [
{
id: 'silent-socials',
title: 'Socials go quiet for weeks',
desc: 'You know you should post, you don\'t, and the algorithm punishes you for it.',
products: ['social']
},
{
id: 'inbox-overload',
title: 'Inbox full of "what\'s your rate?"',
desc: '40% of enquiries are the wrong fit. You reply anyway because you never know.',
products: ['ai-worker', 'website']
},
{
id: 'client-onboarding',
title: 'Onboarding every client from scratch',
desc: 'Same 8 emails, same brief, same "when can you start?" — every single time.',
products: ['workflow', 'ai-worker']
}
]
},
professional: {
id: 'professional',
label: 'Professional services',
blurb: 'Lawyer, accountant, consultant, agency — billing by the hour, drowning in admin you don\'t bill for.',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18M3 12h18M3 17h12"/></svg>',
headaches: [
{
id: 'lead-qual',
title: 'Leads that aren\'t leads',
desc: 'You spend 30 minutes on a call only to discover they can\'t afford you and never could.',
products: ['ai-worker', 'website']
},
{
id: 'data-entry',
title: 'Data entry across 4 apps',
desc: 'CRM, accounting, time tracker, document storage — every entry is the same data four times.',
products: ['workflow', 'dashboard']
},
{
id: 'reporting',
title: 'Reports that take a day to build',
desc: 'Every Monday you spend 4 hours exporting CSVs and formatting a deck nobody reads carefully.',
products: ['dashboard', 'app']
}
]
}
};
// -------- 2. PRODUCT CATALOGUE (matches pricing.html) --------
var PRODUCTS = {
'ai-worker': {
id: 'ai-worker', num: '01',
kicker: 'AI Worker',
name: 'Your own automated worker.',
blurb: 'Chat to it, give it tasks, it gets them done. A digital employee, not a chatbot.',
bullets: [
'Answers WhatsApp &amp; email enquiries in your tone',
'Drafts quote responses, follow-ups, status updates',
'Escalates to a human when the rules say so'
],
price: 1400, unit: '/ month',
priceRange: 'R1 400',
onceOff: false,
mockType: 'phone'
},
workflow: {
id: 'workflow', num: '02',
kicker: 'Workflow Automation',
name: 'Connect your apps. Automate the busywork.',
blurb: 'Lead to CRM, email to quote, invoice to ledger. We build the wire and keep it running.',
bullets: [
'One end-to-end automation, tested and live',
'Monitored monthly, fixed when it breaks',
'Scales to 2+ workflows on the care plan'
],
price: 3500, unit: 'once-off',
priceRange: 'R3 500',
onceOff: true,
mockType: 'flow'
},
social: {
id: 'social', num: '03',
kicker: 'Social Auto-Post',
name: 'Facebook &amp; Instagram on autopilot.',
blurb: 'Posts written and scheduled automatically — so the page never goes quiet while you run the business.',
bullets: [
'~12 posts a month, AI-written in your voice',
'Both platforms, scheduled at the right times',
'Monthly performance digest'
],
price: 1800, unit: '/ month',
priceRange: 'R1 800',
onceOff: false,
mockType: 'ig'
},
website: {
id: 'website', num: '04',
kicker: 'Starter Site',
name: 'A site that ships in a week, not a quarter.',
blurb: 'A 1-page template landing site when you need a credible URL. Care plan keeps it live after.',
bullets: [
'Copy, design &amp; deploy in 5–7 days',
'Hosting, backups &amp; small edits included',
'Care plan from R650/month — cancel any time'
],
price: 1500, unit: 'once-off',
priceRange: 'R1 500',
onceOff: true,
mockType: 'tile-website'
},
app: {
id: 'app', num: '05',
kicker: 'App',
name: 'Internal tools, dashboards, portals.',
blurb: 'Adapted from our existing app components — order systems, ops dashboards, agent tooling.',
bullets: [
'Quoted to scope, component-based not from scratch',
'You own the code, the data, the accounts',
'Support retainer from R1.2k/month'
],
price: 8500, unit: 'starting',
priceRange: 'from R8 500',
onceOff: true,
mockType: 'tile-app'
},
dashboard: {
id: 'dashboard', num: '06',
kicker: 'Dashboard',
name: 'Numbers in one place, refreshed automatically.',
blurb: 'A live view of the metrics that actually run the business, pulling from the apps you already use.',
bullets: [
'First dashboard scoped to your real questions',
'Auto-refreshes from Xero, Shopify, HubSpot, etc.',
'Maintenance retainer from R800/month'
],
price: 4500, unit: 'starting',
priceRange: 'from R4 500',
onceOff: true,
mockType: 'tile-dashboard'
}
};
// -------- 3. STATE -------------------------------------------
var state = {
archetype: null,
headache: null,
products: []
};
// -------- 4. STEP ROUTER -------------------------------------
function go(stepIdx) {
var steps = document.querySelectorAll('.sand-step');
steps.forEach(function (s) { s.classList.remove('is-active'); });
var target = document.querySelector('.sand-step[data-step="' + stepIdx + '"]');
if (target) target.classList.add('is-active');
document.getElementById('stepNum').textContent = (stepIdx + 1);
document.querySelectorAll('.sand-stepper .pip').forEach(function (p, i) {
p.classList.toggle('is-on', i === stepIdx);
p.classList.toggle('is-done', i < stepIdx);
});
// smooth scroll to top of stage
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.querySelectorAll('[data-go]').forEach(function (b) {
b.addEventListener('click', function () {
var dir = b.getAttribute('data-go');
var cur = parseInt(document.querySelector('.sand-step.is-active').getAttribute('data-step'), 10);
go(dir === 'next' ? cur + 1 : cur - 1);
});
});
// -------- 5. RENDER: STEP 1 (ARCHETYPES) ---------------------
function renderArchetypes() {
var grid = document.getElementById('archGrid');
grid.innerHTML = Object.values(ARCH).map(function (a) {
return '<button class="arch-card" data-arch="' + a.id + '">' +
'<div class="arch-emoji">' + a.icon + '</div>' +
'<h3>' + a.label + '</h3>' +
'<p>' + a.blurb + '</p>' +
'<span class="arch-arrow">Continue →</span>' +
'</button>';
}).join('');
grid.querySelectorAll('.arch-card').forEach(function (card) {
card.addEventListener('click', function () {
state.archetype = card.getAttribute('data-arch');
renderHeadaches();
go(1);
});
});
}
// -------- 6. RENDER: STEP 2 (HEADACHES) ----------------------
function renderHeadaches() {
var arch = ARCH[state.archetype];
document.getElementById('headSub').textContent = 'Pick the one that costs the most time in ' + arch.label.toLowerCase() + '.';
var grid = document.getElementById('headGrid');
grid.innerHTML = arch.headaches.map(function (h, i) {
return '<button class="head-card" data-head="' + h.id + '">' +
'<div class="head-num">' + String(i + 1).padStart(2, '0') + '</div>' +
'<div><h3>' + h.title + '</h3><p>' + h.desc + '</p></div>' +
'</button>';
}).join('');
grid.querySelectorAll('.head-card').forEach(function (card) {
card.addEventListener('click', function () {
state.headache = card.getAttribute('data-head');
state.products = arch.headaches.find(function (h) { return h.id === state.headache; }).products;
renderReveal();
go(2);
});
});
}
// -------- 7. RENDER: STEP 3 (REVEAL + MINI-MOCKS) -----------
function renderReveal() {
var arch = ARCH[state.archetype];
var head = arch.headaches.find(function (h) { return h.id === state.headache; });
document.getElementById('revealH1').innerHTML = 'For "<span class="accent">' + head.title + '</span>".';
document.getElementById('revealSub').textContent = 'Here\'s the ' + state.products.length + '-product shortlist we\'d build for ' + arch.label.toLowerCase() + ' to kill "' + head.title.toLowerCase() + '".';
// price total — only include monthly items in the "monthly" total
var monthly = 0;
var onceOff = 0;
state.products.forEach(function (id) {
var p = PRODUCTS[id];
if (p.onceOff) onceOff += p.price; else monthly += p.price;
});
document.getElementById('priceTotal').textContent = 'R' + monthly.toLocaleString('en-ZA');
document.getElementById('priceCycle').textContent = onceOff > 0
? '/ month + R' + onceOff.toLocaleString('en-ZA') + ' setup'
: '/ month';
var grid = document.getElementById('revealGrid');
grid.innerHTML = state.products.map(function (id) {
var p = PRODUCTS[id];
return '<article class="reveal-card">' +
'<div class="reveal-text">' +
'<div class="kicker">' + p.num + ' · ' + p.kicker + '</div>' +
'<h2>' + p.name + '</h2>' +
'<p class="blurb">' + p.blurb + '</p>' +
'<div class="reveal-price">' +
'<span class="amt">' + p.priceRange + '</span>' +
'<span class="unit">' + p.unit + '</span>' +
'</div>' +
'<ul>' + p.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join('') + '</ul>' +
'<a class="btn-solid" href="contact.html?topic=' + p.id + '&via=sandbox">Start this →</a>' +
'</div>' +
'<div class="mock">' + buildMock(p.mockType, p.id) + '</div>' +
'</article>';
}).join('');
// kick off any in-mock animations
startMocks();
}
// -------- 8. SCENARIO DATA (per archetype × product) -----------
// Each scenario is a (archetype, product) script. The mocks pull from
// here so a Trades visitor sees a burst-pipe chat; a Content visitor
// sees a brand-inquiry chat. Same engine, different narrative.
var SCENARIOS = {
trades: {
'ai-worker': {
chatName: 'Stackless AI Worker',
chatSub: 'ONLINE · 24/7 AUTO-REPLY',
customer: 'Customer',
conversation: [
{ side: 'in', text: 'Hi, do you do emergency callouts on a Sunday?', delay: 250 },
{ side: 'typing', replyMs: 4200, delay: 1100 },
{ side: 'out', text: 'Yes — 24/7 across Cape Town. What\'s the address and what\'s happening?', delay: 600 },
{ side: 'in', text: 'Burst pipe in the kitchen, water everywhere 😬', delay: 1500 },
{ side: 'typing', replyMs: 3900, delay: 1100 },
{ side: 'out', text: 'On it. Sending someone in 20 min — you\'ll get a name and number now.', delay: 700 },
{ side: 'in', text: 'Thank you 🙏', delay: 1300 },
{ side: 'typing', replyMs: 5100, delay: 1100 },
{ side: 'out', text: 'You\'re welcome. The quote will land in your inbox after the job.', delay: 800 },
{ side: 'escalate', text: '→ escalated to David (owner) · 21:04', delay: 900 }
]
},
workflow: {
stamp: 'Live · last run 2s ago',
steps: [
{ from: 'FB', to: 'AI', label: 'New lead form' },
{ from: 'AI', to: 'AI', label: 'AI qualifies + drafts reply' },
{ from: 'AI', to: 'CRM', label: 'New contact in HubSpot' },
{ from: 'CRM', to: 'SLK', label: '#sales ping · "New lead · Cape Town"' }
]
},
'social': {
brand: '@bplumbing',
sub: 'FACEBOOK · INSTAGRAM',
caption: 'Burst pipe at 9pm Sunday — we were there in 18 min. That\'s what 24/7 actually means. 🚿🔧',
scheduled: 'Thu 18:30',
queue: ['Mon 07:30', 'Wed 18:30', 'Fri 12:00']
},
website: {
business: 'Bruce\'s Plumbing',
url: 'brucesplumbing.co.za',
tagline: '24/7 emergency callouts · Cape Town',
services: [
{ name: 'Burst pipes', meta: '20-min response' },
{ name: 'Geyser repairs', meta: 'Same day' },
{ name: 'Blocked drains', meta: 'From R450' },
{ name: 'Maintenance', meta: 'Monthly plans' }
]
},
app: {
title: 'Jobs board',
kpis: [
{ label: 'Today', val: 12, delta: '+3' },
{ label: 'In progress', val: 5, delta: '' },
{ label: 'Done', val: 7, delta: '' }
],
orders: [
{ id: '#1842', name: 'Burst pipe · Sea Point', status: 'proc', label: 'EN ROUTE' },
{ id: '#1841', name: 'Geyser replace · Claremont', status: 'ok', label: 'DONE' },
{ id: '#1840', name: 'Blocked drain · CBD', status: 'ok', label: 'DONE' },
{ id: '#1839', name: 'Quote follow-up · Wynberg', status: 'warn', label: 'PENDING' }
]
},
dashboard: {
title: 'Weekly snapshot',
rows: [
{ label: 'Revenue (wk)', val: 'R 184 200', flash: true },
{ label: 'New leads', val: '37', pill: ['ok', '+22%'] },
{ label: 'Invoices overdue', val: '4', pill: ['warn', '!'] },
{ label: 'Avg. response', val: '2.4 min' },
{ label: 'Repeat customers', val: '68%', pill: ['ok', '+4 pts'] }
]
}
},
creator: {
'ai-worker': {
chatName: 'Studio Inbox Bot',
chatSub: 'ONLINE · Triage &amp; reply',
customer: 'DM',
conversation: [
{ side: 'in', text: 'Hey! Quick q — what are your rates for a 60s brand video?', delay: 300 },
{ side: 'typing', replyMs: 3800, delay: 1200 },
{ side: 'out', text: 'Hey 👋 depends on scope. Are you thinking a single shoot day, or with editing + cutdowns?', delay: 700 },
{ side: 'in', text: 'Just a single day, edited. Budget around R15k — does that work?', delay: 1600 },
{ side: 'typing', replyMs: 4400, delay: 1100 },
{ side: 'out', text: 'It does. I\'ve got a half-day in Cape Town next Thursday — want me to send a one-pager with samples?', delay: 800 },
{ side: 'in', text: 'Yes please 🙏', delay: 1300 },
{ side: 'typing', replyMs: 5200, delay: 1000 },
{ side: 'out', text: 'Sent. Calendar link is at the bottom if you want to lock 15 min to chat through it.', delay: 700 },
{ side: 'escalate', text: '→ qualified · calendar booked · 14:02', delay: 900 }
]
},
workflow: {
stamp: 'Live · last run 9s ago',
steps: [
{ from: 'IG', to: 'AI', label: 'New DM' },
{ from: 'AI', to: 'CRM', label: 'Lead scored: warm' },
{ from: 'CRM', to: 'AI', label: 'AI drafts intro' },
{ from: 'AI', to: 'NOT', label: 'Email + calendar link sent' }
]
},
'social': {
brand: '@thelocalframe',
sub: 'INSTAGRAM · TWITTER',
caption: 'Three things I learned shooting at 5am for a year. (No. 3 still surprises me.)',
scheduled: 'Tue 07:00',
queue: ['Mon 18:30', 'Wed 12:00', 'Fri 07:00']
},
website: {
business: 'The Local Frame',
url: 'thelocalframe.co',
tagline: 'Brand video for founders who hate being on camera',
services: [
{ name: 'Brand videos', meta: 'from R12k' },
{ name: 'Shoot days', meta: 'Half / full' },
{ name: 'Editing', meta: '48h turnaround' },
{ name: 'Strategy', meta: '1h consult' }
]
},
app: {
title: 'Content pipeline',
kpis: [
{ label: 'Drafts', val: 8, delta: '+2' },
{ label: 'Scheduled', val: 14, delta: '' },
{ label: 'Posted', val: 6, delta: '+6' }
],
orders: [
{ id: 'IG-184', name: '"5am shoot" reel', status: 'proc', label: 'SCHEDULED' },
{ id: 'IG-183', name: 'Client BTS · Coffee Co', status: 'ok', label: 'POSTED' },
{ id: 'TW-079', name: 'Q&amp;A thread', status: 'warn', label: 'DRAFT' },
{ id: 'IG-182', name: 'Studio tour · 3 photos', status: 'ok', label: 'POSTED' }
]
},
dashboard: {
title: 'Audience snapshot',
rows: [
{ label: 'Followers (30d)', val: '+1 240', flash: true },
{ label: 'Engagement rate', val: '6.8%', pill: ['ok', '+0.4'] },
{ label: 'Top post (wk)', val: '"5am shoot" · 18k' },
{ label: 'Inbound DMs', val: '94', pill: ['warn', '!'] },
{ label: 'Waitlist signups', val: '38' }
]
}
},
professional: {
'ai-worker': {
chatName: 'Engagement Intake',
chatSub: 'ONLINE · Lead qualification',
customer: 'Website visitor',
conversation: [
{ side: 'in', text: 'Hi — we\'re a 12-person SaaS, looking at a fractional CFO. Where do we start?', delay: 300 },
{ side: 'typing', replyMs: 4600, delay: 1200 },
{ side: 'out', text: 'Hi 👋 — quick two questions: monthly revenue band, and is this for fundraising prep or ops?', delay: 700 },
{ side: 'in', text: 'R3–5m/month, ops side. Not raising right now.', delay: 1700 },
{ side: 'typing', replyMs: 4800, delay: 1100 },
{ side: 'out', text: 'Got it. I do a 60-min scoping call before any proposal — book here: [link]. You\'ll get a one-pager 24h after.', delay: 800 },
{ side: 'in', text: 'Booked for Thursday. Thanks!', delay: 1300 },
{ side: 'typing', replyMs: 5500, delay: 1000 },
{ side: 'out', text: 'See you Thursday. I\'ll send a short brief tomorrow so we can skip the warm-up.', delay: 700 },
{ side: 'escalate', text: '→ qualified · call booked · 11:08', delay: 900 }
]
},
workflow: {
stamp: 'Live · last run 14s ago',
steps: [
{ from: 'WEB', to: 'AI', label: 'New contact form' },
{ from: 'AI', to: 'XLS', label: 'Logged to CRM' },
{ from: 'XLS', to: 'DOC', label: 'Engagement letter drafted' },
{ from: 'DOC', to: 'MAIL', label: 'Sent for signature' }
]
},
'social': {
brand: '@brightline.legal',
sub: 'LINKEDIN · TWITTER',
caption: 'Three clauses founders always forget in their first employment contract. (Number 2 cost a client R180k.)',
scheduled: 'Wed 09:00',
queue: ['Mon 09:00', 'Wed 14:00', 'Fri 09:00']
},
website: {
business: 'Brightline Legal',
url: 'brightline.legal',
tagline: 'Commercial law for SA founders, R1.2k/month retainer',
services: [
{ name: 'Fractional counsel', meta: 'from R1.2k/mo' },
{ name: 'Fundraising', meta: 'SA + cross-border' },
{ name: 'Employment', meta: 'Templates + review' },
{ name: 'Compliance', meta: 'Quarterly check-in' }
]
},
app: {
title: 'Matters board',
kpis: [
{ label: 'Active', val: 23, delta: '+2' },
{ label: 'On hold', val: 4, delta: '' },
{ label: 'Invoiced', val: 11, delta: '' }
],
orders: [
{ id: 'M-2294', name: 'Series A · TechCo', status: 'proc', label: 'IN REVIEW' },
{ id: 'M-2291', name: 'Employment contract · Acme', status: 'ok', label: 'SIGNED' },
{ id: 'M-2289', name: 'Vendor MSA · Globex', status: 'warn', label: 'AWAITING' },
{ id: 'M-2286', name: 'Trademark · BrandCo', status: 'ok', label: 'FILED' }
]
},
dashboard: {
title: 'Practice snapshot',
rows: [
{ label: 'Billable (wk)', val: 'R 92 400', flash: true },
{ label: 'New matters', val: '6', pill: ['ok', '+2'] },
{ label: 'Invoices overdue', val: '3', pill: ['warn', '!'] },
{ label: 'Utilization', val: '74%' },
{ label: 'Realisation', val: '88%' }
]
}
}
};
// generic fallback (when a combo has no bespoke data) — uses trades
function scenarioFor(productId) {
return (SCENARIOS[state.archetype] && SCENARIOS[state.archetype][productId])
|| SCENARIOS.trades[productId]
|| null;
}
// -------- 9. MOCK BUILDERS + INTERACTIVITY -------------------
// timers for cycling effects — start/stop on reveal
var mockTimers = [];
function clearMockTimers() {
mockTimers.forEach(function (id) { clearInterval(id); clearTimeout(id); });
mockTimers = [];
}
function buildMock(type, productId) {
var s = scenarioFor(productId);
switch (type) {
case 'phone': return buildPhone(s);
case 'flow': return buildFlow(s);
case 'ig': return buildIG(s);
case 'tile-website':return buildWebsite(s);
case 'tile-app': return buildApp(s);
case 'tile-dashboard': return buildDashboard(s);
default: return '';
}
}
// -- phone (AI Worker) --------------------------------------
function buildPhone(s) {
return '<div class="phone">' +
'<div class="phone__stamp">● LIVE</div>' +
'<div class="phone__notch"></div>' +
'<div class="phone__bar">' +
'<div class="av">SA</div>' +
'<div><div class="name">' + s.chatName + '</div><div class="sub">' + s.chatSub + '</div></div>' +
'</div>' +
'<div class="phone__chat" id="phoneChat"></div>' +
'</div>';
}
// -- workflow (Workflow Automation) --------------------------
function buildFlow(s) {
return '<div class="flow">' +
'<div class="flow__stamp"><span>WORKFLOW</span><strong>' + s.stamp + '</strong></div>' +
// 3 rows of node → arrow → node
'<div class="flow__row" data-row="0">' +
node(s.steps[0].from, 'lead', labelFor(s.steps[0].from)) +
arrow() +
node(s.steps[0].to, nodeKind(s.steps[0].to), labelFor(s.steps[0].to), 'step', 0) +
'</div>' +
'<div class="flow__row" data-row="1">' +
node(s.steps[1].from, nodeKind(s.steps[1].from), labelFor(s.steps[1].from), 'step', 0, 'done') +
arrow() +
node(s.steps[1].to, nodeKind(s.steps[1].to), labelFor(s.steps[1].to), 'step', 1) +
'</div>' +
'<div class="flow__row" data-row="2">' +
node(s.steps[2].from, nodeKind(s.steps[2].from), labelFor(s.steps[2].from), 'step', 1, 'done') +
arrow() +
node(s.steps[2].to, nodeKind(s.steps[2].to), labelFor(s.steps[2].to), 'step', 2) +
'</div>' +
'<div class="flow__row" data-row="3">' +
node(s.steps[3].from, nodeKind(s.steps[3].from), labelFor(s.steps[3].from), 'step', 2, 'done') +
arrow() +
node(s.steps[3].to, nodeKind(s.steps[3].to), labelFor(s.steps[3].to), 'step', 3, 'done') +
'</div>' +
'<div class="flow__pulse" id="flowPulse"></div>' +
'<div class="flow__status"><span>STATUS</span><strong id="flowStatus">Idle · waiting for trigger</strong></div>' +
'</div>';
}
function node(tag, kind, label, role, step, initial) {
var cls = 'flow__node flow__node--' + kind + (initial ? ' is-' + initial : '');
return '<div class="' + cls + '" data-step="' + (step != null ? step : '') + '">' +
'<div class="ico">' + tag + '</div>' +
'<div>' + label + '</div>' +
'<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></div>' +
'</div>';
}
function arrow() {
return '<div class="flow__arrow" data-arrow><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></div>';
}
function labelFor(tag) {
var map = {
'FB': 'New lead form', 'IG': 'New DM', 'WEB': 'Website form',
'AI': 'AI Worker', 'CRM': 'HubSpot', 'XLS': 'CRM row',
'SLK': 'Slack #sales', 'NOT': 'Email sent', 'DOC': 'Engagement letter',
'MAIL': 'Mail · awaiting signature'
};
return '<div>' + (map[tag] || tag) + '</div>';
}
function nodeKind(tag) {
if (tag === 'FB' || tag === 'IG' || tag === 'WEB') return 'lead';
if (tag === 'CRM' || tag === 'XLS') return 'crm';
if (tag === 'SLK' || tag === 'NOT' || tag === 'MAIL') return 'slack';
return 'email';
}
// -- social (Social Auto-Post) ------------------------------
function buildIG(s) {
var q = s.queue.map(function (t, i) {
return '<div class="ig__queue-item' + (i === 0 ? ' is-on' : '') + '">' + t + '</div>';
}).join('');
return '<div class="ig">' +
'<div class="ig__head">' +
'<div class="ig__av">' + s.brand.charAt(1).toUpperCase() + '</div>' +
'<div><div class="ig__name">' + s.brand + '</div><div class="ig__sub">' + s.sub + '</div></div>' +
'<div class="ig__status"><span class="dot"></span> AI drafting</div>' +
'</div>' +
'<div class="ig__composer">' +
'<div class="ig__composer-label" id="igLabel">AI WRITING · 0%</div>' +
'<div class="ig__caption" id="igCaption"></div>' +
'</div>' +
'<div class="ig__image"><div class="stamp" id="igStamp">Auto-post · #3 of 12</div><div class="watermark">@' + s.brand.substring(1) + '</div></div>' +
'<div class="ig__queue">' + q + '</div>' +
'</div>';
}
// -- website (Starter Site) ---------------------------------
function buildWebsite(s) {
var svcs = s.services.map(function (sv) {
return '<div class="site__service"><strong>' + sv.name + '</strong><span>' + sv.meta + '</span></div>';
}).join('');
return '<div class="site">' +
'<div class="site__chrome"><span></span><span></span><span></span><div class="site__url">' + s.url + '</div></div>' +
'<div class="site__nav">' +
'<button class="site__tab is-on" data-tab="hero">Hero</button>' +
'<button class="site__tab" data-tab="services">Services</button>' +
'<button class="site__tab" data-tab="contact">Contact</button>' +
'</div>' +
'<div class="site__view site__view--hero is-on" data-view="hero">' +
'<h5>' + s.business + '</h5>' +
'<p>' + s.tagline + '</p>' +
'<button class="site__cta">Get a quote →</button>' +
'</div>' +
'<div class="site__view site__view--services" data-view="services" style="display:none;">' + svcs + '</div>' +
'<div class="site__view site__view--contact" data-view="contact" style="display:none;">' +
'<label>Name</label><input type="text" placeholder="Jane" />' +
'<label>Phone</label><input type="text" placeholder="082 000 0000" />' +
'<label>How can we help?</label><textarea placeholder="A few words…"></textarea>' +
'<button class="site__cta">Send message →</button>' +
'</div>' +
'</div>';
}
// -- app (Internal app) -------------------------------------
function buildApp(s) {
var k = s.kpis.map(function (kpi) {
return '<div class="ops__kpi">' +
'<div class="ops__kpi-label">' + kpi.label + '</div>' +
'<div class="ops__kpi-val" data-target="' + kpi.val + '">0</div>' +
(kpi.delta ? '<div class="ops__kpi-delta">' + kpi.delta + '</div>' : '') +
'</div>';
}).join('');
var o = s.orders.map(function (o) {
return '<div class="ops__row">' +
'<span class="id">' + o.id + '</span>' +
'<span class="name">' + o.name + '</span>' +
'<span class="pill pill--' + o.status + '">' + o.label + '</span>' +
'</div>';
}).join('');
return '<div class="ops">' +
'<div class="ops__chrome"><span></span><span></span><span></span><span class="ops__title">' + s.title + '</span></div>' +
'<div class="ops__view">' +
'<div class="ops__kpis">' + k + '</div>' +
'<div class="ops__chart" id="opsChart">' +
'<span style="height:35%"></span><span style="height:55%"></span><span style="height:42%"></span>' +
'<span style="height:68%"></span><span style="height:58%"></span><span style="height:78%"></span>' +
'<span style="height:64%"></span><span style="height:88%"></span><span style="height:72%"></span>' +
'<span style="height:95%"></span><span style="height:80%"></span><span style="height:60%"></span>' +
'</div>' +
'<div class="ops__list-head"><span>Recent</span><span id="opsListCount">Live · 4</span></div>' +
'<div class="ops__list" id="opsList">' + o + '</div>' +
'</div>' +
'</div>';
}
// -- dashboard (Dashboard) ----------------------------------
function buildDashboard(s) {
var r = s.rows.map(function (row, i) {
var pill = row.pill ? '<span class="pill pill--' + row.pill[0] + '">' + row.pill[1] + '</span>' : '';
return '<div class="kpi__row" data-idx="' + i + '">' +
'<span class="label">' + row.label + '</span>' +
'<span class="right">' + pill +
'<span class="val" data-val="' + row.val + '"' + (row.flash ? ' data-flash' : '') + '>' + row.val + '</span>' +
'</span>' +
'</div>';
}).join('');
return '<div class="kpi">' +
'<div class="kpi__chrome"><span></span><span></span><span></span><span class="kpi__title">' + s.title + '</span></div>' +
'<div class="kpi__view">' +
'<div class="kpi__head">' +
'<h6>This week</h6>' +
'<div class="kpi__refresh" id="kpiRefresh"><span class="dot"></span> <span id="kpiRefreshText">refreshed 2s ago</span></div>' +
'</div>' +
r +
'</div>' +
'</div>';
}
// -------- 10. MOCK ANIMATIONS -------------------------------
function startMocks() {
clearMockTimers();
// run an animation for every mock present on screen
if (document.getElementById('phoneChat')) startPhone();
if (document.getElementById('flowPulse')) startFlow();
if (document.getElementById('igCaption')) startIG();
document.querySelectorAll('.site__tab').forEach(function (t) {
t.addEventListener('click', function () { siteTab(t.getAttribute('data-tab')); });
});
if (document.getElementById('opsList')) startApp();
if (document.getElementById('kpiRefresh')) startDashboard();
}
// -- phone: scripted chat (per archetype) --------------------
function startPhone() {
var chat = document.getElementById('phoneChat');
chat.innerHTML = '';
var s = scenarioFor('ai-worker');
var t = 0;
s.conversation.forEach(function (line) {
t += line.delay;
var id = setTimeout(function () {
if (line.side === 'typing') {
var d = document.createElement('div');
d.className = 'bub bub--in bub--typing';
d.innerHTML = '<span></span><span></span><span></span>';
chat.appendChild(d);
chat.scrollTop = chat.scrollHeight;
} else if (line.side === 'escalate') {
var d = document.createElement('div');
d.className = 'phone__escalate';
d.innerHTML = '<span class="arr">→</span> ' + line.text.replace('→ ', '');
chat.appendChild(d);
chat.scrollTop = chat.scrollHeight;
} else {
var d = document.createElement('div');
d.className = 'bub bub--' + line.side;
d.innerHTML = line.text;
// add a small reply-time pill to the first AI reply
if (line.side === 'out' && line.replyMs) {
// already used; pill added separately below
}
chat.appendChild(d);
chat.scrollTop = chat.scrollHeight;
}
}, t);
mockTimers.push(id);
});
// add a "replied in 4 sec" badge after the first AI reply
var firstReplyAt = 0;
s.conversation.forEach(function (line, i) {
if (line.side === 'out' && !firstReplyAt) firstReplyAt = i;
});
// append badge after the first AI out
var t2 = 0;
var seen = false;
s.conversation.forEach(function (line, i) {
t2 += line.delay;
if (!seen && line.side === 'out') {
seen = true;
var id = setTimeout(function () {
var badge = document.createElement('div');
badge.className = 'phone__badge';
badge.textContent = '↑ replied in 4.2 sec';
chat.insertBefore(badge, chat.lastChild);
chat.scrollTop = chat.scrollHeight;
}, t2 + 200);
mockTimers.push(id);
}
});
}
// -- workflow: pulse travels through nodes -------------------
function startFlow() {
var nodes = document.querySelectorAll('.flow__node');
var arrows = document.querySelectorAll('.flow__arrow');
var status = document.getElementById('flowStatus');
var pulse = document.getElementById('flowPulse');
var rows = document.querySelectorAll('.flow__row');
if (!pulse || !rows.length) return;
// size the pulse to a node
function positionPulse(idx) {
var row = rows[idx];
if (!row) return;
var rect = row.getBoundingClientRect();
var parentRect = pulse.parentElement.getBoundingClientRect();
pulse.style.top = (rect.top - parentRect.top + rect.height / 2 - 5) + 'px';
pulse.style.left = (rect.right - parentRect.left - 10) + 'px';
}
var step = 0;
function tick() {
// reset
nodes.forEach(function (n) { n.classList.remove('is-active', 'is-done'); });
arrows.forEach(function (a) { a.classList.remove('is-active'); });
if (status) status.textContent = 'Idle · waiting for trigger';
var sequence = [
{ delay: 0, row: 0, action: 'active', status: 'Trigger received · starting run' },
{ delay: 900, row: 1, action: 'next', status: 'AI Worker qualifying lead' },
{ delay: 1800, row: 2, action: 'next', status: 'Logged to CRM' },
{ delay: 2700, row: 3, action: 'next', status: 'Slack notified · run complete' },
{ delay: 3800, row: -1, action: 'done', status: 'Run complete · waiting 4 sec for next trigger' }
];
sequence.forEach(function (s) {
var id = setTimeout(function () {
if (s.action === 'active') {
var n0 = rows[0] && rows[0].querySelectorAll('.flow__node')[0];
if (n0) n0.classList.add('is-active');
positionPulse(0);
pulse.classList.add('is-on');
arrows[0] && arrows[0].classList.add('is-active');
} else if (s.action === 'next') {
// mark previous row's second node done
var prevRow = rows[s.row - 1];
if (prevRow) {
prevRow.querySelectorAll('.flow__node').forEach(function (n) { n.classList.remove('is-active'); n.classList.add('is-done'); });
}
if (arrows[s.row - 1]) arrows[s.row - 1].classList.remove('is-active');
// activate this row
var curRow = rows[s.row];
if (curRow) {
var n = curRow.querySelectorAll('.flow__node');
if (n[0]) n[0].classList.add('is-done');
if (n[1]) n[1].classList.add('is-active');
}
if (arrows[s.row]) arrows[s.row].classList.add('is-active');
positionPulse(s.row);
} else if (s.action === 'done') {
rows.forEach(function (r) {
r.querySelectorAll('.flow__node').forEach(function (n) { n.classList.remove('is-active'); n.classList.add('is-done'); });
});
arrows.forEach(function (a) { a.classList.remove('is-active'); });
pulse.classList.remove('is-on');
}
if (status) status.textContent = s.status;
}, s.delay);
mockTimers.push(id);
});
}
tick();
mockTimers.push(setInterval(tick, 6500));
}
// -- social: typing caption → scheduled ----------------------
function startIG() {
var s = scenarioFor('social');
var captionEl = document.getElementById('igCaption');
var labelEl = document.getElementById('igLabel');
var stampEl = document.getElementById('igStamp');
var queueItems = document.querySelectorAll('.ig__queue-item');
function runOnce() {
captionEl.innerHTML = '';
labelEl.textContent = 'AI WRITING · 0%';
queueItems.forEach(function (q, i) { q.classList.toggle('is-on', i === 0); });
var text = s.caption;
var i = 0;
var total = text.length;
function typeChar() {
if (i >= total) {
// schedule
var id1 = setTimeout(function () {
labelEl.textContent = '✓ SCHEDULED · ' + s.scheduled;
stampEl.textContent = 'Goes live ' + s.scheduled;
}, 400);
// rotate queue
var id2 = setTimeout(function () {
queueItems.forEach(function (q, idx) { q.classList.toggle('is-on', idx === 1); });
}, 1500);
mockTimers.push(id1, id2);
return;
}
var ch = text.charAt(i);
captionEl.innerHTML = text.substring(0, i + 1) + '<span class="cursor"></span>';
labelEl.textContent = 'AI WRITING · ' + Math.round((i + 1) / total * 100) + '%';
i++;
var speed = ch === ' ' ? 30 : (Math.random() < 0.05 ? 140 : 18);
var id = setTimeout(typeChar, speed);
mockTimers.push(id);
}
typeChar();
}
runOnce();
mockTimers.push(setInterval(runOnce, 9000));
}
// -- website: tab interaction -------------------------------
function siteTab(name) {
document.querySelectorAll('.site__tab').forEach(function (t) { t.classList.toggle('is-on', t.getAttribute('data-tab') === name); });
document.querySelectorAll('.site__view').forEach(function (v) { v.style.display = v.getAttribute('data-view') === name ? 'block' : 'none'; });
}
// -- app: count up KPIs, scroll new orders, animate chart ----
function startApp() {
// count up
document.querySelectorAll('.ops__kpi-val').forEach(function (el) {
var target = parseInt(el.getAttribute('data-target'), 10) || 0;
var cur = 0;
var step = Math.max(1, Math.ceil(target / 30));
function tick() {
cur = Math.min(target, cur + step);
el.textContent = cur;
if (cur < target) {
var id = setTimeout(tick, 35);
mockTimers.push(id);
}
}
tick();
});
// animate chart bars
var chart = document.getElementById('opsChart');
if (chart) {
var bars = chart.querySelectorAll('span');
mockTimers.push(setInterval(function () {
bars.forEach(function (b) {
var h = 30 + Math.random() * 65;
b.style.height = h + '%';
});
}, 2000));
}
// add a new order every 4s, max 6
var list = document.getElementById('opsList');
var countEl = document.getElementById('opsListCount');
var templates = [
{ id: '#1852', name: 'New enquiry · Lakeside', status: 'proc', label: 'NEW' },
{ id: '#1851', name: 'Quote accepted · Mowbray',status: 'ok', label: 'PAID' },
{ id: '#1850', name: 'Callback · Gardens', status: 'warn', label: 'PENDING' },
{ id: '#1849', name: 'Invoice paid · R1 240', status: 'ok', label: 'PAID' }
];
var ti = 0;
function addOne() {
if (!list) return;
var t = templates[ti % templates.length];
ti++;
// cap at 6 rows visible
while (list.children.length >= 5) list.removeChild(list.firstChild);
var row = document.createElement('div');
row.className = 'ops__row';
row.innerHTML = '<span class="id">' + t.id + '</span><span class="name">' + t.name + '</span><span class="pill pill--' + t.status + '">' + t.label + '</span>';
list.insertBefore(row, list.firstChild);
if (countEl) countEl.textContent = 'Live · ' + list.children.length;
}
mockTimers.push(setInterval(addOne, 4000));
}
// -- dashboard: auto-refresh with flash ---------------------
function startDashboard() {
var refresh = document.getElementById('kpiRefresh');
var refreshText = document.getElementById('kpiRefreshText');
var count = 2;
function tick() {
// flash a random row
var rows = document.querySelectorAll('.kpi__row .val');
if (rows.length) {
var pick = rows[Math.floor(Math.random() * rows.length)];
pick.classList.add('is-flash');
// reformat value slightly to look "updated"
var raw = pick.getAttribute('data-val');
// simple wiggle: +1 to numeric, random percent bump on % values
if (/^\d[\d\s]*$/.test(raw.replace(/\s/g, ''))) {
var n = parseInt(raw.replace(/\s/g, ''), 10);
n = n + (Math.random() < 0.5 ? 1 : -1);
pick.textContent = n.toLocaleString('en-ZA');
pick.setAttribute('data-val', String(n));
} else if (raw.indexOf('min') > -1) {
pick.textContent = (1.8 + Math.random() * 1.4).toFixed(1) + ' min';
} else if (raw.indexOf('%') > -1 && raw.length < 6) {
var p = parseFloat(raw);
pick.textContent = Math.max(50, Math.min(95, p + (Math.random() * 4 - 2))).toFixed(0) + '%';
} else if (raw.charAt(0) === 'R') {
var rn = parseInt(raw.replace(/[^\d]/g, ''), 10);
rn = rn + Math.floor(Math.random() * 8000 - 4000);
pick.textContent = 'R ' + rn.toLocaleString('en-ZA');
} else if (raw.charAt(0) === '+') {
pick.textContent = '+' + (parseInt(raw.replace(/[^\d]/g, ''), 10) + Math.floor(Math.random() * 80 - 40)).toLocaleString('en-ZA');
}
setTimeout(function () { pick.classList.remove('is-flash'); }, 800);
}
// refresh ticker
if (refresh) refresh.classList.add('is-flash');
if (refreshText) refreshText.textContent = 'refreshing…';
setTimeout(function () {
if (refresh) refresh.classList.remove('is-flash');
if (refreshText) refreshText.textContent = 'refreshed just now';
}, 700);
}
mockTimers.push(setInterval(tick, 5000));
}
// -------- 10. EMAIL GATE + SHARE URL -------------------------
var gate = document.getElementById('gateBackdrop');
document.getElementById('openGate').addEventListener('click', function () { gate.classList.add('is-open'); });
document.getElementById('gateClose').addEventListener('click', function () { gate.classList.remove('is-open'); });
gate.addEventListener('click', function (e) { if (e.target === gate) gate.classList.remove('is-open'); });
function buildShareUrl() {
var payload = {
a: state.archetype,
h: state.headache,
p: state.products,
t: Date.now()
};
var encoded = btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
var base = location.origin + location.pathname.replace(/\/[^/]*$/, '/') + 'sandbox.html';
return base + '#c=' + encoded;
}
function readHashState() {
var m = location.hash.match(/#c=([^&]+)/);
if (!m) return null;
try {
return JSON.parse(decodeURIComponent(escape(atob(m[1]))));
} catch (_) { return null; }
}
document.getElementById('gateSubmit').addEventListener('click', function () {
var name = document.getElementById('gateName');
var email = document.getElementById('gateEmail');
var err = document.getElementById('gateErr');
var ok = name.value.trim().length >= 1 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
name.classList.toggle('is-invalid', !ok);
email.classList.toggle('is-invalid', !ok);
if (!ok) { err.classList.add('is-on'); return; }
err.classList.remove('is-on');
var url = buildShareUrl();
// best-effort capture — fire-and-forget; the user still gets the link
// if the request fails. Replace the webhook URL once one is wired.
try {
fetch('https://n8nmac.apphub.co.za/webhook/34f4684f-a603-4207-a8a9-75746a345463', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chatInput: 'SANDBOX LEAD: ' + name.value + ' (' + email.value + ') — ' +
state.archetype + '/' + state.headache + ' → ' + state.products.join(', '),
sessionId: 'sandbox-' + Date.now(),
message: 'Sandbox lead from /sandbox.html',
name: name.value,
email: email.value,
archetype: state.archetype,
headache: state.headache,
products: state.products,
shareUrl: url
})
}).catch(function () {});
} catch (_) {}
// persist locally so the user can come back without re-clicking the link
try { localStorage.setItem('stackless:sandbox:last', url); } catch (_) {}
// show success state
document.getElementById('gateForm').style.display = 'none';
document.getElementById('gateSuccess').classList.add('is-on');
document.getElementById('gateLinkText').textContent = url;
document.getElementById('gateOpen').addEventListener('click', function () {
go(3);
document.getElementById('finalLink').href = url;
gate.classList.remove('is-open');
});
});
// -------- 11. BOOT (handle shared-link re-entry) ------------
function boot() {
var shared = readHashState();
if (shared && ARCH[shared.a]) {
state.archetype = shared.a;
if (shared.h && ARCH[shared.a].headaches.find(function (h) { return h.id === shared.h; })) {
state.headache = shared.h;
state.products = shared.p || ARCH[shared.a].headaches.find(function (h) { return h.id === shared.h; }).products;
renderHeadaches();
renderReveal();
go(2);
return;
}
}
renderArchetypes();
go(0);
}
boot();
})();
</script>
<script src="chat-widget.js" defer></script>
</body>
</html>