Storybook — Pop-Up Book Landing
A tactile pop-up-book landing for the fictional childrens title The Whirlwood Pop-Up Storybook, built in HTML, CSS and vanilla JS. A paper hero scene unfolds layer by layer with stand-up hills, a waving paper fox and pointer parallax. Feature spreads lift from the gutter on scroll with offset shadows and fold lines via IntersectionObserver, an interactive forest tilts to separate its layers, and a springy pre-order CTA card validates an email. Easy-read font toggle, sparkle bursts and reduced-motion support included.
MCP
Code
:root {
--bg: #fff8ef;
--paper: #fffdf8;
--ink: #2c2350;
--muted: #6a6188;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--r: 22px;
--r-lg: 30px;
--ring: #5ec5d6;
--shadow-soft: 0 14px 30px rgba(44, 35, 80, 0.14);
--shadow-pop: 0 22px 0 -8px rgba(44, 35, 80, 0.12), 0 30px 44px rgba(44, 35, 80, 0.22);
--font-display: "Baloo 2", system-ui, sans-serif;
--font-body: "Nunito", system-ui, sans-serif;
--ease-pop: cubic-bezier(0.34, 1.56, 0.64, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font-body);
color: var(--ink);
line-height: 1.5;
background:
radial-gradient(120% 90% at 50% -10%, #fff 0%, var(--bg) 46%, #fdeedd 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* dyslexia / easy-read mode */
body.easy-read {
font-family: "Comic Sans MS", "Nunito", system-ui, sans-serif;
letter-spacing: 0.4px;
word-spacing: 2px;
line-height: 1.7;
}
h1, h2, h3 { font-family: var(--font-display); line-height: 1.1; margin: 0; }
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
position: absolute; left: 12px; top: -60px; z-index: 50;
background: var(--ink); color: #fff; padding: 12px 18px;
border-radius: 999px; font-weight: 800; text-decoration: none;
transition: top 0.2s var(--ease-pop);
}
.skip-link:focus-visible { top: 12px; }
:focus-visible {
outline: 3px solid var(--ring);
outline-offset: 3px;
border-radius: 6px;
}
/* ===== pills / buttons ===== */
.pill {
font-family: var(--font-display);
font-weight: 700;
font-size: 1rem;
border: 3px solid var(--ink);
border-radius: 999px;
padding: 12px 22px;
min-height: 48px;
display: inline-flex; align-items: center; gap: 8px; justify-content: center;
cursor: pointer;
text-decoration: none;
color: var(--ink);
background: #fff;
transition: transform 0.18s var(--ease-pop), box-shadow 0.18s ease, background 0.18s ease;
box-shadow: 0 5px 0 var(--ink);
}
.pill:hover { transform: translateY(-2px); }
.pill:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--ink); }
.pill--solid { background: var(--primary); color: #fff; }
.pill--solid:hover { background: #ff7a23; }
.pill--ghost { background: var(--paper); }
.pill--lg { font-size: 1.08rem; padding: 14px 26px; min-height: 54px; }
.pill[aria-pressed="true"] { background: var(--accent); }
/* ===== top bar ===== */
.topbar {
position: sticky; top: 0; z-index: 40;
display: flex; align-items: center; gap: 16px;
padding: 12px clamp(14px, 4vw, 40px);
background: rgba(255, 248, 239, 0.86);
backdrop-filter: blur(10px);
border-bottom: 3px solid rgba(44, 35, 80, 0.1);
}
.brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; color: var(--ink); }
.brand__mark {
display: grid; place-items: center;
background: var(--accent);
border: 3px solid var(--ink);
border-radius: 14px;
padding: 3px;
box-shadow: 0 4px 0 var(--ink);
}
.brand__name { font-family: var(--font-display); font-weight: 800; font-size: 1.25rem; }
.topnav { display: flex; gap: 6px; margin-inline: auto; }
.topnav a {
font-family: var(--font-display); font-weight: 600;
color: var(--muted); text-decoration: none;
padding: 8px 14px; border-radius: 999px;
transition: background 0.18s ease, color 0.18s ease;
}
.topnav a:hover { background: #fff; color: var(--ink); }
.topbar__tools { display: flex; align-items: center; gap: 10px; }
/* ===== hero ===== */
.hero {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: clamp(20px, 4vw, 56px);
align-items: center;
max-width: 1180px;
margin: clamp(24px, 5vw, 60px) auto;
padding: 0 clamp(16px, 4vw, 40px);
}
.hero__paper {
position: relative;
aspect-ratio: 5 / 4;
background:
repeating-linear-gradient(115deg, rgba(255,255,255,0.5) 0 2px, transparent 2px 8px),
linear-gradient(180deg, #fffdf8, #fdf2e2);
border: 4px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
overflow: hidden;
}
.hero__spine {
position: absolute; left: 50%; top: 6%; bottom: 6%; width: 2px;
background: repeating-linear-gradient(180deg, rgba(44,35,80,0.28) 0 7px, transparent 7px 14px);
transform: translateX(-50%);
}
.scene { position: absolute; inset: 0; }
.scene__sky {
position: absolute; inset: 0;
background: linear-gradient(180deg, #cdeefb 0%, #e9f7ff 52%, #fdf2e2 52% 100%);
}
.layer {
position: absolute;
will-change: transform;
transition: transform 0.18s ease-out;
}
/* pop-up reveal */
.layer.pop {
transform-origin: bottom center;
transform: rotateX(82deg) translateY(20px);
opacity: 0;
}
.hero__paper.is-popped .layer.pop { animation: standUp 0.7s var(--ease-pop) both; }
@keyframes standUp {
0% { transform: rotateX(82deg) translateY(20px); opacity: 0; }
60% { opacity: 1; }
100% { transform: rotateX(0) translateY(0); opacity: 1; }
}
.layer--sun { top: 8%; right: 10%; }
.layer--cloud { top: 14%; }
.cloud-a { left: 8%; }
.cloud-b { right: 26%; top: 26%; }
.layer--hill {
left: -8%; right: -8%; height: 46%;
border-radius: 50% 50% 0 0 / 80% 80% 0 0;
border: 3px solid var(--ink);
}
.hill-back { bottom: 30%; background: #aee3a0; }
.hill-front { bottom: 18%; background: var(--green); left: -14%; right: -14%; height: 40%; }
.layer--fox { left: 50%; bottom: 22%; transform-origin: bottom center; margin-left: -62px; }
.hero__paper.is-popped .layer--fox { animation: standUp 0.7s var(--ease-pop) 0.15s both; }
.hero__paper.is-popped .layer--fox::after { content: ""; }
.layer--grass {
left: 0; right: 0; bottom: 0; height: 22%;
background:
radial-gradient(14px 24px at 10% 100%, #6ac378 0 92%, transparent 93%),
radial-gradient(14px 30px at 26% 100%, #7bd389 0 92%, transparent 93%),
radial-gradient(14px 22px at 42% 100%, #6ac378 0 92%, transparent 93%),
radial-gradient(14px 28px at 60% 100%, #7bd389 0 92%, transparent 93%),
radial-gradient(14px 24px at 76% 100%, #6ac378 0 92%, transparent 93%),
radial-gradient(14px 30px at 92% 100%, #7bd389 0 92%, transparent 93%),
linear-gradient(180deg, #5cb574, #4ea766);
border-top: 3px solid var(--ink);
}
.hero__copy { max-width: 34rem; }
.kicker {
display: inline-flex; align-items: center; gap: 8px;
font-family: var(--font-display); font-weight: 700; font-size: 0.92rem;
text-transform: uppercase; letter-spacing: 1px;
color: var(--primary);
margin: 0 0 12px;
}
.kicker__dot { width: 12px; height: 12px; border-radius: 50%; background: var(--pink); border: 2px solid var(--ink); }
.hero h1 { font-size: clamp(2.1rem, 5.2vw, 3.4rem); font-weight: 800; }
.hl { color: var(--primary); }
.lede { font-size: 1.12rem; color: var(--muted); margin: 18px 0 26px; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 14px; }
.hero__facts {
display: flex; flex-wrap: wrap; gap: 10px 26px;
list-style: none; padding: 0; margin: 28px 0 0;
}
.hero__facts li { font-size: 0.98rem; color: var(--muted); }
.hero__facts strong { display: block; font-family: var(--font-display); font-size: 1.3rem; color: var(--ink); }
/* ===== section heads ===== */
.section-head { max-width: 640px; margin: 0 auto clamp(28px, 5vw, 48px); text-align: center; }
.section-head--left { text-align: left; margin: 0 0 28px; }
.section-head h2 { font-size: clamp(1.7rem, 4vw, 2.5rem); font-weight: 800; }
.section-sub { color: var(--muted); font-size: 1.06rem; margin: 14px 0 0; }
/* ===== feature pages ===== */
.pages { max-width: 980px; margin: clamp(40px, 8vw, 90px) auto; padding: 0 clamp(16px, 4vw, 40px); }
.page-stack { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(26px, 5vw, 48px); }
.page-card {
position: relative;
display: grid;
grid-template-columns: auto 1fr;
gap: clamp(16px, 3vw, 30px);
align-items: center;
padding: clamp(18px, 3vw, 30px);
background: var(--paper);
border: 4px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
transform: rotate(var(--tilt));
}
.page-card::before {
content: ""; position: absolute; inset: 8px 8px -2px 8px; z-index: -1;
background: color-mix(in srgb, var(--tint) 35%, #fff);
border-radius: var(--r-lg);
}
.page-card__fold {
position: absolute; left: 0; right: 0; top: 0; height: 18px;
background: repeating-linear-gradient(90deg, transparent 0 16px, rgba(44,35,80,0.16) 16px 18px);
border-bottom: 2px dashed rgba(44,35,80,0.2);
border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.page-card__art {
display: grid; place-items: center;
width: clamp(96px, 14vw, 128px); height: clamp(96px, 14vw, 128px);
background: color-mix(in srgb, var(--tint) 22%, #fff);
border: 3px solid var(--ink);
border-radius: 20px;
}
.page-card__folio {
display: inline-block;
font-family: var(--font-display); font-weight: 700; font-size: 0.78rem;
text-transform: uppercase; letter-spacing: 1px;
color: #fff; background: var(--ink);
padding: 4px 12px; border-radius: 999px; margin-bottom: 8px;
}
.page-card h3 { font-size: clamp(1.3rem, 3vw, 1.7rem); font-weight: 800; }
.page-card p { color: var(--muted); margin: 8px 0 0; }
/* ===== scene tilt stage ===== */
.scenes { background: linear-gradient(180deg, transparent, #fdeedd 40%, #fdeedd); padding: clamp(40px, 8vw, 90px) 0; }
.scenes__inner {
max-width: 1080px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 40px);
display: grid; grid-template-columns: 0.9fr 1.1fr; gap: clamp(24px, 4vw, 48px); align-items: center;
}
.tilt-stage {
position: relative;
aspect-ratio: 16 / 11;
perspective: 900px;
background:
repeating-linear-gradient(125deg, rgba(255,255,255,0.4) 0 2px, transparent 2px 9px),
linear-gradient(180deg, #fffdf8, #fdf2e2);
border: 4px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
overflow: hidden;
cursor: grab;
}
.tilt-stage__hint {
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
z-index: 6; font-family: var(--font-display); font-weight: 700; font-size: 0.86rem;
color: var(--ink); background: var(--accent);
padding: 4px 14px; border-radius: 999px; border: 2px solid var(--ink);
box-shadow: 0 3px 0 var(--ink);
}
.tlayer { position: absolute; inset: 0; will-change: transform; transition: transform 0.12s ease-out; }
.t-sky { background: linear-gradient(180deg, #cdeefb 0%, #e9f7ff 55%, #fdf2e2 55%); }
.t-back {
left: -12%; right: -12%; bottom: 26%; top: auto; height: 44%;
border-radius: 50% 50% 0 0 / 80% 80% 0 0;
background: #aee3a0; border: 3px solid var(--ink);
}
.t-mid {
left: -14%; right: -14%; bottom: 14%; top: auto; height: 42%;
border-radius: 50% 50% 0 0 / 78% 78% 0 0;
background: var(--green); border: 3px solid var(--ink);
}
.t-fox { display: grid; place-items: end center; padding-bottom: 16%; }
.t-front {
left: 0; right: 0; bottom: 0; top: auto; height: 18%;
background:
radial-gradient(14px 26px at 14% 100%, #5cb574 0 92%, transparent 93%),
radial-gradient(14px 32px at 38% 100%, #4ea766 0 92%, transparent 93%),
radial-gradient(14px 24px at 62% 100%, #5cb574 0 92%, transparent 93%),
radial-gradient(14px 30px at 86% 100%, #4ea766 0 92%, transparent 93%),
linear-gradient(180deg, #4ea766, #3f9457);
border-top: 3px solid var(--ink);
}
/* ===== cta ===== */
.cta { max-width: 980px; margin: clamp(40px, 8vw, 100px) auto; padding: 0 clamp(16px, 4vw, 40px); }
.cta__card {
position: relative;
display: grid; grid-template-columns: auto 1fr; gap: clamp(20px, 4vw, 44px); align-items: center;
padding: clamp(26px, 5vw, 48px);
background:
repeating-linear-gradient(115deg, rgba(255,255,255,0.45) 0 2px, transparent 2px 10px),
linear-gradient(180deg, var(--accent), #ffc31f);
border: 4px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
}
.cta__card.spring { animation: springCta 0.65s var(--ease-pop); }
@keyframes springCta {
0% { transform: scale(0.94) rotate(-1deg); }
55% { transform: scale(1.03) rotate(0.6deg); }
100% { transform: scale(1) rotate(0); }
}
.cta__pop {
position: absolute; top: -16px; right: 24px;
font-family: var(--font-display); font-weight: 800; font-size: 1.1rem;
color: #fff; background: var(--pink);
padding: 6px 16px; border-radius: 999px; border: 3px solid var(--ink);
transform: rotate(8deg); box-shadow: 0 4px 0 var(--ink);
}
.cta__book { filter: drop-shadow(0 12px 16px rgba(44,35,80,0.3)); }
.cta__copy h2 { font-size: clamp(1.7rem, 4vw, 2.4rem); font-weight: 800; }
.cta__copy > p { color: var(--ink); opacity: 0.85; margin: 12px 0 20px; font-size: 1.08rem; }
.cta__form { display: flex; flex-wrap: wrap; gap: 12px; }
.cta__form input {
flex: 1 1 200px; min-height: 54px;
font-family: var(--font-body); font-size: 1.02rem;
padding: 12px 18px;
border: 3px solid var(--ink); border-radius: 999px;
background: var(--paper); color: var(--ink);
}
.cta__form input::placeholder { color: #9a93b0; }
.cta__form input[aria-invalid="true"] { border-color: var(--pink); background: #fff0f5; }
.cta__fine { margin: 16px 0 0; font-size: 0.88rem; color: var(--ink); opacity: 0.7; }
/* ===== footer ===== */
.site-foot {
display: flex; flex-wrap: wrap; gap: 16px; align-items: center; justify-content: space-between;
max-width: 1080px; margin: 0 auto; padding: 30px clamp(16px, 4vw, 40px) 50px;
color: var(--muted);
}
.site-foot p { margin: 0; }
/* ===== reveal on scroll ===== */
.reveal {
opacity: 0;
transform: translateY(34px) rotateX(28deg);
transform-origin: bottom center;
transition: opacity 0.6s ease, transform 0.7s var(--ease-pop);
}
.reveal.in {
opacity: 1;
transform: translateY(0) rotateX(0) rotate(var(--tilt, 0deg));
}
/* ===== toast ===== */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(140%);
z-index: 60;
background: var(--ink); color: #fff;
font-family: var(--font-display); font-weight: 700;
padding: 14px 22px; border-radius: 999px;
box-shadow: var(--shadow-soft);
transition: transform 0.4s var(--ease-pop);
max-width: min(90vw, 420px); text-align: center;
}
.toast.show { transform: translateX(-50%) translateY(0); }
/* ===== sparkle burst ===== */
.spark {
position: fixed; z-index: 55; pointer-events: none;
width: 12px; height: 12px; border-radius: 50%;
animation: sparkOut 0.7s ease-out forwards;
}
@keyframes sparkOut {
to { transform: translate(var(--dx), var(--dy)) scale(0.2); opacity: 0; }
}
/* ===== responsive ===== */
@media (max-width: 900px) {
.topnav { display: none; }
.hero { grid-template-columns: 1fr; }
.hero__paper { order: -1; }
.scenes__inner { grid-template-columns: 1fr; }
.section-head--left { text-align: center; }
}
@media (max-width: 620px) {
.page-card { grid-template-columns: 1fr; text-align: center; transform: rotate(0); }
.page-card__art { margin: 6px auto 0; }
.cta__card { grid-template-columns: 1fr; text-align: center; }
.cta__book { margin: 0 auto; }
.cta__form { justify-content: center; }
.topbar__tools .pill--solid { display: none; }
}
@media (max-width: 380px) {
.brand__name { display: none; }
.pill--lg { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
.layer.pop { transform: none; opacity: 1; }
.reveal { opacity: 1; transform: none; }
.tlayer { transition: none; }
}(function () {
"use strict";
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
/* ---------- 1. hero pop-up reveal ---------- */
var heroPaper = document.querySelector(".hero__paper");
function popHero() {
if (!heroPaper) return;
heroPaper.classList.remove("is-popped");
// force reflow so the animation can replay
// eslint-disable-next-line no-unused-expressions
void heroPaper.offsetWidth;
heroPaper.classList.add("is-popped");
}
var popAllBtn = document.getElementById("popAllBtn");
if (popAllBtn) {
popAllBtn.addEventListener("click", function () {
popHero();
sparkleBurst(popAllBtn);
toast("✨ Whirlwood pops up!");
});
}
/* ---------- 2. IntersectionObserver: reveal pages + auto-pop hero ---------- */
var revealEls = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window && !reduceMotion) {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("in");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.2, rootMargin: "0px 0px -8% 0px" }
);
revealEls.forEach(function (el) {
io.observe(el);
});
// pop the hero the first time it is on screen
var heroIO = new IntersectionObserver(
function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
popHero();
heroIO.disconnect();
}
});
},
{ threshold: 0.4 }
);
if (heroPaper) heroIO.observe(heroPaper);
} else {
// no IO / reduced motion: show everything immediately
revealEls.forEach(function (el) {
el.classList.add("in");
});
if (heroPaper) heroPaper.classList.add("is-popped");
}
/* ---------- 3. hero parallax on pointer ---------- */
var heroLayers = document.querySelectorAll("#heroScene .layer[data-depth]");
if (heroPaper && !reduceMotion) {
heroPaper.addEventListener("pointermove", function (e) {
var rect = heroPaper.getBoundingClientRect();
var cx = (e.clientX - rect.left) / rect.width - 0.5;
var cy = (e.clientY - rect.top) / rect.height - 0.5;
heroLayers.forEach(function (layer) {
var depth = parseFloat(layer.getAttribute("data-depth")) || 0;
var f = depth / 18;
layer.style.transform =
"translate(" + cx * f + "px," + cy * f * 0.6 + "px)";
});
});
heroPaper.addEventListener("pointerleave", function () {
heroLayers.forEach(function (l) {
l.style.transform = "";
});
});
}
/* ---------- 4. tilt stage (pointer + keyboard) ---------- */
var stage = document.getElementById("tiltStage");
var tlayers = stage ? stage.querySelectorAll(".tlayer[data-depth]") : [];
var tiltX = 0; // -1..1
function applyTilt() {
if (reduceMotion) return;
if (stage) {
stage.style.transform = "rotateY(" + tiltX * 6 + "deg)";
}
tlayers.forEach(function (l) {
var depth = parseFloat(l.getAttribute("data-depth")) || 0;
l.style.transform = "translateX(" + tiltX * depth * 0.55 + "px)";
});
}
if (stage && !reduceMotion) {
stage.style.transition = "transform 0.12s ease-out";
stage.addEventListener("pointermove", function (e) {
var rect = stage.getBoundingClientRect();
tiltX = (e.clientX - rect.left) / rect.width - 0.5;
applyTilt();
});
stage.addEventListener("pointerleave", function () {
tiltX = 0;
applyTilt();
});
stage.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft") {
tiltX = Math.max(-0.5, tiltX - 0.12);
applyTilt();
e.preventDefault();
} else if (e.key === "ArrowRight") {
tiltX = Math.min(0.5, tiltX + 0.12);
applyTilt();
e.preventDefault();
}
});
}
/* ---------- 5. CTA card springs into view + form ---------- */
var ctaCard = document.getElementById("ctaCard");
if (ctaCard && "IntersectionObserver" in window && !reduceMotion) {
var ctaIO = new IntersectionObserver(
function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
ctaCard.classList.add("spring");
ctaIO.disconnect();
}
});
},
{ threshold: 0.45 }
);
ctaIO.observe(ctaCard);
}
var form = document.getElementById("ctaForm");
var email = document.getElementById("email");
if (form && email) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var value = email.value.trim();
var ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if (!ok) {
email.setAttribute("aria-invalid", "true");
email.focus();
toast("Pop in a valid email so we can write back 💌");
return;
}
email.removeAttribute("aria-invalid");
email.value = "";
if (ctaCard && !reduceMotion) {
ctaCard.classList.remove("spring");
void ctaCard.offsetWidth;
ctaCard.classList.add("spring");
}
sparkleBurst(form.querySelector("button"));
toast("📚 Pre-order saved! Whirlwood is on its way.");
});
email.addEventListener("input", function () {
if (email.getAttribute("aria-invalid")) {
email.removeAttribute("aria-invalid");
}
});
}
/* ---------- 6. easy-read toggle ---------- */
var readToggle = document.getElementById("readToggle");
if (readToggle) {
readToggle.addEventListener("click", function () {
var on = document.body.classList.toggle("easy-read");
readToggle.setAttribute("aria-pressed", on ? "true" : "false");
toast(on ? "Easy-read font on 🔤" : "Easy-read font off");
});
}
/* ---------- 7. back to top ---------- */
var topBtn = document.getElementById("topBtn");
if (topBtn) {
topBtn.addEventListener("click", function () {
window.scrollTo({
top: 0,
behavior: reduceMotion ? "auto" : "smooth"
});
});
}
/* ---------- sparkle burst helper ---------- */
function sparkleBurst(originEl) {
if (reduceMotion || !originEl) return;
var rect = originEl.getBoundingClientRect();
var ox = rect.left + rect.width / 2;
var oy = rect.top + rect.height / 2;
var colors = ["#ff8a3d", "#5ec5d6", "#ffd23f", "#ff6f9c", "#7bd389"];
for (var i = 0; i < 14; i++) {
var s = document.createElement("span");
s.className = "spark";
var angle = (Math.PI * 2 * i) / 14;
var dist = 60 + Math.random() * 50;
s.style.left = ox + "px";
s.style.top = oy + "px";
s.style.background = colors[i % colors.length];
s.style.setProperty("--dx", Math.cos(angle) * dist + "px");
s.style.setProperty("--dy", Math.sin(angle) * dist + "px");
document.body.appendChild(s);
(function (node) {
setTimeout(function () {
node.remove();
}, 750);
})(s);
}
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pop-Up Pages — The Whirlwood Pop-Up Storybook</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=Baloo+2:wght@500;600;700;800&family=Nunito:ital,wght@0,400;0,600;0,700;0,800;1,600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- ============ TOP BAR ============ -->
<header class="topbar" role="banner">
<a class="brand" href="#main" aria-label="Pop-Up Pages home">
<span class="brand__mark" aria-hidden="true">
<svg viewBox="0 0 48 48" width="40" height="40" focusable="false">
<path d="M24 8 L40 14 L40 38 L24 32 L8 38 L8 14 Z" fill="#fff" stroke="#2c2350" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M24 8 L24 32" stroke="#2c2350" stroke-width="2.5"/>
<path d="M12 18 h8 M12 23 h8 M28 18 h8 M28 23 h8" stroke="#ff8a3d" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</span>
<span class="brand__name">Pop-Up Pages</span>
</a>
<nav class="topnav" aria-label="Primary">
<a href="#features">Pages</a>
<a href="#scenes">Scenes</a>
<a href="#cta">Get the book</a>
</nav>
<div class="topbar__tools">
<button id="readToggle" class="pill pill--ghost" type="button" aria-pressed="false">
<span aria-hidden="true">🔤</span> Easy-read
</button>
<a class="pill pill--solid" href="#cta">Open the book</a>
</div>
</header>
<main id="main">
<!-- ============ HERO — THE PAPER SCENE UNFOLDS ============ -->
<section class="hero" aria-labelledby="hero-title">
<div class="hero__paper">
<!-- fold line down the spine -->
<span class="hero__spine" aria-hidden="true"></span>
<!-- layered pop-up scene -->
<div class="scene" id="heroScene" aria-hidden="true">
<div class="scene__sky"></div>
<div class="layer layer--sun pop" data-depth="6">
<svg viewBox="0 0 120 120" width="120" height="120"><circle cx="60" cy="60" r="42" fill="#ffd23f"/><g stroke="#ffb627" stroke-width="6" stroke-linecap="round"><path d="M60 6 v14"/><path d="M60 100 v14"/><path d="M6 60 h14"/><path d="M100 60 h14"/><path d="M22 22 l10 10"/><path d="M88 88 l10 10"/><path d="M98 22 l-10 10"/><path d="M32 88 l-10 10"/></g></svg>
</div>
<div class="layer layer--cloud cloud-a pop" data-depth="14"><svg viewBox="0 0 160 70" width="150" height="66"><path d="M30 50 a22 22 0 0 1 0-44 a26 26 0 0 1 48-6 a24 24 0 0 1 44 14 a20 20 0 0 1-6 36 Z" fill="#ffffff" stroke="#2c2350" stroke-width="2.5"/></svg></div>
<div class="layer layer--cloud cloud-b pop" data-depth="22"><svg viewBox="0 0 130 60" width="110" height="50"><path d="M26 44 a18 18 0 0 1 0-36 a22 22 0 0 1 40-4 a20 20 0 0 1 36 12 a16 16 0 0 1-6 30 Z" fill="#ffffff" stroke="#2c2350" stroke-width="2.5"/></svg></div>
<!-- stand-up paper hills -->
<div class="layer layer--hill hill-back pop" data-depth="30"></div>
<div class="layer layer--hill hill-front pop" data-depth="46"></div>
<!-- a little fox character that springs up -->
<div class="layer layer--fox pop" data-depth="60">
<svg viewBox="0 0 140 150" width="124" height="134" role="img" aria-label="A friendly paper fox waving">
<ellipse cx="70" cy="138" rx="46" ry="9" fill="#2c2350" opacity=".15"/>
<path d="M70 60 C40 60 30 96 36 120 C40 138 100 138 104 120 C110 96 100 60 70 60 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="3"/>
<path d="M70 92 C56 92 50 110 54 124 C58 134 82 134 86 124 C90 110 84 92 70 92 Z" fill="#fff3e6" stroke="#2c2350" stroke-width="2.5"/>
<path d="M44 64 L30 30 L60 52 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="3" stroke-linejoin="round"/>
<path d="M96 64 L110 30 L80 52 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="3" stroke-linejoin="round"/>
<path d="M48 60 L40 42 L54 52 Z" fill="#fff3e6"/>
<path d="M92 60 L100 42 L86 52 Z" fill="#fff3e6"/>
<circle cx="58" cy="80" r="5" fill="#2c2350"/>
<circle cx="82" cy="80" r="5" fill="#2c2350"/>
<circle cx="60" cy="78" r="1.6" fill="#fff"/>
<circle cx="84" cy="78" r="1.6" fill="#fff"/>
<path d="M65 92 q5 6 10 0" fill="none" stroke="#2c2350" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="70" cy="88" r="3.4" fill="#ff6f9c"/>
</svg>
</div>
<!-- pop-up grass tufts -->
<div class="layer layer--grass pop" data-depth="70"></div>
</div>
</div>
<div class="hero__copy">
<p class="kicker"><span class="kicker__dot"></span> A pop-up storybook</p>
<h1 id="hero-title">Open the page,<br /><span class="hl">watch Whirlwood spring to life.</span></h1>
<p class="lede">
Fold by fold, the forest stands up off the paper. Tip-top trees lean
forward, the river ripples, and little Fennec the fox waves hello —
all built from layered paper and shadows, no screens required.
</p>
<div class="hero__actions">
<a class="pill pill--solid pill--lg" href="#cta">Pre-order the book</a>
<button id="popAllBtn" class="pill pill--ghost pill--lg" type="button">
<span aria-hidden="true">✨</span> Pop it up
</button>
</div>
<ul class="hero__facts" aria-label="Book facts">
<li><strong>14</strong> pop-up spreads</li>
<li><strong>Ages 3–7</strong> bedtime read</li>
<li><strong>Sturdy</strong> board pages</li>
</ul>
</div>
</section>
<!-- ============ FEATURE PAGES THAT LIFT ============ -->
<section id="features" class="pages" aria-labelledby="pages-title">
<header class="section-head reveal">
<p class="kicker"><span class="kicker__dot"></span> Inside the book</p>
<h2 id="pages-title">Every page lifts as you turn it</h2>
<p class="section-sub">Scroll down — each spread folds up from the gutter, casting its own soft paper shadow.</p>
</header>
<ol class="page-stack" role="list">
<li class="page-card reveal" style="--tilt:-1.4deg;--tint:var(--secondary)">
<span class="page-card__fold" aria-hidden="true"></span>
<div class="page-card__art" aria-hidden="true">
<svg viewBox="0 0 120 120" width="84" height="84"><circle cx="60" cy="64" r="40" fill="#5ec5d6"/><path d="M60 28 a18 18 0 0 0 0 36 a18 18 0 0 1 0-36" fill="#fff8ef"/><circle cx="46" cy="50" r="4" fill="#2c2350"/></svg>
</div>
<div class="page-card__body">
<span class="page-card__folio">Spread 02</span>
<h3>The Moonrise Meadow</h3>
<p>A glowing moon pops up over silver grass while fireflies drift between the layers on tiny paper springs.</p>
</div>
</li>
<li class="page-card reveal" style="--tilt:1.2deg;--tint:var(--green)">
<span class="page-card__fold" aria-hidden="true"></span>
<div class="page-card__art" aria-hidden="true">
<svg viewBox="0 0 120 120" width="84" height="84"><path d="M60 18 L92 96 L28 96 Z" fill="#7bd389"/><path d="M60 48 L78 96 L42 96 Z" fill="#5cb574"/><rect x="54" y="96" width="12" height="14" fill="#a9744f"/></svg>
</div>
<div class="page-card__body">
<span class="page-card__folio">Spread 05</span>
<h3>The Whispering Pines</h3>
<p>Pull the tab and the tallest pine bends to share a secret. Five stand-up trees layer into a deep paper forest.</p>
</div>
</li>
<li class="page-card reveal" style="--tilt:-0.8deg;--tint:var(--pink)">
<span class="page-card__fold" aria-hidden="true"></span>
<div class="page-card__art" aria-hidden="true">
<svg viewBox="0 0 120 120" width="84" height="84"><path d="M30 70 q30 -34 60 0 q-30 22 -60 0" fill="#ff6f9c"/><path d="M30 70 q30 28 60 0" fill="#ffb1c9"/><circle cx="60" cy="62" r="7" fill="#fff8ef"/></svg>
</div>
<div class="page-card__body">
<span class="page-card__folio">Spread 09</span>
<h3>The Rumbling River</h3>
<p>A ribbon of blue paper folds into rolling rapids. Tip the page and Fennec's little boat bobs along the bend.</p>
</div>
</li>
<li class="page-card reveal" style="--tilt:1.6deg;--tint:var(--accent)">
<span class="page-card__fold" aria-hidden="true"></span>
<div class="page-card__art" aria-hidden="true">
<svg viewBox="0 0 120 120" width="84" height="84"><rect x="30" y="40" width="60" height="50" rx="6" fill="#ffd23f"/><path d="M26 44 L60 18 L94 44 Z" fill="#ff8a3d"/><rect x="52" y="64" width="16" height="26" fill="#a9744f"/></svg>
</div>
<div class="page-card__body">
<span class="page-card__folio">Spread 12</span>
<h3>The Lantern Cottage</h3>
<p>The little house unfolds with a flap roof and a warm window. Press it flat, open it again — it pops up every time.</p>
</div>
</li>
</ol>
</section>
<!-- ============ SCENE STRIP ============ -->
<section id="scenes" class="scenes reveal" aria-labelledby="scenes-title">
<div class="scenes__inner">
<header class="section-head section-head--left">
<p class="kicker"><span class="kicker__dot"></span> Tactile by design</p>
<h2 id="scenes-title">Real paper engineering, drawn in shadow</h2>
<p class="section-sub">
Tip the book left and right to see how the layers separate — back hills,
standing trees, and the front meadow each catch their own light.
</p>
</header>
<div class="tilt-stage" id="tiltStage" tabindex="0" role="group"
aria-label="Interactive pop-up forest. Move pointer or use arrow keys to tilt.">
<div class="tilt-stage__hint" aria-hidden="true">↔ Tilt me</div>
<div class="tlayer t-sky"></div>
<div class="tlayer t-back" data-depth="10"></div>
<div class="tlayer t-mid" data-depth="26"></div>
<div class="tlayer t-fox" data-depth="44" aria-hidden="true">
<svg viewBox="0 0 100 110" width="78" height="86"><path d="M50 44 C30 44 24 70 28 88 C31 100 69 100 72 88 C76 70 70 44 50 44 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="2.5"/><path d="M34 48 L24 24 L44 40 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="2.5" stroke-linejoin="round"/><path d="M66 48 L76 24 L56 40 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="2.5" stroke-linejoin="round"/><circle cx="42" cy="58" r="3.6" fill="#2c2350"/><circle cx="58" cy="58" r="3.6" fill="#2c2350"/><circle cx="50" cy="66" r="2.6" fill="#ff6f9c"/></svg>
</div>
<div class="tlayer t-front" data-depth="64"></div>
</div>
</div>
</section>
<!-- ============ CTA CARD THAT SPRINGS ============ -->
<section id="cta" class="cta" aria-labelledby="cta-title">
<div class="cta__card reveal" id="ctaCard">
<span class="cta__pop" aria-hidden="true">pop!</span>
<div class="cta__book" aria-hidden="true">
<svg viewBox="0 0 120 140" width="120" height="140">
<rect x="14" y="12" width="92" height="120" rx="8" fill="#5ec5d6" stroke="#2c2350" stroke-width="3"/>
<rect x="22" y="20" width="76" height="104" rx="5" fill="#fff8ef" stroke="#2c2350" stroke-width="2"/>
<path d="M40 44 L80 44 M40 56 L80 56 M40 68 L70 68" stroke="#ff8a3d" stroke-width="3" stroke-linecap="round"/>
<path d="M50 88 a14 14 0 0 1 20 0 a14 14 0 0 1-20 0" fill="#ffd23f"/>
<text x="60" y="116" text-anchor="middle" font-family="Baloo 2, sans-serif" font-size="11" font-weight="800" fill="#2c2350">WHIRLWOOD</text>
</svg>
</div>
<div class="cta__copy">
<h2 id="cta-title">Bring Whirlwood home</h2>
<p>First-edition hardcover pop-up book, hand-folded board pages, and a fold-out forest poster tucked in the back.</p>
<form class="cta__form" id="ctaForm" novalidate>
<label class="sr-only" for="email">Email for launch news</label>
<input id="email" name="email" type="email" inputmode="email"
placeholder="grown-up@email.com" autocomplete="email" required />
<button class="pill pill--solid pill--lg" type="submit">Pre-order · $24</button>
</form>
<p class="cta__fine">Ships Whirlwood-ready. Free bookmark with every order. (Demo — nothing is charged.)</p>
</div>
</div>
</section>
</main>
<footer class="site-foot" role="contentinfo">
<p>Pop-Up Pages — a fictional pop-up book studio. Illustrative kids' UI only.</p>
<button id="topBtn" class="pill pill--ghost" type="button">Back to the cover ↑</button>
</footer>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Pop-Up Book Landing
A landing page for the fictional pop-up book The Whirlwood Pop-Up Storybook, art-directed to feel like a real paper engineering spread. The hero is a thick paper page with a printed center spine and a ruled paper texture; inside it, a whole scene unfolds — a beaming sun, drifting clouds, two stand-up paper hills, a meadow of grass tufts and little Fennec the fox — each layer rotating up from flat with a bouncy standUp keyframe and casting its own offset drop-shadow. Moving the pointer over the page drives a gentle multi-depth parallax, and a Pop it up button replays the unfold with a colourful sparkle burst.
Below the fold, four feature spreads are stacked like board pages. Each one carries a perforated fold strip, a page folio chip, a CSS-drawn spot illustration and a slightly different tilt, and they lift into view on scroll — IntersectionObserver adds an in class that rotates them up from the gutter with a layered paper shadow. A tactile by design section adds an interactive forest stage: move the pointer (or press the arrow keys) and the sky, hills, fox and front meadow slide apart at different depths so you can see the paper layers separate. The page closes with a pre-order CTA card that springs in when it scrolls into view, validates an email inline, and confirms with a toast and sparkles.
Everything is keyboard reachable with visible focus rings, the tilt stage is a focusable group with an aria label, a skip link jumps to the content, and an easy-read font toggle swaps the body type and loosens spacing for easier reading. The layout collapses to a single readable column on phones and holds together down to 360px, and prefers-reduced-motion disables every pop, parallax, spring and sparkle.
Illustrative kids’ UI only — fictional stories, characters, and audio.