Storybook — Two-page Illustrated Spread
A polished two-page storybook spread for the fictional picture book Maple and the Moon-Boat, built entirely in HTML, CSS and vanilla JS. The left page is a full inline-SVG night-river scene with a paper boat, glowing moon, swaying lantern and drifting fireflies; the right page carries a large decorative drop-cap story column with a spot firefly-jar illustration. Ornamental vine page frames, page folios, gentle pointer and tilt parallax, a sparkle Read-it-again button, an easy-read font toggle, and a layout that stacks to one column on phones.
MCP
Code
:root {
--bg: #fff8ef;
--paper: #fffdf7;
--paper-edge: #f4e7d2;
--ink: #2c2350;
--ink-soft: #5a4f7a;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--r: 22px;
--r-lg: 30px;
--shadow: 0 18px 40px -18px rgba(44, 35, 80, 0.45);
--shadow-soft: 0 8px 20px -10px rgba(44, 35, 80, 0.35);
--display: "Baloo 2", system-ui, sans-serif;
--body: "Nunito", system-ui, sans-serif;
--pad: clamp(18px, 4vw, 40px);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--body);
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(1100px 700px at 15% -10%, #ffe6c7 0, transparent 55%),
radial-gradient(1000px 700px at 100% 110%, #d8f3f8 0, transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(16px, 4vw, 48px);
}
/* Dyslexia-friendly toggle (class on body) */
body.legible {
--body: "Verdana", "Trebuchet MS", system-ui, sans-serif;
letter-spacing: 0.02em;
word-spacing: 0.08em;
}
body.legible .prose {
line-height: 1.8;
}
.skip-link {
position: absolute;
left: 14px;
top: -60px;
background: var(--ink);
color: #fff8ef;
padding: 10px 16px;
border-radius: 999px;
font-weight: 700;
z-index: 20;
transition: top 0.2s ease;
}
.skip-link:focus {
top: 14px;
}
.stage {
position: relative;
width: min(1080px, 100%);
}
/* ---- Book header ---- */
.bookbar {
text-align: center;
margin-bottom: clamp(16px, 3vw, 26px);
}
.series {
margin: 0;
font-family: var(--display);
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
font-size: 0.72rem;
color: var(--primary);
}
#book-title {
margin: 4px 0 6px;
font-family: var(--display);
font-weight: 800;
font-size: clamp(1.9rem, 6vw, 3rem);
line-height: 1.05;
color: var(--ink);
text-shadow: 0 2px 0 #fff, 0 4px 14px rgba(255, 138, 61, 0.25);
}
.byline {
margin: 0;
color: var(--ink-soft);
font-weight: 600;
}
.byline em {
color: var(--pink);
font-style: normal;
}
/* ---- The open book ---- */
.book {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-radius: var(--r-lg);
background: var(--paper);
box-shadow: var(--shadow);
border: 3px solid var(--ink);
overflow: hidden;
perspective: 1400px;
}
.book::before {
/* outer book cover lip */
content: "";
position: absolute;
inset: -10px;
border-radius: calc(var(--r-lg) + 8px);
background: linear-gradient(135deg, var(--secondary), var(--green));
z-index: -1;
}
.spine {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 30px;
transform: translateX(-50%);
background: linear-gradient(
90deg,
rgba(44, 35, 80, 0) 0,
rgba(44, 35, 80, 0.16) 45%,
rgba(44, 35, 80, 0.22) 50%,
rgba(44, 35, 80, 0.16) 55%,
rgba(44, 35, 80, 0) 100%
);
z-index: 4;
pointer-events: none;
}
.page {
position: relative;
padding: clamp(20px, 3.4vw, 38px);
background:
radial-gradient(circle at 50% 0, #fff, transparent 60%),
repeating-linear-gradient(0deg, transparent 0 28px, rgba(244, 231, 210, 0.5) 28px 29px),
var(--paper);
min-height: clamp(420px, 60vh, 560px);
}
.page-left {
border-right: 1px solid var(--paper-edge);
box-shadow: inset -22px 0 30px -24px rgba(44, 35, 80, 0.4);
}
.page-right {
box-shadow: inset 22px 0 30px -24px rgba(44, 35, 80, 0.4);
}
.page-right:focus-visible {
outline: none;
}
/* Ornamental vine frame */
.ornament {
position: absolute;
inset: 12px;
color: var(--green);
pointer-events: none;
z-index: 1;
}
.ornament svg {
width: 100%;
height: 100%;
display: block;
}
.page-left .ornament {
color: var(--accent);
}
/* ---- Left page: illustration ---- */
.scene {
position: relative;
z-index: 2;
height: 100%;
display: grid;
place-items: center;
padding: clamp(8px, 2vw, 18px);
}
.scene-svg {
width: 100%;
max-width: 460px;
aspect-ratio: 1 / 1;
border-radius: 20px;
box-shadow: var(--shadow-soft);
display: block;
}
.layer {
will-change: transform;
transition: transform 0.18s ease-out;
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.float {
animation: bob 5s ease-in-out infinite;
}
.boat {
animation: bob 4s ease-in-out infinite;
}
.lantern {
transform-origin: 252px 296px;
animation: sway 3.4s ease-in-out infinite;
}
@keyframes sway {
0%,
100% {
transform: rotate(-3deg);
}
50% {
transform: rotate(3deg);
}
}
.firefly {
fill: var(--accent);
filter: drop-shadow(0 0 4px rgba(255, 210, 63, 0.9));
}
/* ---- Right page: story prose ---- */
.prose {
position: relative;
z-index: 2;
max-width: 46ch;
margin: 0 auto;
padding: clamp(6px, 1.6vw, 16px) clamp(4px, 1.4vw, 14px);
}
.chapter {
font-family: var(--display);
font-weight: 700;
font-size: clamp(1.3rem, 3.4vw, 1.85rem);
margin: 0 0 0.5em;
color: var(--secondary);
text-align: center;
letter-spacing: 0.01em;
}
.chapter::after {
content: "\273B";
display: block;
color: var(--pink);
font-size: 0.9rem;
margin-top: 0.1em;
}
.prose p {
margin: 0 0 1em;
font-size: clamp(1rem, 2.2vw, 1.12rem);
color: var(--ink);
}
.dropcap::first-letter {
font-family: var(--display);
font-weight: 800;
float: left;
font-size: 3.4em;
line-height: 0.72;
padding: 0.06em 0.12em 0 0;
margin-right: 0.06em;
color: var(--primary);
text-shadow: 0 3px 0 #fff, 0 5px 0 rgba(255, 138, 61, 0.25);
}
.spot {
float: right;
width: clamp(72px, 18vw, 104px);
margin: 0 0 0.4em 0.8em;
shape-outside: circle(50%);
animation: bob 6s ease-in-out infinite;
}
.spot svg {
width: 100%;
display: block;
}
/* ---- Read again button ---- */
.turn-cue {
text-align: center;
margin-top: 0.4em;
}
.again-btn {
display: inline-flex;
align-items: center;
gap: 0.5em;
min-height: 48px;
padding: 0 22px;
font-family: var(--display);
font-weight: 700;
font-size: 1rem;
color: var(--ink);
background: var(--accent);
border: 3px solid var(--ink);
border-radius: 999px;
box-shadow: 0 5px 0 var(--ink);
cursor: pointer;
transition:
transform 0.12s ease,
box-shadow 0.12s ease,
background 0.2s ease;
}
.again-btn:hover {
background: #ffdd6b;
transform: translateY(-1px);
}
.again-btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 var(--ink);
}
.again-btn:focus-visible {
outline: 3px solid var(--pink);
outline-offset: 3px;
}
.again-spark {
display: inline-block;
animation: twinkle 2.4s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
transform: scale(1) rotate(0);
opacity: 1;
}
50% {
transform: scale(1.3) rotate(18deg);
opacity: 0.7;
}
}
/* Re-read pop on prose */
@keyframes pop-in {
0% {
transform: translateY(10px) scale(0.98);
opacity: 0;
}
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
.prose.replay > * {
animation: pop-in 0.5s ease both;
}
.prose.replay > *:nth-child(2) {
animation-delay: 0.08s;
}
.prose.replay > *:nth-child(3) {
animation-delay: 0.14s;
}
.prose.replay > *:nth-child(4) {
animation-delay: 0.2s;
}
.prose.replay > *:nth-child(5) {
animation-delay: 0.26s;
}
/* Sparkle burst (JS injected) */
.sparkle {
position: fixed;
width: 12px;
height: 12px;
pointer-events: none;
z-index: 30;
color: var(--accent);
animation: sparkle-fly 0.9s ease-out forwards;
}
.sparkle svg {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
filter: drop-shadow(0 0 4px rgba(255, 210, 63, 0.9));
}
@keyframes sparkle-fly {
0% {
transform: translate(-50%, -50%) scale(0.2) rotate(0);
opacity: 1;
}
100% {
transform: translate(var(--dx), var(--dy)) scale(1.1) rotate(160deg);
opacity: 0;
}
}
/* ---- Page folios ---- */
.folio {
position: absolute;
bottom: 16px;
font-family: var(--display);
font-weight: 700;
color: var(--ink-soft);
font-size: 0.95rem;
z-index: 3;
}
.folio-left {
left: 26px;
}
.folio-right {
right: 26px;
}
.caption {
text-align: center;
margin: 18px auto 0;
color: var(--ink-soft);
font-weight: 600;
font-size: 0.9rem;
max-width: 60ch;
}
/* ---- Dyslexia toggle control ---- */
.legible-toggle {
position: absolute;
top: 14px;
right: 14px;
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 6px 14px;
background: var(--paper);
border: 2px solid var(--ink);
border-radius: 999px;
font-weight: 700;
font-size: 0.82rem;
color: var(--ink);
cursor: pointer;
box-shadow: var(--shadow-soft);
z-index: 12;
}
.legible-toggle:focus-visible {
outline: 3px solid var(--secondary);
outline-offset: 2px;
}
.legible-toggle .dot {
width: 30px;
height: 18px;
border-radius: 999px;
background: #e3d8c4;
position: relative;
transition: background 0.2s ease;
flex: none;
}
.legible-toggle .dot::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
transition: transform 0.2s ease;
}
.legible-toggle[aria-pressed="true"] .dot {
background: var(--green);
}
.legible-toggle[aria-pressed="true"] .dot::after {
transform: translateX(12px);
}
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff8ef;
padding: 12px 20px;
border-radius: 999px;
font-weight: 700;
font-size: 0.92rem;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition:
transform 0.3s ease,
opacity 0.3s ease;
z-index: 40;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---- Responsive: stack to one column ---- */
@media (max-width: 720px) {
.book {
grid-template-columns: 1fr;
}
.spine {
display: none;
}
.page-left {
border-right: none;
border-bottom: 2px dashed var(--paper-edge);
box-shadow: none;
}
.page-right {
box-shadow: none;
}
.page {
min-height: auto;
}
}
@media (max-width: 400px) {
.legible-toggle {
position: static;
margin: 0 auto 14px;
}
.spot {
width: 84px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
.float,
.boat,
.lantern,
.spot,
.again-spark,
.prose.replay > * {
animation: none !important;
transition: none !important;
}
.layer {
transform: none !important;
}
}(function () {
"use strict";
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- tiny toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ---------- fireflies ---------- */
var SVGNS = "http://www.w3.org/2000/svg";
var fireflyGroup = document.getElementById("fireflies");
var flies = [];
if (fireflyGroup) {
var seeds = [
[120, 150, 2.4],
[200, 110, 1.8],
[270, 170, 2.1],
[90, 200, 1.6],
[310, 130, 2.0],
[170, 90, 1.5],
[240, 210, 2.2],
];
seeds.forEach(function (s, i) {
var c = document.createElementNS(SVGNS, "circle");
c.setAttribute("class", "firefly");
c.setAttribute("cx", s[0]);
c.setAttribute("cy", s[1]);
c.setAttribute("r", s[2]);
fireflyGroup.appendChild(c);
flies.push({
el: c,
bx: s[0],
by: s[1],
ax: 6 + (i % 3) * 4, // wander amplitude
ay: 5 + (i % 2) * 4,
phase: i * 1.1,
speed: 0.6 + (i % 3) * 0.18,
});
});
}
var start = performance.now();
function animateFlies(now) {
var t = (now - start) / 1000;
for (var i = 0; i < flies.length; i++) {
var f = flies[i];
var x = f.bx + Math.sin(t * f.speed + f.phase) * f.ax;
var y = f.by + Math.cos(t * f.speed * 0.8 + f.phase) * f.ay;
f.el.setAttribute("cx", x.toFixed(2));
f.el.setAttribute("cy", y.toFixed(2));
f.el.style.opacity = (
0.45 +
0.55 * Math.abs(Math.sin(t * 1.4 + f.phase))
).toFixed(2);
}
rafId = requestAnimationFrame(animateFlies);
}
var rafId;
if (!reduceMotion && flies.length) {
rafId = requestAnimationFrame(animateFlies);
}
/* ---------- parallax / float on scene ---------- */
var scene = document.getElementById("scene");
var layers = scene ? scene.querySelectorAll(".layer") : [];
var targetX = 0,
targetY = 0,
curX = 0,
curY = 0;
function applyParallax() {
curX += (targetX - curX) * 0.08;
curY += (targetY - curY) * 0.08;
for (var i = 0; i < layers.length; i++) {
var depth = parseFloat(layers[i].getAttribute("data-depth")) || 0;
var k = depth / 40;
layers[i].style.transform =
"translate(" +
(curX * k * 18).toFixed(2) +
"px," +
(curY * k * 14).toFixed(2) +
"px)";
}
parallaxRaf = requestAnimationFrame(applyParallax);
}
var parallaxRaf;
if (scene && layers.length && !reduceMotion) {
parallaxRaf = requestAnimationFrame(applyParallax);
scene.addEventListener("pointermove", function (e) {
var r = scene.getBoundingClientRect();
targetX = (e.clientX - r.left) / r.width - 0.5;
targetY = (e.clientY - r.top) / r.height - 0.5;
});
scene.addEventListener("pointerleave", function () {
targetX = 0;
targetY = 0;
});
// device tilt parallax (mobile)
window.addEventListener(
"deviceorientation",
function (e) {
if (e.gamma == null || e.beta == null) return;
targetX = Math.max(-1, Math.min(1, e.gamma / 35));
targetY = Math.max(-1, Math.min(1, (e.beta - 45) / 35));
},
true
);
}
/* ---------- "Read it again" sparkle button ---------- */
var againBtn = document.getElementById("again");
var prose = document.getElementById("prose");
function sparkleBurst(x, y) {
if (reduceMotion) return;
var n = 12;
for (var i = 0; i < n; i++) {
var s = document.createElement("div");
s.className = "sparkle";
var ang = (Math.PI * 2 * i) / n + Math.random() * 0.4;
var dist = 40 + Math.random() * 60;
s.style.left = x + "px";
s.style.top = y + "px";
s.style.setProperty(
"--dx",
(Math.cos(ang) * dist).toFixed(1) + "px"
);
s.style.setProperty(
"--dy",
(Math.sin(ang) * dist).toFixed(1) + "px"
);
s.innerHTML =
'<svg viewBox="0 0 24 24"><path d="M12 0l2.6 8.4L23 11l-8.4 2.6L12 22l-2.6-8.4L1 11l8.4-2.6z"/></svg>';
document.body.appendChild(s);
(function (node) {
setTimeout(function () {
node.remove();
}, 950);
})(s);
}
}
if (againBtn && prose) {
againBtn.addEventListener("click", function () {
var r = againBtn.getBoundingClientRect();
sparkleBurst(r.left + r.width / 2, r.top + r.height / 2);
// replay the page-turn pop
prose.classList.remove("replay");
// force reflow so the animation restarts
void prose.offsetWidth;
prose.classList.add("replay");
// scroll the story page into view (mobile / stacked)
var story = document.getElementById("story");
if (story && typeof story.scrollIntoView === "function") {
story.scrollIntoView({
behavior: reduceMotion ? "auto" : "smooth",
block: "nearest",
});
}
toast("Once more, from the top ✨");
});
prose.addEventListener("animationend", function () {
prose.classList.remove("replay");
});
}
/* ---------- dyslexia-friendly font toggle ---------- */
var legibleBtn = document.getElementById("legible");
if (legibleBtn) {
legibleBtn.addEventListener("click", function () {
var on = document.body.classList.toggle("legible");
legibleBtn.setAttribute("aria-pressed", on ? "true" : "false");
toast(on ? "Easy-read font on" : "Easy-read font off");
});
}
/* ---------- pause heavy loops when tab hidden ---------- */
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
if (rafId) cancelAnimationFrame(rafId);
if (parallaxRaf) cancelAnimationFrame(parallaxRaf);
} else if (!reduceMotion) {
if (flies.length) rafId = requestAnimationFrame(animateFlies);
if (scene && layers.length)
parallaxRaf = requestAnimationFrame(applyParallax);
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Storybook — Two-page Illustrated Spread</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;1,400&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#story">Skip to story</a>
<main class="stage" aria-labelledby="book-title">
<button
id="legible"
class="legible-toggle"
type="button"
aria-pressed="false"
>
<span class="dot" aria-hidden="true"></span>
Easy-read font
</button>
<header class="bookbar">
<p class="series">The Little Lantern Library</p>
<h1 id="book-title">Maple & the Moon-Boat</h1>
<p class="byline">Chapter Three — <em>A Light on the Water</em></p>
</header>
<!-- The open book: two facing pages -->
<article class="book" aria-label="Open storybook spread">
<div class="spine" aria-hidden="true"></div>
<!-- LEFT PAGE — full illustration scene -->
<section class="page page-left" aria-label="Illustration: the moon-boat on the river">
<div class="ornament" aria-hidden="true">
<svg viewBox="0 0 600 600" preserveAspectRatio="none">
<defs>
<pattern id="vine" width="60" height="60" patternUnits="userSpaceOnUse">
<path d="M30 0 q14 14 0 30 q-14 16 0 30" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<circle cx="42" cy="15" r="3.4" fill="currentColor" opacity="0.55"/>
<circle cx="18" cy="45" r="3.4" fill="currentColor" opacity="0.55"/>
</pattern>
</defs>
<rect x="6" y="6" width="588" height="588" rx="26" fill="none" stroke="url(#vine)" stroke-width="22"/>
</svg>
</div>
<div class="scene" id="scene">
<svg class="scene-svg" viewBox="0 0 400 400" role="img" aria-label="A moonlit river at night with a paper boat carrying a small fox, fireflies drifting overhead and reeds along the bank">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a2f6e"/>
<stop offset="0.55" stop-color="#5b4fb0"/>
<stop offset="1" stop-color="#8d7fe0"/>
</linearGradient>
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5ec5d6"/>
<stop offset="1" stop-color="#2f7e96"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#ffd23f" stop-opacity="0.95"/>
<stop offset="1" stop-color="#ffd23f" stop-opacity="0"/>
</radialGradient>
<clipPath id="frame"><rect x="0" y="0" width="400" height="400" rx="20"/></clipPath>
</defs>
<g clip-path="url(#frame)">
<!-- sky -->
<rect width="400" height="280" fill="url(#sky)"/>
<!-- stars -->
<g fill="#fff8ef" class="layer" data-depth="6">
<circle cx="48" cy="40" r="1.6"/><circle cx="120" cy="28" r="1.2"/>
<circle cx="190" cy="52" r="1.8"/><circle cx="300" cy="34" r="1.3"/>
<circle cx="356" cy="66" r="1.6"/><circle cx="84" cy="92" r="1.1"/>
<circle cx="262" cy="86" r="1.4"/><circle cx="338" cy="118" r="1.2"/>
</g>
<!-- moon -->
<g class="layer float" data-depth="12">
<circle cx="312" cy="80" r="46" fill="url(#glow)"/>
<circle cx="312" cy="80" r="30" fill="#fff3c9"/>
<circle cx="324" cy="72" r="30" fill="#5b4fb0" opacity="0.35"/>
</g>
<!-- distant hills -->
<path d="M0 280 Q70 220 150 252 T300 240 T400 262 L400 300 L0 300 Z" fill="#4a3f8c" opacity="0.7"/>
<path d="M0 290 Q110 248 220 276 T400 270 L400 320 L0 320 Z" fill="#3a2f6e" opacity="0.6"/>
<!-- water -->
<rect y="280" width="400" height="120" fill="url(#water)"/>
<!-- moon reflection -->
<g class="float" data-depth="4" opacity="0.55">
<ellipse cx="312" cy="316" rx="22" ry="6" fill="#fff3c9"/>
<ellipse cx="312" cy="336" rx="30" ry="5" fill="#fff3c9" opacity="0.6"/>
<ellipse cx="312" cy="356" rx="38" ry="5" fill="#fff3c9" opacity="0.4"/>
</g>
<!-- ripple lines -->
<g stroke="#bdeef6" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.55">
<path d="M40 312 q12 -6 24 0 t24 0"/>
<path d="M150 348 q12 -6 24 0 t24 0"/>
<path d="M250 376 q12 -6 24 0 t24 0"/>
</g>
<!-- paper boat with fox -->
<g class="layer boat" data-depth="20">
<!-- boat -->
<path d="M150 330 L250 330 L232 364 L168 364 Z" fill="#fff8ef" stroke="#2c2350" stroke-width="3" stroke-linejoin="round"/>
<path d="M200 330 L200 282 L246 320 Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="3" stroke-linejoin="round"/>
<path d="M150 330 L250 330" stroke="#2c2350" stroke-width="3"/>
<!-- fox -->
<g transform="translate(176 300)">
<ellipse cx="14" cy="24" rx="13" ry="10" fill="#ff8a3d"/>
<circle cx="14" cy="10" r="10" fill="#ff8a3d"/>
<path d="M5 3 L9 -6 L13 2 Z" fill="#ff8a3d"/>
<path d="M23 3 L19 -6 L15 2 Z" fill="#ff8a3d"/>
<path d="M6 11 q8 7 16 0 q-8 4 -16 0 Z" fill="#fff8ef"/>
<circle cx="10" cy="9" r="1.7" fill="#2c2350"/>
<circle cx="18" cy="9" r="1.7" fill="#2c2350"/>
<circle cx="14" cy="13" r="1.6" fill="#2c2350"/>
</g>
<!-- lantern on a stick -->
<g class="lantern">
<line x1="232" y1="330" x2="252" y2="296" stroke="#2c2350" stroke-width="3" stroke-linecap="round"/>
<circle cx="256" cy="292" r="34" fill="url(#glow)"/>
<rect x="248" y="284" width="16" height="18" rx="5" fill="#ffd23f" stroke="#2c2350" stroke-width="2.5"/>
<line x1="248" y1="290" x2="264" y2="290" stroke="#2c2350" stroke-width="1.5"/>
</g>
</g>
<!-- reeds foreground -->
<g class="layer reeds" data-depth="28" stroke="#3f7d4a" stroke-width="5" stroke-linecap="round" fill="none">
<path d="M20 400 q-6 -50 4 -78"/>
<path d="M34 400 q6 -42 -2 -66"/>
<path d="M386 400 q6 -54 -6 -84"/>
<path d="M372 400 q-6 -40 2 -62"/>
</g>
<g fill="#7bd389">
<ellipse cx="24" cy="320" rx="4" ry="9"/>
<ellipse cx="380" cy="314" rx="4" ry="9"/>
</g>
<!-- fireflies (animated by JS) -->
<g id="fireflies" class="layer" data-depth="34"></g>
</g>
</svg>
</div>
<p class="folio folio-left" aria-hidden="true">22</p>
</section>
<!-- RIGHT PAGE — story text -->
<section class="page page-right" id="story" aria-label="Story text" tabindex="-1">
<div class="ornament" aria-hidden="true">
<svg viewBox="0 0 600 600" preserveAspectRatio="none">
<rect x="6" y="6" width="588" height="588" rx="26" fill="none" stroke="url(#vine)" stroke-width="22"/>
</svg>
</div>
<div class="prose" id="prose">
<h2 class="chapter">A Light on the Water</h2>
<p class="dropcap">
Maple had never sailed at night before, and the river was wider
than any she had crossed in daylight. She tucked her paws beneath
her tail and watched the moon balance on the very tip of her little
paper boat, as if it might tumble in and float away like a coin.
</p>
<div class="spot" aria-hidden="true">
<svg viewBox="0 0 120 120" role="img" aria-label="A small jar of fireflies">
<ellipse cx="60" cy="108" rx="34" ry="6" fill="#2c2350" opacity="0.12"/>
<path d="M40 44 h40 v44 a20 20 0 0 1 -40 0 Z" fill="#bdeef6" stroke="#2c2350" stroke-width="3"/>
<rect x="36" y="36" width="48" height="10" rx="4" fill="#ff8a3d" stroke="#2c2350" stroke-width="3"/>
<circle cx="52" cy="70" r="4" fill="#ffd23f"/>
<circle cx="68" cy="62" r="3.4" fill="#ffd23f"/>
<circle cx="64" cy="82" r="3.4" fill="#ffd23f"/>
<circle cx="50" cy="86" r="2.8" fill="#ffd23f"/>
</svg>
</div>
<p>
“Don’t worry,” whispered the smallest firefly,
settling on the rim of her ear. “The moon knows the way home.
All we have to do is keep our lantern bright and follow where the
ripples shine.”
</p>
<p>
So Maple held the lantern a little higher, and one by one the
fireflies blinked awake along the reeds — a tiny golden trail
stitched across the dark water, leading her gently toward the far,
sleeping shore.
</p>
<p class="turn-cue">
<button id="again" class="again-btn" type="button">
<span class="again-spark" aria-hidden="true">✨</span>
Read it again
</button>
</p>
</div>
<p class="folio folio-right" aria-hidden="true">23</p>
</section>
</article>
<p class="caption">Tilt or move your cursor over the scene — the boat, moon and fireflies drift along.</p>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Two-page Illustrated Spread
A single open-book spread from the fictional picture book Maple & the Moon-Boat, art-directed like a page you could turn in a bedtime story. The book sits on a soft cover lip with a faint center spine and inked page edges, and each page wears an ornamental vine frame and a printed page folio. The left page is one full inline-SVG illustration — a moonlit river where a little fox sails a paper boat, a swaying paper lantern glows over the water, hills and reeds layer into the distance, and fireflies drift across the dark. The right page opens the chapter with a large decorative drop-cap, a flowing column of story text, and a small spot illustration of a firefly jar that the paragraphs wrap around.
The scene is alive. Moving the cursor over the illustration (or tilting a phone) drives a gentle multi-layer parallax — the boat, moon, reeds and fireflies shift at different depths — while the moon bobs, the lantern sways, and the fireflies wander and twinkle on their own animation loop. The Read it again button fires a radial sparkle burst, replays the page with a soft pop-in cascade, scrolls the story back into view on small screens, and confirms with a toast. An easy-read font toggle swaps the body to a dyslexia-friendlier typeface with looser spacing.
Everything is keyboard reachable with visible focus rings, the scene carries descriptive SVG role="img" labels, a skip link jumps to the story, and prefers-reduced-motion disables parallax, float and sparkles. Below 720px the two pages stack into a single readable column; the design holds together down to 360px.
Illustrative kids’ UI only — fictional stories, characters, and audio.