Stackless-Website/sandbox.html

2516 lines
95 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, 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>