2517 lines
95 KiB
HTML
2517 lines
95 KiB
HTML
|
|
<!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 & 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 & 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 & 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 & 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 & 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 & 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 & deploy in 5–7 days',
|
|||
|
|
'Hosting, backups & 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 & 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&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>
|