Video — Booking
A cinematic five-step booking flow for a video production studio: pick a project type, choose a shoot date from an interactive calendar, select a coverage package with add-ons, fill in a project brief, then review and confirm. A sticky progress rail tracks each stage, letterbox amber accents and JetBrains Mono timecodes set the film-forward mood, and a final confirmation screen returns a booking reference with a running cost summary that updates live as choices change.
MCP
Code
:root{
--bg:#0a0a0b; --surface:#141416; --surface-2:#1c1c1f;
--amber:#ffb020; --amber-d:#e6971a; --red:#ff4d4d;
--ink:#f4f4f6; --muted:#8a8a92;
--line:rgba(255,255,255,0.10); --line-2:rgba(255,255,255,0.18); --white:#fff;
--r-sm:6px; --r-md:10px; --r-lg:16px;
--mono:"JetBrains Mono",ui-monospace,monospace;
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0; background:var(--bg); color:var(--ink);
font-family:Inter,system-ui,sans-serif; line-height:1.5;
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
h1,h2{margin:0; letter-spacing:-.02em}
button{font-family:inherit}
:focus-visible{outline:2px solid var(--amber); outline-offset:2px; border-radius:4px}
.stage{max-width:1120px; margin:0 auto; padding:0 20px 72px}
/* ---------- HERO ---------- */
.hero{position:relative; margin:0 -20px 34px; overflow:hidden}
.bar{height:26px; background:#000}
.hero-inner{
background:
radial-gradient(120% 140% at 78% -10%, rgba(255,176,32,.20), transparent 55%),
linear-gradient(180deg,#111 0%, #0c0c0d 100%);
padding:44px 30px 40px; text-align:left;
}
.brand{display:flex; align-items:center; gap:9px; margin-bottom:22px}
.rec{width:11px; height:11px; border-radius:50%; background:var(--red); box-shadow:0 0 0 4px rgba(255,77,77,.18); animation:rec 1.6s ease-in-out infinite}
.rec.sm{width:8px; height:8px; box-shadow:0 0 0 3px rgba(255,77,77,.18)}
@keyframes rec{0%,100%{opacity:1}50%{opacity:.35}}
.brand-name{font-weight:800; letter-spacing:.26em; font-size:12.5px; color:var(--muted)}
.hero-title{font-size:clamp(30px,6vw,52px); font-weight:800}
.hero-sub{margin:12px 0 0; color:var(--muted); max-width:46ch; font-size:15px}
.hero-meta{display:flex; align-items:center; gap:12px; margin-top:20px; font-size:13px; color:var(--muted)}
.tc{font-family:var(--mono); color:var(--amber); letter-spacing:.04em}
.dot{opacity:.5}
/* ---------- LAYOUT ---------- */
.wrap{
display:grid;
grid-template-columns:200px minmax(0,1fr) 260px;
gap:26px; align-items:start;
}
/* ---------- RAIL ---------- */
.rail{position:sticky; top:20px}
.steps{list-style:none; margin:0; padding:0; display:grid; gap:4px}
.step{display:flex; gap:12px; align-items:center; padding:11px 12px; border-radius:var(--r-md); color:var(--muted); position:relative}
.step .idx{
flex:none; width:26px; height:26px; border-radius:50%;
display:grid; place-items:center; font-size:12.5px; font-weight:700;
background:var(--surface-2); border:1px solid var(--line); color:var(--muted);
transition:.2s;
}
.step .lbl{display:flex; flex-direction:column; line-height:1.25}
.step .lbl b{font-size:13.5px; color:var(--ink); font-weight:600}
.step .lbl small{font-size:11px; color:var(--muted)}
.step.is-active{background:var(--surface)}
.step.is-active .idx{background:var(--amber); border-color:var(--amber); color:#111}
.step.is-active .lbl b{color:var(--amber)}
.step.is-done .idx{background:transparent; border-color:var(--amber); color:var(--amber)}
.step.is-done .idx::after{content:"✓"}
.step.is-done .idx{font-size:0}
.step.is-done .idx::after{font-size:13px}
/* ---------- PANELS ---------- */
.panelwrap{background:var(--surface); border:1px solid var(--line); border-radius:var(--r-lg); padding:24px 24px 18px; min-height:400px}
.panel{border:0; margin:0; padding:0; min-inline-size:0}
.panel[hidden]{display:none}
.panel.is-active{animation:panelIn .32s ease both}
@keyframes panelIn{from{opacity:0; transform:translateY(8px)}to{opacity:1; transform:none}}
.panel-h{padding:0; display:block; margin-bottom:20px}
.eyebrow{font-family:var(--mono); font-size:11px; letter-spacing:.14em; color:var(--amber); text-transform:uppercase}
.panel-h h2{font-size:22px; margin-top:6px}
/* ---------- STEP 1 : TYPE CARDS ---------- */
.grid{display:grid; gap:12px}
.grid-type{grid-template-columns:1fr 1fr}
.typecard,.pkg{cursor:pointer; position:relative}
.typecard input,.pkg input,.addon input,.terms input{position:absolute; opacity:0; width:1px; height:1px}
.tc-face{
display:flex; flex-direction:column; gap:4px;
padding:16px; border:1px solid var(--line); border-radius:var(--r-md);
background:var(--surface-2); transition:.18s; height:100%;
}
.tc-emoji{font-size:22px; margin-bottom:4px}
.tc-face b{font-size:15px}
.tc-face small{color:var(--muted); font-size:12.5px}
.tc-price{margin-top:8px; font-family:var(--mono); font-size:12px; color:var(--amber)}
.typecard:hover .tc-face{transform:translateY(-3px); border-color:var(--line-2)}
.typecard input:focus-visible + .tc-face{outline:2px solid var(--amber); outline-offset:2px}
.typecard input:checked + .tc-face{
border-color:var(--amber); background:linear-gradient(180deg,rgba(255,176,32,.10),rgba(255,176,32,.02));
box-shadow:0 0 0 1px var(--amber) inset;
}
/* ---------- STEP 2 : CALENDAR ---------- */
.cal{max-width:400px}
.cal-top{display:flex; align-items:center; justify-content:space-between; margin-bottom:14px}
.cal-month{font-weight:700; font-size:15px}
.cal-nav{
width:34px; height:34px; border-radius:var(--r-sm); background:var(--surface-2);
border:1px solid var(--line); color:var(--ink); font-size:18px; cursor:pointer; transition:.15s; line-height:1;
}
.cal-nav:hover{border-color:var(--amber); color:var(--amber)}
.cal-nav:disabled{opacity:.3; cursor:not-allowed}
.cal-dow{display:grid; grid-template-columns:repeat(7,1fr); gap:6px; margin-bottom:6px}
.cal-dow span{text-align:center; font-size:11px; color:var(--muted); font-weight:600}
.cal-grid{display:grid; grid-template-columns:repeat(7,1fr); gap:6px}
.cal-cell{
aspect-ratio:1; border:1px solid var(--line); border-radius:var(--r-sm);
background:var(--surface-2); color:var(--ink); font-size:13px; cursor:pointer;
font-family:var(--mono); transition:.14s; display:grid; place-items:center;
}
.cal-cell:hover:not(:disabled){border-color:var(--amber); color:var(--amber)}
.cal-cell.empty{background:transparent; border-color:transparent; cursor:default}
.cal-cell:disabled{color:var(--muted); opacity:.4; cursor:not-allowed; text-decoration:line-through}
.cal-cell.blocked{color:var(--muted); background:repeating-linear-gradient(45deg,var(--surface-2),var(--surface-2) 4px,rgba(255,77,77,.10) 4px,rgba(255,77,77,.10) 8px); cursor:not-allowed}
.cal-cell.sel{background:var(--amber); border-color:var(--amber); color:#111; font-weight:700}
.cal-legend{display:flex; align-items:center; gap:8px; font-size:11.5px; color:var(--muted); margin:14px 0 0}
.lg{width:12px; height:12px; border-radius:3px; display:inline-block; margin-left:10px}
.lg-free{background:var(--surface-2); border:1px solid var(--line-2)}
.lg-block{background:repeating-linear-gradient(45deg,var(--surface-2),var(--surface-2) 3px,rgba(255,77,77,.30) 3px,rgba(255,77,77,.30) 6px)}
.cal-legend .lg:first-child{margin-left:0}
/* ---------- STEP 3 : PACKAGES ---------- */
.grid-pkg{grid-template-columns:repeat(3,1fr)}
.pkg-face{display:block; padding:16px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--surface-2); transition:.18s; height:100%; position:relative}
.pkg-row{display:flex; align-items:baseline; justify-content:space-between; gap:8px}
.pkg-row b{font-size:16px}
.pkg-add{font-family:var(--mono); font-size:11.5px; color:var(--amber)}
.pkg-face small{display:block; color:var(--muted); font-size:12px; margin:6px 0 10px}
.pkg-face ul{margin:0; padding-left:16px; display:grid; gap:3px}
.pkg-face li{font-size:12px; color:var(--ink)}
.pkg:hover .pkg-face{transform:translateY(-3px); border-color:var(--line-2)}
.pkg input:focus-visible + .pkg-face{outline:2px solid var(--amber); outline-offset:2px}
.pkg input:checked + .pkg-face{border-color:var(--amber); box-shadow:0 0 0 1px var(--amber) inset; background:linear-gradient(180deg,rgba(255,176,32,.09),rgba(255,176,32,.02))}
.badge-pop{position:absolute; top:-9px; right:12px; background:var(--amber); color:#111; font-size:10px; font-weight:800; letter-spacing:.04em; padding:3px 8px; border-radius:20px; text-transform:uppercase}
.addons{margin-top:20px; border-top:1px solid var(--line); padding-top:16px}
.addons-h{margin:0 0 10px; font-size:12px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
.addon{display:flex; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--line); border-radius:var(--r-md); background:var(--surface-2); cursor:pointer; margin-bottom:8px; transition:.15s}
.addon:hover{border-color:var(--line-2)}
.addon span{display:flex; flex-direction:column}
.addon span b{font-size:13.5px}
.addon span small{font-size:11.5px; color:var(--muted)}
.addon .addon-price{margin-left:auto; font-family:var(--mono); font-size:12.5px; color:var(--muted)}
.addon input:checked ~ .addon-price{color:var(--amber)}
.addon:has(input:checked){border-color:var(--amber); box-shadow:0 0 0 1px var(--amber) inset}
.addon input:focus-visible + span{outline:2px solid var(--amber); outline-offset:3px; border-radius:4px}
/* ---------- STEP 4 : FORM ---------- */
.formgrid{display:grid; grid-template-columns:1fr 1fr; gap:16px}
.field{display:flex; flex-direction:column; gap:6px; min-width:0}
.field-full{grid-column:1 / -1}
.field label{font-size:12.5px; font-weight:600; color:var(--ink)}
.field input,.field select,.field textarea{
background:var(--surface-2); border:1px solid var(--line); color:var(--ink);
border-radius:var(--r-sm); padding:11px 12px; font:inherit; font-size:14px; transition:.15s; width:100%;
}
.field textarea{resize:vertical; min-height:74px}
.field input::placeholder,.field textarea::placeholder{color:#5c5c63}
.field input:focus,.field select:focus,.field textarea:focus{outline:none; border-color:var(--amber); box-shadow:0 0 0 3px rgba(255,176,32,.14)}
.field.invalid input,.field.invalid select,.field.invalid textarea{border-color:var(--red); box-shadow:0 0 0 3px rgba(255,77,77,.14)}
.err{font-size:11.5px; color:var(--red); min-height:14px}
/* ---------- STEP 5 : REVIEW ---------- */
.review{margin:0; display:grid; gap:0; border:1px solid var(--line); border-radius:var(--r-md); overflow:hidden}
.review .rrow{display:flex; justify-content:space-between; gap:16px; padding:12px 16px; border-bottom:1px solid var(--line)}
.review .rrow:last-child{border-bottom:0}
.review dt{color:var(--muted); font-size:13px}
.review dd{margin:0; font-weight:600; font-size:13.5px; text-align:right}
.review .rrow.tot dd{font-family:var(--mono); color:var(--amber)}
.terms{display:flex; align-items:center; gap:10px; margin-top:16px; font-size:13px; color:var(--muted); cursor:pointer}
.terms input{width:18px; height:18px; accent-color:var(--amber); flex:none; position:static; opacity:1}
/* ---------- NAV ---------- */
.navbar{display:flex; align-items:center; gap:12px; margin-top:24px; padding-top:18px; border-top:1px solid var(--line)}
.spacer{flex:1}
.btn{border:1px solid var(--line-2); background:var(--surface-2); color:var(--ink); padding:11px 20px; border-radius:var(--r-sm); font-weight:600; font-size:14px; cursor:pointer; transition:.15s}
.btn:hover{border-color:var(--line-2); transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.ghost:hover{color:var(--amber); border-color:var(--amber)}
.btn.primary{background:var(--amber); border-color:var(--amber); color:#111}
.btn.primary:hover{background:var(--amber-d)}
/* ---------- SUMMARY ---------- */
.summary{position:sticky; top:20px; background:var(--surface); border:1px solid var(--line); border-radius:var(--r-lg); padding:20px; overflow:hidden}
.summary::before{content:""; position:absolute; inset:0 0 auto 0; height:3px; background:linear-gradient(90deg,var(--amber),transparent)}
.sum-head{display:flex; align-items:center; gap:8px; font-weight:700; font-size:13px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); margin-bottom:16px}
.sum-lines{margin:0}
.sum-row{display:flex; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px dashed var(--line); font-size:13px}
.sum-row dt{color:var(--muted)}
.sum-row dd{margin:0; text-align:right; font-weight:600; max-width:150px}
.sum-row.muted dd{font-weight:500}
.sum-row.addon-line dd{font-family:var(--mono); color:var(--amber); font-size:12px}
.sum-total{display:flex; align-items:center; justify-content:space-between; margin-top:14px; padding-top:14px; border-top:1px solid var(--line-2); font-weight:700}
.tc.big{font-size:22px}
.sum-note{font-size:11px; color:var(--muted); margin:14px 0 0; line-height:1.4}
/* ---------- CONFIRM ---------- */
.confirm{margin-top:10px}
.confirm .bar{height:44px; background:#000}
.confirm-inner{
background:radial-gradient(120% 130% at 50% -20%, rgba(255,176,32,.16), transparent 60%), var(--surface);
border-left:1px solid var(--line); border-right:1px solid var(--line);
padding:50px 30px; text-align:center;
}
.confirm[hidden]{display:none}
.check{width:64px; height:64px; margin:0 auto 20px; border-radius:50%; background:var(--amber); color:#111; font-size:32px; font-weight:800; display:grid; place-items:center; animation:pop .5s cubic-bezier(.2,1.4,.5,1) both}
@keyframes pop{from{transform:scale(0); opacity:0}to{transform:scale(1); opacity:1}}
.confirm-inner h2{font-size:26px}
.confirm-sub{color:var(--muted); margin:10px auto 26px; max-width:42ch}
.confirm-card{display:grid; grid-template-columns:1fr 1fr; gap:1px; max-width:420px; margin:0 auto 28px; background:var(--line); border:1px solid var(--line); border-radius:var(--r-md); overflow:hidden}
.confirm-card div{background:var(--surface-2); padding:14px 16px; text-align:left; display:flex; flex-direction:column; gap:4px}
.confirm-card span{font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em}
.confirm-card b{font-size:15px}
/* ---------- TOAST ---------- */
.toast{
position:fixed; left:50%; bottom:26px; transform:translate(-50%,20px);
background:var(--surface-2); border:1px solid var(--line-2); color:var(--ink);
padding:12px 18px; border-radius:var(--r-md); font-size:13.5px; font-weight:500;
box-shadow:0 12px 40px rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:.28s; z-index:60; max-width:88vw;
}
.toast.show{opacity:1; transform:translate(-50%,0)}
.toast.err{border-color:var(--red); color:#ffd7d7}
/* ---------- RESPONSIVE ---------- */
@media (max-width:900px){
.wrap{grid-template-columns:1fr}
.rail{position:static}
.steps{grid-auto-flow:column; grid-auto-columns:minmax(64px,1fr); overflow-x:auto; padding-bottom:4px; gap:6px}
.step{flex-direction:column; text-align:center; gap:6px; padding:8px 6px}
.step .lbl small{display:none}
.step .lbl b{font-size:11px}
.summary{position:static}
}
@media (max-width:520px){
.stage{padding:0 12px 56px}
.hero{margin:0 -12px 26px}
.hero-inner{padding:32px 20px}
.bar{height:18px}
.panelwrap{padding:18px 16px}
.grid-type{grid-template-columns:1fr}
.grid-pkg{grid-template-columns:1fr}
.formgrid{grid-template-columns:1fr}
.confirm-card{grid-template-columns:1fr}
.navbar{flex-wrap:wrap}
.btn{flex:1; text-align:center}
.btn.ghost{flex:0 0 auto}
.step .lbl b{display:none}
}(function () {
"use strict";
var form = document.getElementById("bookingForm");
var panels = Array.prototype.slice.call(document.querySelectorAll(".panel"));
var stepEls = Array.prototype.slice.call(document.querySelectorAll(".step"));
var btnBack = document.getElementById("btnBack");
var btnNext = document.getElementById("btnNext");
var btnConfirm = document.getElementById("btnConfirm");
var TOTAL = panels.length; // 5
var current = 1;
var state = {
ptype: null, ptypePrice: 0,
date: null,
pkg: null, pkgPrice: 0,
addons: [] // {name, price}
};
/* ---------- toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg, isErr) {
toastEl.textContent = msg;
toastEl.classList.toggle("err", !!isErr);
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
function money(n) { return "$" + Math.round(n).toLocaleString("en-US"); }
/* ---------- hero timecode clock ---------- */
var clock = document.getElementById("heroClock");
var frame = 0;
setInterval(function () {
frame = (frame + 1) % (24 * 60 * 60 * 24);
var f = frame % 24;
var totalSec = Math.floor(frame / 24);
var s = totalSec % 60, m = Math.floor(totalSec / 60) % 60, h = Math.floor(totalSec / 3600) % 24;
function p(x) { return String(x).padStart(2, "0"); }
clock.textContent = p(h) + ":" + p(m) + ":" + p(s) + ":" + p(f);
}, 42);
/* ---------- step navigation ---------- */
function showStep(n) {
current = n;
panels.forEach(function (p) {
var pn = +p.getAttribute("data-panel");
var active = pn === n;
p.hidden = !active;
p.classList.toggle("is-active", active);
});
stepEls.forEach(function (el) {
var sn = +el.getAttribute("data-step");
el.classList.toggle("is-active", sn === n);
el.classList.toggle("is-done", sn < n);
});
btnBack.hidden = n === 1;
btnNext.hidden = n === TOTAL;
btnConfirm.hidden = n !== TOTAL;
if (n === TOTAL) buildReview();
document.querySelector(".panelwrap").scrollIntoView({ behavior: "smooth", block: "nearest" });
}
/* ---------- validation per step ---------- */
function validateStep(n) {
if (n === 1) {
if (!state.ptype) { toast("Pick a project type to continue.", true); return false; }
return true;
}
if (n === 2) {
if (!state.date) { toast("Choose an available shoot date.", true); return false; }
return true;
}
if (n === 3) {
if (!state.pkg) { toast("Select a production package.", true); return false; }
return true;
}
if (n === 4) return validateForm();
return true;
}
function validateForm() {
var reqs = [
{ id: "fName", msg: "Enter your name." },
{ id: "fEmail", msg: "Enter a valid email.", email: true },
{ id: "fLocation", msg: "Where's the shoot?" },
{ id: "fRuntime", msg: "Pick a runtime." }
];
var firstBad = null;
reqs.forEach(function (r) {
var el = document.getElementById(r.id);
var wrap = el.closest(".field");
var errEl = wrap.querySelector(".err");
var val = (el.value || "").trim();
var bad = !val;
if (!bad && r.email) bad = !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
wrap.classList.toggle("invalid", bad);
errEl.textContent = bad ? r.msg : "";
if (bad && !firstBad) firstBad = el;
});
if (firstBad) { firstBad.focus(); toast("Fix the highlighted fields.", true); return false; }
return true;
}
btnNext.addEventListener("click", function () {
if (!validateStep(current)) return;
if (current < TOTAL) showStep(current + 1);
});
btnBack.addEventListener("click", function () {
if (current > 1) showStep(current - 1);
});
// allow clicking rail to jump backward to completed steps
stepEls.forEach(function (el) {
el.addEventListener("click", function () {
var n = +el.getAttribute("data-step");
if (n < current) showStep(n);
});
});
/* ---------- step 1 : project type ---------- */
form.querySelectorAll('input[name="ptype"]').forEach(function (r) {
r.addEventListener("change", function () {
state.ptype = r.value;
state.ptypePrice = +r.getAttribute("data-price");
updateSummary();
});
});
/* ---------- step 3 : package + addons ---------- */
form.querySelectorAll('input[name="pkg"]').forEach(function (r) {
r.addEventListener("change", function () {
state.pkg = r.value;
state.pkgPrice = +r.getAttribute("data-price");
updateSummary();
});
});
form.querySelectorAll('input[name="addon"]').forEach(function (c) {
c.addEventListener("change", function () {
state.addons = [];
form.querySelectorAll('input[name="addon"]:checked').forEach(function (x) {
state.addons.push({ name: x.value, price: +x.getAttribute("data-price") });
});
updateSummary();
});
});
/* live-clear field errors */
form.querySelectorAll(".field input, .field select, .field textarea").forEach(function (el) {
el.addEventListener("input", function () {
var wrap = el.closest(".field");
if (wrap.classList.contains("invalid")) {
wrap.classList.remove("invalid");
wrap.querySelector(".err").textContent = "";
}
});
});
/* ---------- calendar ---------- */
var calGrid = document.getElementById("calGrid");
var calMonth = document.getElementById("calMonth");
var calPrev = document.getElementById("calPrev");
var calNext = document.getElementById("calNext");
var MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var today = new Date(); today.setHours(0, 0, 0, 0);
var view = new Date(today.getFullYear(), today.getMonth(), 1);
// deterministic blackout days per month (studio booked out)
function blackoutSet(y, m) {
var seed = (y * 12 + m);
var a = ((seed * 7) % 26) + 2;
var b = ((seed * 13) % 26) + 2;
return { a: a, b: b };
}
function renderCal() {
calGrid.innerHTML = "";
var y = view.getFullYear(), m = view.getMonth();
calMonth.textContent = MONTHS[m] + " " + y;
// Monday-first offset
var firstDow = (new Date(y, m, 1).getDay() + 6) % 7;
var days = new Date(y, m + 1, 0).getDate();
var bo = blackoutSet(y, m);
for (var i = 0; i < firstDow; i++) {
var e = document.createElement("div");
e.className = "cal-cell empty";
calGrid.appendChild(e);
}
for (var d = 1; d <= days; d++) {
var cell = document.createElement("button");
cell.type = "button";
cell.className = "cal-cell";
cell.textContent = d;
var cellDate = new Date(y, m, d); cellDate.setHours(0, 0, 0, 0);
var iso = y + "-" + String(m + 1).padStart(2, "0") + "-" + String(d).padStart(2, "0");
cell.setAttribute("data-iso", iso);
var past = cellDate < today;
var weekend = [0, 6].indexOf(cellDate.getDay()) !== -1;
var blocked = (d === bo.a || d === bo.b) && !past;
if (past || weekend) {
cell.disabled = true;
cell.setAttribute("aria-disabled", "true");
} else if (blocked) {
cell.disabled = true;
cell.classList.add("blocked");
cell.title = "Booked out";
} else {
cell.setAttribute("aria-label", MONTHS[m] + " " + d + ", " + y + ", available");
cell.addEventListener("click", function () { pickDate(this); });
}
if (state.date && state.date.iso === iso) cell.classList.add("sel");
calGrid.appendChild(cell);
}
// disable prev nav if at/ before current month
calPrev.disabled = (y === today.getFullYear() && m === today.getMonth());
}
function pickDate(cell) {
calGrid.querySelectorAll(".cal-cell.sel").forEach(function (c) { c.classList.remove("sel"); });
cell.classList.add("sel");
var iso = cell.getAttribute("data-iso");
var parts = iso.split("-");
var dt = new Date(+parts[0], +parts[1] - 1, +parts[2]);
var pretty = dt.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric" });
state.date = { iso: iso, pretty: pretty };
updateSummary();
toast("Shoot date locked: " + pretty);
}
calPrev.addEventListener("click", function () {
view.setMonth(view.getMonth() - 1); renderCal();
});
calNext.addEventListener("click", function () {
view.setMonth(view.getMonth() + 1); renderCal();
});
/* ---------- summary + totals ---------- */
var TAX = 0.0875;
function subtotal() {
var addonSum = state.addons.reduce(function (a, x) { return a + x.price; }, 0);
return state.ptypePrice + state.pkgPrice + addonSum;
}
function grandTotal() {
var sub = subtotal();
return sub + sub * TAX;
}
function updateSummary() {
document.getElementById("sProject").textContent = state.ptype
? state.ptype + " · " + money(state.ptypePrice) : "—";
document.getElementById("sDate").textContent = state.date ? state.date.pretty : "—";
document.getElementById("sPkg").textContent = state.pkg
? state.pkg + (state.pkgPrice ? " · +" + money(state.pkgPrice) : " · included") : "—";
var addWrap = document.getElementById("sAddons");
addWrap.innerHTML = "";
state.addons.forEach(function (x) {
var row = document.createElement("div");
row.className = "sum-row addon-line";
row.innerHTML = "<dt>+ " + x.name + "</dt><dd>" + money(x.price) + "</dd>";
addWrap.appendChild(row);
});
var sub = subtotal();
document.getElementById("sTax").textContent = money(sub * TAX);
document.getElementById("sTotal").textContent = money(grandTotal());
}
/* ---------- review ---------- */
function buildReview() {
var list = document.getElementById("reviewList");
var name = (document.getElementById("fName").value || "—").trim() || "—";
var email = (document.getElementById("fEmail").value || "—").trim() || "—";
var company = (document.getElementById("fCompany").value || "").trim();
var loc = (document.getElementById("fLocation").value || "—").trim() || "—";
var runtime = document.getElementById("fRuntime").value || "—";
var rows = [
["Project", state.ptype || "—"],
["Shoot date", state.date ? state.date.pretty : "—"],
["Package", state.pkg || "—"],
["Add-ons", state.addons.length ? state.addons.map(function (a) { return a.name; }).join(", ") : "None"],
["Client", name + (company ? " · " + company : "")],
["Email", email],
["Location", loc],
["Runtime", runtime]
];
list.innerHTML = "";
rows.forEach(function (r) {
var div = document.createElement("div");
div.className = "rrow";
div.innerHTML = "<dt>" + r[0] + "</dt><dd>" + escapeHtml(r[1]) + "</dd>";
list.appendChild(div);
});
var tot = document.createElement("div");
tot.className = "rrow tot";
tot.innerHTML = "<dt>Total (incl. tax)</dt><dd>" + money(grandTotal()) + "</dd>";
list.appendChild(tot);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
/* ---------- confirm ---------- */
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!document.getElementById("agree").checked) {
toast("Please accept the deposit policy.", true);
return;
}
var ref = "AG-" + Math.random().toString(36).slice(2, 6).toUpperCase() +
"-" + (100 + Math.floor(Math.random() * 899));
document.getElementById("cRef").textContent = ref;
document.getElementById("cDate").textContent = state.date ? state.date.pretty : "—";
document.getElementById("cPkg").textContent = state.pkg || "—";
document.getElementById("cTotal").textContent = money(grandTotal());
document.querySelector(".wrap").style.display = "none";
var conf = document.getElementById("confirm");
conf.hidden = false;
conf.scrollIntoView({ behavior: "smooth", block: "start" });
toast("Booking confirmed · " + ref);
});
document.getElementById("btnAgain").addEventListener("click", function () {
form.reset();
state = { ptype: null, ptypePrice: 0, date: null, pkg: null, pkgPrice: 0, addons: [] };
document.querySelectorAll(".field.invalid").forEach(function (w) {
w.classList.remove("invalid");
var er = w.querySelector(".err"); if (er) er.textContent = "";
});
view = new Date(today.getFullYear(), today.getMonth(), 1);
renderCal();
updateSummary();
document.getElementById("confirm").hidden = true;
document.querySelector(".wrap").style.display = "";
showStep(1);
window.scrollTo({ top: 0, behavior: "smooth" });
});
/* ---------- init ---------- */
renderCal();
updateSummary();
showStep(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aperture & Grain — Book a Shoot</title>
<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;800&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="stage">
<!-- Hero -->
<header class="hero" role="banner">
<div class="bar bar-top" aria-hidden="true"></div>
<div class="hero-inner">
<div class="brand">
<span class="rec" aria-hidden="true"></span>
<span class="brand-name">APERTURE & GRAIN</span>
</div>
<h1 class="hero-title">Book a shoot</h1>
<p class="hero-sub">Cinematic production, from first frame to final grade. Five steps, ninety seconds.</p>
<div class="hero-meta">
<span class="tc" id="heroClock">00:00:00:00</span>
<span class="dot" aria-hidden="true">•</span>
<span>Studio 12 — Brooklyn, NY</span>
</div>
</div>
<div class="bar bar-bot" aria-hidden="true"></div>
</header>
<main class="wrap">
<!-- Progress rail -->
<nav class="rail" aria-label="Booking progress">
<ol class="steps" id="stepList">
<li class="step is-active" data-step="1"><span class="idx">1</span><span class="lbl"><b>Project</b><small>Type of production</small></span></li>
<li class="step" data-step="2"><span class="idx">2</span><span class="lbl"><b>Date</b><small>Pick a shoot day</small></span></li>
<li class="step" data-step="3"><span class="idx">3</span><span class="lbl"><b>Package</b><small>Crew & deliverables</small></span></li>
<li class="step" data-step="4"><span class="idx">4</span><span class="lbl"><b>Details</b><small>Brief & contact</small></span></li>
<li class="step" data-step="5"><span class="idx">5</span><span class="lbl"><b>Review</b><small>Confirm & book</small></span></li>
</ol>
</nav>
<!-- Wizard -->
<section class="panelwrap" aria-live="polite">
<form id="bookingForm" novalidate>
<!-- STEP 1 -->
<fieldset class="panel is-active" data-panel="1">
<legend class="panel-h"><span class="eyebrow">Step 1 / 5</span><h2>What are we shooting?</h2></legend>
<div class="grid grid-type" role="radiogroup" aria-label="Project type">
<label class="typecard">
<input type="radio" name="ptype" value="Commercial" data-price="4200" required />
<span class="tc-face">
<span class="tc-emoji" aria-hidden="true">🎬</span>
<b>Commercial</b>
<small>Brand film / TVC, 15–60s cut</small>
<span class="tc-price">from $4,200</span>
</span>
</label>
<label class="typecard">
<input type="radio" name="ptype" value="Music Video" data-price="5600" />
<span class="tc-face">
<span class="tc-emoji" aria-hidden="true">🎸</span>
<b>Music Video</b>
<small>Performance + narrative, full track</small>
<span class="tc-price">from $5,600</span>
</span>
</label>
<label class="typecard">
<input type="radio" name="ptype" value="Event Film" data-price="2800" />
<span class="tc-face">
<span class="tc-emoji" aria-hidden="true">🎤</span>
<b>Event Film</b>
<small>Conference, launch, live recap</small>
<span class="tc-price">from $2,800</span>
</span>
</label>
<label class="typecard">
<input type="radio" name="ptype" value="Documentary" data-price="6400" />
<span class="tc-face">
<span class="tc-emoji" aria-hidden="true">📽️</span>
<b>Documentary</b>
<small>Interview-led short, multi-day</small>
<span class="tc-price">from $6,400</span>
</span>
</label>
</div>
</fieldset>
<!-- STEP 2 -->
<fieldset class="panel" data-panel="2" hidden>
<legend class="panel-h"><span class="eyebrow">Step 2 / 5</span><h2>Choose a shoot date</h2></legend>
<div class="cal">
<div class="cal-top">
<button type="button" class="cal-nav" id="calPrev" aria-label="Previous month">‹</button>
<span class="cal-month" id="calMonth">Month</span>
<button type="button" class="cal-nav" id="calNext" aria-label="Next month">›</button>
</div>
<div class="cal-dow" aria-hidden="true"><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span><span>Su</span></div>
<div class="cal-grid" id="calGrid" role="grid" aria-label="Available dates"></div>
<p class="cal-legend"><span class="lg lg-free"></span> Available <span class="lg lg-block"></span> Booked out</p>
</div>
</fieldset>
<!-- STEP 3 -->
<fieldset class="panel" data-panel="3" hidden>
<legend class="panel-h"><span class="eyebrow">Step 3 / 5</span><h2>Pick a production package</h2></legend>
<div class="grid grid-pkg" role="radiogroup" aria-label="Package">
<label class="pkg">
<input type="radio" name="pkg" value="Spark" data-price="0" required />
<span class="pkg-face">
<span class="pkg-row"><b>Spark</b><span class="pkg-add">included</span></span>
<small>Solo shooter · 4h block · 1 edited cut · 1080p</small>
<ul><li>1 camera op</li><li>Same-week delivery</li><li>2 revisions</li></ul>
</span>
</label>
<label class="pkg">
<input type="radio" name="pkg" value="Signal" data-price="1900" />
<span class="pkg-face">
<span class="badge-pop">Most booked</span>
<span class="pkg-row"><b>Signal</b><span class="pkg-add">+$1,900</span></span>
<small>2-cam crew · full day · 2 cuts · 4K</small>
<ul><li>Director + DP</li><li>Colour grade</li><li>4 revisions</li></ul>
</span>
</label>
<label class="pkg">
<input type="radio" name="pkg" value="Summit" data-price="4300" />
<span class="pkg-face">
<span class="pkg-row"><b>Summit</b><span class="pkg-add">+$4,300</span></span>
<small>Full crew · 2 days · 4 cuts · 6K + social</small>
<ul><li>Grip & lighting team</li><li>Sound design</li><li>Unlimited revisions</li></ul>
</span>
</label>
</div>
<div class="addons">
<p class="addons-h">Add-ons</p>
<label class="addon"><input type="checkbox" name="addon" value="Drone / aerial" data-price="650" /><span><b>Drone / aerial</b><small>Licensed FPV + mavic operator</small></span><span class="addon-price">$650</span></label>
<label class="addon"><input type="checkbox" name="addon" value="Hair & makeup" data-price="400" /><span><b>Hair & makeup</b><small>On-set artist, full day</small></span><span class="addon-price">$400</span></label>
<label class="addon"><input type="checkbox" name="addon" value="Original score" data-price="900" /><span><b>Original score</b><small>Composer + mix, licence cleared</small></span><span class="addon-price">$900</span></label>
</div>
</fieldset>
<!-- STEP 4 -->
<fieldset class="panel" data-panel="4" hidden>
<legend class="panel-h"><span class="eyebrow">Step 4 / 5</span><h2>Project brief</h2></legend>
<div class="formgrid">
<div class="field">
<label for="fName">Full name</label>
<input id="fName" name="name" type="text" autocomplete="name" placeholder="Mara Ellison" required />
<span class="err" data-for="fName"></span>
</div>
<div class="field">
<label for="fEmail">Email</label>
<input id="fEmail" name="email" type="email" autocomplete="email" placeholder="[email protected]" required />
<span class="err" data-for="fEmail"></span>
</div>
<div class="field">
<label for="fCompany">Company / artist</label>
<input id="fCompany" name="company" type="text" placeholder="Northlight Records" />
<span class="err" data-for="fCompany"></span>
</div>
<div class="field">
<label for="fLocation">Shoot location</label>
<input id="fLocation" name="location" type="text" placeholder="Rooftop — Bushwick" required />
<span class="err" data-for="fLocation"></span>
</div>
<div class="field">
<label for="fRuntime">Target runtime</label>
<select id="fRuntime" name="runtime" required>
<option value="" disabled selected>Select…</option>
<option>15–30 seconds</option>
<option>30–60 seconds</option>
<option>1–3 minutes</option>
<option>3 minutes +</option>
</select>
<span class="err" data-for="fRuntime"></span>
</div>
<div class="field field-full">
<label for="fBrief">Creative brief</label>
<textarea id="fBrief" name="brief" rows="3" placeholder="Moody neon-lit performance, handheld energy, one hero location…"></textarea>
<span class="err" data-for="fBrief"></span>
</div>
</div>
</fieldset>
<!-- STEP 5 -->
<fieldset class="panel" data-panel="5" hidden>
<legend class="panel-h"><span class="eyebrow">Step 5 / 5</span><h2>Review & confirm</h2></legend>
<dl class="review" id="reviewList"></dl>
<label class="terms"><input type="checkbox" id="agree" name="agree" required /><span>I agree to the 25% deposit and cancellation policy.</span></label>
</fieldset>
<!-- Nav -->
<div class="navbar">
<button type="button" class="btn ghost" id="btnBack" hidden>‹ Back</button>
<div class="spacer"></div>
<button type="button" class="btn primary" id="btnNext">Next ›</button>
<button type="submit" class="btn primary" id="btnConfirm" hidden>Confirm booking</button>
</div>
</form>
</section>
<!-- Order summary -->
<aside class="summary" aria-label="Order summary">
<div class="sum-head">
<span class="rec sm" aria-hidden="true"></span>
<span>Your quote</span>
</div>
<dl class="sum-lines" id="sumLines">
<div class="sum-row muted"><dt>Project</dt><dd id="sProject">—</dd></div>
<div class="sum-row muted"><dt>Date</dt><dd id="sDate">—</dd></div>
<div class="sum-row"><dt>Package</dt><dd id="sPkg">—</dd></div>
<div id="sAddons"></div>
<div class="sum-row"><dt>Est. tax</dt><dd id="sTax">$0</dd></div>
</dl>
<div class="sum-total">
<span>Total</span>
<span class="tc big" id="sTotal">$0</span>
</div>
<p class="sum-note">25% deposit today · balance on wrap day.</p>
</aside>
</main>
<!-- Confirmation -->
<section class="confirm" id="confirm" hidden aria-live="assertive">
<div class="bar" aria-hidden="true"></div>
<div class="confirm-inner">
<div class="check" aria-hidden="true">✓</div>
<h2>You're on the schedule</h2>
<p class="confirm-sub">We've emailed your brief. A producer will reach out within one business day.</p>
<div class="confirm-card">
<div><span>Reference</span><b class="tc" id="cRef">—</b></div>
<div><span>Shoot date</span><b id="cDate">—</b></div>
<div><span>Package</span><b id="cPkg">—</b></div>
<div><span>Total</span><b class="tc" id="cTotal">—</b></div>
</div>
<button type="button" class="btn primary" id="btnAgain">Book another project</button>
</div>
<div class="bar" aria-hidden="true"></div>
</section>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A multi-step booking flow for Aperture & Grain, a fictional video production house, styled as a cinematic dark control surface. The hero wears letterbox black bars and an amber production slate; below it a progress rail walks the client through five stages — Project, Date, Package, Details and Review — before a letterboxed confirmation card closes the deal. Each stage is a self-contained panel; only one shows at a time, and the rail marks the active step with an amber glow.
Step one presents selectable project-type cards (Commercial, Music Video, Event Film, Documentary) with hover lift and a checked state. Step two renders a real month calendar you can page through, disabling past and blackout days and printing the chosen shoot date as a JetBrains Mono timecode. Step three lists coverage packages with per-tier pricing plus optional add-ons, and step four collects contact, location and brief details with inline validation that highlights the first offending field.
A persistent order summary recalculates the running total — base package, add-ons and an estimated tax line — whenever a choice changes, and Back/Next buttons gate progress until the current step is valid, firing a toast when it is not. Confirming generates a fictional booking reference, swaps in a success panel with a subtle amber reveal, and offers a reset to book another project — all in vanilla JS with no dependencies.