Storybook — Classic Fairytale Landing
A cream-and-gold fairytale storybook landing with an illustrated castle-and-forest SVG hero, a glowing once-upon-a-time headline, and featured tale covers you can queue for bedtime. A working read-aloud strip highlights each word as a cosy narrator plays, with adjustable speed and selectable voices, plus an Easy-Read accessibility toggle, parent testimonials, ornamental gold dividers, gentle float animations, and a validated trial-signup call to action.
MCP
Code
/* ====================================================================
Once Upon a Tale — Classic Fairytale Storybook Landing
Cream + gold + storybook red · ornate serif display
==================================================================== */
:root {
/* palette */
--bg: #fdf6e6;
--bg-warm: #fbeed3;
--panel: #fffaf0;
--ink: #3a2a16;
--ink-soft: #6b5638;
--cream: #fff7e4;
--gold: #c9962e;
--gold-bright: #e9b94f;
--gold-pale: #f3e2b0;
--red: #b23a57;
--red-deep: #8e2a44;
--rose: #ff6f9c;
--teal: #5ec5d6;
--green: #7bd389;
/* type */
--display: "Cinzel", Georgia, "Times New Roman", serif;
--serif: "Cormorant Garamond", Georgia, serif;
--body: "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif;
/* shape */
--r: 22px;
--r-lg: 30px;
--r-sm: 14px;
/* shadow */
--shadow-sm: 0 4px 14px rgba(90, 61, 18, .12);
--shadow-md: 0 14px 34px rgba(90, 61, 18, .16);
--shadow-gold: 0 0 0 2px var(--gold-pale), 0 12px 28px rgba(201, 150, 46, .22);
--maxw: 1120px;
--ease: cubic-bezier(.34, 1.56, .64, 1);
}
/* ---- reset ---- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--body);
font-size: clamp(16px, 1.05vw + 14px, 18px);
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(900px 500px at 85% -10%, #fff2d4 0%, transparent 60%),
radial-gradient(800px 500px at -5% 10%, #ffe9d6 0%, transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; color: inherit; }
.shell { width: min(100% - 2.4rem, var(--maxw)); margin-inline: auto; }
.vis-hidden {
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: 50%; top: -60px; transform: translateX(-50%);
background: var(--ink); color: var(--cream); padding: .6rem 1.2rem;
border-radius: 0 0 var(--r-sm) var(--r-sm); z-index: 999; transition: top .2s;
}
.skip-link:focus { top: 0; }
:focus-visible {
outline: 3px solid var(--red);
outline-offset: 3px;
border-radius: 6px;
}
/* ---- buttons ---- */
.pill-btn {
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
min-height: 48px; padding: .7rem 1.5rem;
border-radius: 999px; border: 2px solid transparent;
font-family: var(--body); font-weight: 800; font-size: 1rem;
cursor: pointer; transition: transform .18s var(--ease), box-shadow .2s, background .2s;
letter-spacing: .01em;
}
.pill-primary {
background: linear-gradient(180deg, #ffd266, var(--gold));
color: #4a330f; border-color: #b07f24;
box-shadow: var(--shadow-sm);
}
.pill-primary:hover { transform: translateY(-2px) scale(1.02); box-shadow: var(--shadow-md); }
.pill-primary:active { transform: translateY(0) scale(.98); }
.pill-ghost {
background: var(--panel); color: var(--ink);
border-color: var(--gold-pale);
box-shadow: var(--shadow-sm);
}
.pill-ghost:hover { transform: translateY(-2px); border-color: var(--gold); }
.pill-ghost:active { transform: translateY(0); }
.ghost-btn {
display: inline-flex; align-items: center; gap: .4rem;
min-height: 44px; padding: .4rem .9rem;
background: transparent; border: 2px solid var(--gold-pale);
border-radius: 999px; font-weight: 700; cursor: pointer;
transition: background .2s, border-color .2s, transform .18s var(--ease);
}
.ghost-btn:hover { background: var(--cream); border-color: var(--gold); transform: translateY(-1px); }
.ghost-btn[aria-pressed="true"] {
background: linear-gradient(180deg, #ffe9a8, var(--gold-bright));
border-color: var(--gold); color: #4a330f;
}
/* ====================================================================
HEADER
==================================================================== */
.site-header {
position: sticky; top: 0; z-index: 50;
background: rgba(253, 246, 230, .86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--gold-pale);
}
.header-inner {
display: flex; align-items: center; gap: 1rem;
padding-block: .7rem; flex-wrap: wrap;
}
.brand { display: inline-flex; align-items: center; gap: .65rem; margin-right: auto; }
.brand-crest { filter: drop-shadow(0 2px 4px rgba(138, 98, 18, .35)); }
.brand-crest svg { animation: gentleSpin 14s ease-in-out infinite; }
.brand-text { display: flex; flex-direction: column; line-height: 1; }
.brand-line1 { font-family: var(--serif); font-style: italic; color: var(--ink-soft); font-size: .85rem; }
.brand-line2 { font-family: var(--display); font-weight: 700; color: var(--red-deep); font-size: 1.25rem; letter-spacing: .02em; }
.site-nav { display: flex; gap: .35rem; }
.site-nav a {
padding: .5rem .85rem; border-radius: 999px; font-weight: 700; color: var(--ink-soft);
transition: background .2s, color .2s;
}
.site-nav a:hover { background: var(--cream); color: var(--red-deep); }
.header-actions { display: flex; align-items: center; gap: .6rem; }
.dyslexia-label { font-size: .9rem; }
/* ====================================================================
HERO
==================================================================== */
.hero { position: relative; padding-top: clamp(1.5rem, 4vw, 3rem); }
.hero-inner {
display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(1.5rem, 4vw, 3.5rem);
align-items: center; padding-bottom: clamp(2rem, 5vw, 3.5rem);
}
.kicker {
display: inline-flex; align-items: center; gap: .5rem;
font-family: var(--serif); font-style: italic; font-size: 1.15rem;
color: var(--gold); font-weight: 600;
background: var(--cream); padding: .35rem 1rem; border-radius: 999px;
border: 1px solid var(--gold-pale); box-shadow: var(--shadow-sm);
}
.flourish-mini { color: var(--red); }
h1#hero-title { margin-top: 1rem; }
.once {
display: block; font-family: var(--serif); font-style: italic;
font-size: clamp(1.6rem, 3.5vw, 2.3rem); color: var(--red);
}
.big {
display: block; font-family: var(--display); font-weight: 700;
font-size: clamp(2.1rem, 5.2vw, 3.5rem); line-height: 1.08;
color: var(--ink); margin-top: .2rem; letter-spacing: -.01em;
}
.lede {
max-width: 46ch; margin-top: 1.1rem; color: var(--ink-soft);
font-size: 1.08rem;
}
.hero-cta { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 1.6rem; }
.big-cta { font-size: 1.05rem; padding: .85rem 1.8rem; }
.hero-stats {
list-style: none; display: flex; gap: clamp(1rem, 3vw, 2.2rem);
margin-top: 2rem; padding: 0; flex-wrap: wrap;
}
.hero-stats li { display: flex; flex-direction: column; }
.hero-stats strong {
font-family: var(--display); font-size: 1.5rem; color: var(--gold); line-height: 1.1;
}
.hero-stats span { font-size: .85rem; color: var(--ink-soft); font-weight: 700; }
/* hero scene */
.hero-scene { position: relative; }
.scene-svg {
width: 100%; height: auto; border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
border: 3px solid var(--gold-pale);
animation: floatScene 7s ease-in-out infinite;
}
.window { animation: glowPulse 3.2s ease-in-out infinite; transform-origin: center; }
.window:nth-of-type(2) { animation-delay: .8s; }
.window:nth-of-type(3) { animation-delay: 1.5s; }
.flag { animation: wave 2.4s ease-in-out infinite; transform-origin: left center; }
.owl { animation: owlBob 3.6s ease-in-out infinite; transform-origin: center; }
.twk { animation: twinkle 2.8s ease-in-out infinite; transform-origin: center; }
.twk:nth-child(2) { animation-delay: .6s; }
.twk:nth-child(3) { animation-delay: 1.1s; }
.twk:nth-child(4) { animation-delay: 1.7s; }
.scene-badge {
position: absolute; bottom: -14px; left: -14px;
display: inline-flex; align-items: center; gap: .45rem;
background: var(--panel); padding: .55rem 1rem;
border-radius: 999px; border: 2px solid var(--gold-pale);
box-shadow: var(--shadow-sm); font-weight: 800; color: var(--red-deep);
animation: floatScene 6s ease-in-out infinite reverse;
}
.badge-emoji { font-size: 1.3rem; }
/* floating ornaments */
.floaties { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.floaty {
position: absolute; color: var(--gold-bright); font-size: 1.4rem; opacity: .6;
animation: floatUp 9s ease-in-out infinite;
}
.floaty.f1 { left: 6%; top: 22%; }
.floaty.f2 { left: 22%; top: 64%; font-size: 1rem; animation-delay: 1.4s; color: var(--rose); }
.floaty.f3 { left: 50%; top: 12%; animation-delay: 2.6s; }
.floaty.f4 { left: 70%; top: 70%; animation-delay: 3.8s; color: var(--red); }
.floaty.f5 { left: 88%; top: 30%; font-size: 1.1rem; animation-delay: 1s; }
/* ornamental divider */
.divider {
display: flex; align-items: center; justify-content: center; gap: 1rem;
margin: clamp(1.5rem, 4vw, 2.6rem) auto; width: min(100% - 2.4rem, 640px);
}
.divider .rule {
height: 2px; flex: 1;
background: linear-gradient(90deg, transparent, var(--gold-pale), var(--gold), var(--gold-pale), transparent);
}
.divider .orn { color: var(--gold); font-size: 1.4rem; }
/* ====================================================================
SECTIONS (shared)
==================================================================== */
.section { padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.section-head { text-align: center; margin-bottom: clamp(1.6rem, 4vw, 2.6rem); }
.eyebrow {
font-family: var(--serif); font-style: italic; font-size: 1.2rem;
color: var(--red); font-weight: 600;
}
.eyebrow.gold { color: var(--gold); }
.section-head h2, .read-copy h2, .voices h2, .cta h2 {
font-family: var(--display); font-weight: 700;
font-size: clamp(1.8rem, 4vw, 2.6rem); color: var(--ink);
line-height: 1.12; margin-top: .25rem;
}
.section-sub { color: var(--ink-soft); max-width: 52ch; margin: .7rem auto 0; }
/* ====================================================================
FEATURED TALES
==================================================================== */
.tale-grid {
display: grid; gap: 1.2rem;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.tale-card {
position: relative; display: flex; flex-direction: column;
background: var(--panel); border-radius: var(--r);
border: 2px solid var(--gold-pale); overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform .2s var(--ease), box-shadow .2s, border-color .2s;
cursor: pointer; text-align: left;
}
.tale-card:hover, .tale-card:focus-visible {
transform: translateY(-6px); box-shadow: var(--shadow-gold); border-color: var(--gold);
}
.tale-cover {
position: relative; aspect-ratio: 4 / 3; display: grid; place-items: center;
color: #fff;
}
.tale-emoji { font-size: 3.4rem; filter: drop-shadow(0 4px 6px rgba(0,0,0,.25)); }
.tale-cover::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.28), transparent 60%);
}
.tale-ribbon {
position: absolute; top: .7rem; left: .7rem; z-index: 2;
background: var(--red); color: #fff; font-weight: 800; font-size: .72rem;
padding: .22rem .6rem; border-radius: 999px; letter-spacing: .04em;
text-transform: uppercase; box-shadow: var(--shadow-sm);
}
.tale-body { padding: .9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: .3rem; flex: 1; }
.tale-title {
font-family: var(--display); font-weight: 600; font-size: 1.1rem; color: var(--ink);
line-height: 1.2;
}
.tale-meta { display: flex; gap: .6rem; font-size: .82rem; color: var(--ink-soft); font-weight: 700; }
.tale-meta .dot { color: var(--gold); }
.tale-foot {
margin-top: auto; padding-top: .7rem;
display: flex; align-items: center; justify-content: space-between;
}
.tale-add {
border: 2px solid var(--gold-pale); background: var(--cream);
border-radius: 999px; min-height: 40px; padding: .35rem .9rem;
font-weight: 800; font-size: .85rem; cursor: pointer; color: var(--red-deep);
display: inline-flex; align-items: center; gap: .35rem;
transition: background .2s, border-color .2s, transform .15s var(--ease);
}
.tale-add:hover { background: var(--gold-pale); border-color: var(--gold); }
.tale-add[data-queued="true"] {
background: linear-gradient(180deg, #ffe9a8, var(--gold-bright));
border-color: var(--gold); color: #4a330f;
}
.tale-add.pop { animation: pop .35s var(--ease); }
.tale-rating { font-weight: 800; color: var(--gold); font-size: .9rem; }
.queue-line {
margin-top: 1.6rem; text-align: center; font-weight: 700; color: var(--ink-soft);
}
/* ====================================================================
READ-ALOUD
==================================================================== */
.read-aloud { background:
linear-gradient(180deg, transparent, rgba(255, 244, 214, .55) 30%, transparent);
}
.read-inner {
display: grid; grid-template-columns: 1fr 1.05fr; gap: clamp(1.5rem, 4vw, 3rem);
align-items: center;
}
.read-copy { text-align: left; }
.read-copy .section-sub { margin-left: 0; }
.feature-list { list-style: none; padding: 0; margin-top: 1.2rem; display: grid; gap: .6rem; }
.feature-list li { display: flex; align-items: center; gap: .6rem; font-weight: 700; }
.feature-list .tick { color: var(--gold); }
/* reader player */
.reader {
background: var(--panel); border-radius: var(--r-lg);
border: 3px solid var(--gold-pale); box-shadow: var(--shadow-md);
padding: clamp(1.2rem, 3vw, 1.8rem); margin: 0;
}
.reader-cap {
display: flex; align-items: center; justify-content: space-between; gap: .8rem;
flex-wrap: wrap; margin-bottom: 1rem;
}
.reader-tale { font-family: var(--display); font-weight: 600; font-size: 1.2rem; color: var(--red-deep); }
.reader-voice select {
border: 2px solid var(--gold-pale); border-radius: 999px;
padding: .4rem 1.8rem .4rem .9rem; background: var(--cream); font-weight: 700;
cursor: pointer; min-height: 40px;
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--gold) 50%), linear-gradient(135deg, var(--gold) 50%, transparent 50%);
background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.reader-text {
font-family: var(--serif); font-size: clamp(1.25rem, 2.4vw, 1.6rem);
line-height: 1.7; color: var(--ink); margin-bottom: 1.1rem;
}
.reader-text .word {
border-radius: 6px; padding: 0 .08em; transition: background .15s, color .15s;
}
.reader-text .word.active {
background: var(--gold-bright); color: #4a330f; box-shadow: 0 0 0 3px var(--gold-pale);
}
.reader-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.round-btn {
width: 48px; height: 48px; border-radius: 50%;
border: 2px solid var(--gold-pale); background: var(--cream);
display: grid; place-items: center; cursor: pointer; font-size: 1.1rem;
transition: transform .15s var(--ease), background .2s, border-color .2s;
}
.round-btn:hover { background: var(--gold-pale); border-color: var(--gold); }
.round-btn:active { transform: scale(.92); }
.round-btn.play {
width: 58px; height: 58px; font-size: 1.3rem;
background: linear-gradient(180deg, #ffd266, var(--gold)); color: #4a330f;
border-color: #b07f24; box-shadow: var(--shadow-sm);
}
.round-btn.play[aria-pressed="true"] {
background: linear-gradient(180deg, var(--red), var(--red-deep)); color: #fff; border-color: var(--red-deep);
}
.speed { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
.speed input[type="range"] { width: clamp(90px, 18vw, 150px); accent-color: var(--gold); cursor: pointer; }
.reader-bar {
height: 8px; border-radius: 999px; background: var(--bg-warm);
margin-top: 1.1rem; overflow: hidden; border: 1px solid var(--gold-pale);
}
.reader-progress {
display: block; height: 100%; width: 0%;
background: linear-gradient(90deg, var(--gold-bright), var(--gold));
border-radius: 999px; transition: width .25s linear;
}
/* ====================================================================
TESTIMONIALS
==================================================================== */
.quote-grid {
display: grid; gap: 1.2rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.quote-card {
position: relative; background: var(--panel); border-radius: var(--r);
border: 2px solid var(--gold-pale); padding: 1.6rem 1.4rem 1.3rem;
box-shadow: var(--shadow-sm); margin: 0;
transition: transform .2s var(--ease), box-shadow .2s;
}
.quote-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); }
.quote-mark {
font-family: var(--display); font-size: 3.4rem; line-height: .6; color: var(--gold-pale);
height: 1.4rem;
}
.quote-card blockquote {
font-family: var(--serif); font-size: 1.25rem; line-height: 1.5; color: var(--ink);
margin: .4rem 0 1.1rem;
}
.quote-card figcaption {
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .7rem;
}
.avatar {
width: 42px; height: 42px; border-radius: 50%;
display: grid; place-items: center; font-family: var(--display); font-weight: 700;
color: #4a330f; background: var(--a, var(--gold-bright));
box-shadow: inset 0 -3px 6px rgba(0,0,0,.12);
}
.quote-card figcaption strong { display: block; font-size: .95rem; }
.quote-card .meta { display: block; font-size: .8rem; color: var(--ink-soft); }
.stars { color: var(--gold); letter-spacing: 1px; font-size: .85rem; }
/* ====================================================================
CTA
==================================================================== */
.cta-card {
position: relative; overflow: hidden;
text-align: center; padding: clamp(2rem, 6vw, 3.4rem) clamp(1.2rem, 4vw, 3rem);
background:
radial-gradient(120% 130% at 50% -10%, #fff5da, transparent 60%),
linear-gradient(180deg, var(--cream), #fbeed0);
border-radius: var(--r-lg); border: 3px solid var(--gold-pale);
box-shadow: var(--shadow-md);
}
.cta-floaties .floaty { font-size: 1.2rem; }
.cta-crown { display: inline-block; font-size: 2.6rem; animation: owlBob 3.4s ease-in-out infinite; }
.cta-card h2 { margin-top: .4rem; }
.cta-card p { color: var(--ink-soft); max-width: 48ch; margin: .8rem auto 0; }
.cta-form {
display: flex; gap: .7rem; flex-wrap: wrap; justify-content: center;
margin: 1.6rem auto 0; max-width: 520px;
}
.cta-form input {
flex: 1 1 220px; min-height: 50px; padding: .7rem 1.2rem;
border-radius: 999px; border: 2px solid var(--gold-pale); background: var(--panel);
font-weight: 600;
}
.cta-form input::placeholder { color: #b8a47d; }
.cta-form input:focus-visible { outline-color: var(--gold); border-color: var(--gold); }
.cta-form input.invalid { border-color: var(--red); background: #fff0f2; }
.cta-fine { font-size: .9rem; margin-top: 1rem; font-weight: 700; }
.cta-fine.ok { color: var(--green); }
/* ====================================================================
FOOTER
==================================================================== */
.site-footer {
margin-top: 1rem; padding-block: 2rem;
background: linear-gradient(180deg, transparent, rgba(201,150,46,.08));
border-top: 1px solid var(--gold-pale);
}
.footer-inner { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; justify-content: space-between; }
.footer-brand { font-family: var(--display); font-weight: 700; color: var(--red-deep); font-size: 1.1rem; }
.footer-nav { display: flex; gap: .3rem; flex-wrap: wrap; }
.footer-nav a { padding: .4rem .7rem; border-radius: 999px; font-weight: 700; color: var(--ink-soft); }
.footer-nav a:hover { background: var(--cream); color: var(--red-deep); }
.footer-fine { width: 100%; text-align: center; font-size: .82rem; color: var(--ink-soft); }
/* ====================================================================
TOAST
==================================================================== */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 24px);
background: var(--ink); color: var(--cream);
padding: .8rem 1.3rem; border-radius: 999px; font-weight: 700;
box-shadow: var(--shadow-md); opacity: 0; pointer-events: none;
transition: opacity .25s, transform .3s var(--ease); z-index: 200;
border: 2px solid var(--gold);
max-width: calc(100% - 2rem); text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ====================================================================
EASY-READ (dyslexia-friendly) MODE
==================================================================== */
body.easy-read {
--display: "Nunito", system-ui, sans-serif;
--serif: "Nunito", system-ui, sans-serif;
letter-spacing: .03em; word-spacing: .12em; line-height: 1.75;
}
body.easy-read .once,
body.easy-read .eyebrow,
body.easy-read .kicker,
body.easy-read .reader-text,
body.easy-read .quote-card blockquote { font-style: normal; }
body.easy-read .reader-text { line-height: 2; }
/* ====================================================================
ANIMATIONS
==================================================================== */
@keyframes floatScene { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@keyframes floatUp {
0%,100% { transform: translateY(0) rotate(0); opacity: .55; }
50% { transform: translateY(-18px) rotate(12deg); opacity: .85; }
}
@keyframes glowPulse { 0%,100% { opacity: .65; } 50% { opacity: 1; } }
@keyframes wave { 0%,100% { transform: scaleX(1); } 50% { transform: scaleX(.6); } }
@keyframes owlBob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes twinkle { 0%,100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.1); } }
@keyframes gentleSpin { 0%,100% { transform: rotate(-6deg); } 50% { transform: rotate(6deg); } }
@keyframes pop { 0% { transform: scale(1); } 40% { transform: scale(1.14); } 100% { transform: scale(1); } }
/* ====================================================================
RESPONSIVE
==================================================================== */
@media (max-width: 920px) {
.hero-inner, .read-inner { grid-template-columns: 1fr; }
.hero-scene { order: -1; max-width: 460px; margin-inline: auto; }
.read-copy { text-align: center; }
.read-copy .section-sub, .feature-list { margin-inline: auto; }
.feature-list { max-width: 280px; }
.lede, .hero-stats { margin-inline: auto; }
.hero-copy { text-align: center; }
.hero-cta { justify-content: center; }
}
@media (max-width: 680px) {
.site-nav { display: none; }
.dyslexia-label { display: none; }
.hero-stats { justify-content: center; }
}
@media (max-width: 420px) {
.header-actions .pill-primary { padding: .6rem 1rem; font-size: .9rem; }
.reader-cap { flex-direction: column; align-items: flex-start; }
.speed { margin-left: 0; }
}
/* ====================================================================
REDUCED MOTION
==================================================================== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
scroll-behavior: auto !important;
}
}/* ====================================================================
Once Upon a Tale — interactions
Vanilla JS, no libs. Every interaction actually works.
==================================================================== */
(function () {
"use strict";
var prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- 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");
}, 2600);
}
/* ================================================================
FEATURED TALES — render cards, queue toggling
================================================================ */
var TALES = [
{ id: "fox", title: "The Lantern Fox", emoji: "🦊", g: "linear-gradient(160deg,#ff9a6c,#e0607c)", mins: 6, age: "4–7", rating: "4.9", ribbon: "New" },
{ id: "castle", title: "The Cloud Castle", emoji: "🏰", g: "linear-gradient(160deg,#7cc0e8,#5e8ad6)", mins: 8, age: "5–8", rating: "4.8", ribbon: "" },
{ id: "dragon", title: "Ember the Kind Dragon", emoji: "🐉", g: "linear-gradient(160deg,#ffb86c,#e0607c)", mins: 7, age: "4–7", rating: "5.0", ribbon: "Loved" },
{ id: "moon", title: "The Moon's Lullaby", emoji: "🌙", g: "linear-gradient(160deg,#8a7cd6,#5e5ad6)", mins: 5, age: "3–6", rating: "4.9", ribbon: "" },
{ id: "rose", title: "The Singing Rose", emoji: "🌹", g: "linear-gradient(160deg,#ff7fa8,#d6488a)", mins: 6, age: "5–8", rating: "4.7", ribbon: "" },
{ id: "swan", title: "Brave Little Swan", emoji: "🦢", g: "linear-gradient(160deg,#67c7c0,#3a8ed6)", mins: 9, age: "6–9", rating: "4.8", ribbon: "" },
{ id: "lamp", title: "The Wishing Lamp", emoji: "🪔", g: "linear-gradient(160deg,#ffcf6c,#d69a3a)", mins: 7, age: "5–8", rating: "4.9", ribbon: "" },
{ id: "forest", title: "The Whispering Wood", emoji: "🌲", g: "linear-gradient(160deg,#6cc28a,#3a9a64)", mins: 8, age: "6–9", rating: "4.6", ribbon: "" }
];
var queue = [];
var grid = document.getElementById("taleGrid");
var queueStatus = document.getElementById("queueStatus");
function renderTales() {
if (!grid) return;
var frag = document.createDocumentFragment();
TALES.forEach(function (t) {
var card = document.createElement("article");
card.className = "tale-card";
card.setAttribute("role", "listitem");
card.setAttribute("tabindex", "0");
card.setAttribute("aria-label", t.title + ", " + t.mins + " minute tale, ages " + t.age);
card.dataset.id = t.id;
card.innerHTML =
'<div class="tale-cover" style="background:' + t.g + '">' +
(t.ribbon ? '<span class="tale-ribbon">' + t.ribbon + "</span>" : "") +
'<span class="tale-emoji" aria-hidden="true">' + t.emoji + "</span>" +
"</div>" +
'<div class="tale-body">' +
'<h3 class="tale-title">' + t.title + "</h3>" +
'<p class="tale-meta">' +
"<span>⏱ " + t.mins + " min</span>" +
'<span class="dot">·</span>' +
"<span>Ages " + t.age + "</span>" +
"</p>" +
'<div class="tale-foot">' +
'<button class="tale-add" type="button" data-queued="false" aria-pressed="false">' +
'<span class="add-ico" aria-hidden="true">+</span><span class="add-txt">Queue</span>' +
"</button>" +
'<span class="tale-rating">★ ' + t.rating + "</span>" +
"</div>" +
"</div>";
frag.appendChild(card);
});
grid.appendChild(frag);
}
function findTale(id) {
for (var i = 0; i < TALES.length; i++) if (TALES[i].id === id) return TALES[i];
return null;
}
function toggleQueue(card, btn) {
var id = card.dataset.id;
var tale = findTale(id);
if (!tale) return;
var idx = queue.indexOf(id);
var addTxt = btn.querySelector(".add-txt");
var addIco = btn.querySelector(".add-ico");
if (idx === -1) {
queue.push(id);
btn.dataset.queued = "true";
btn.setAttribute("aria-pressed", "true");
if (addTxt) addTxt.textContent = "Queued";
if (addIco) addIco.textContent = "✓";
if (!prefersReduced) {
btn.classList.remove("pop");
void btn.offsetWidth; // reflow to restart animation
btn.classList.add("pop");
}
toast("“" + tale.title + "” added to tonight's queue 🌙");
} else {
queue.splice(idx, 1);
btn.dataset.queued = "false";
btn.setAttribute("aria-pressed", "false");
if (addTxt) addTxt.textContent = "Queue";
if (addIco) addIco.textContent = "+";
toast("Removed “" + tale.title + "” from the queue");
}
updateQueueStatus();
}
function updateQueueStatus() {
if (!queueStatus) return;
if (queue.length === 0) {
queueStatus.textContent = "No tales queued yet — choose a favourite to begin.";
return;
}
var totalMins = queue.reduce(function (sum, id) {
var t = findTale(id); return sum + (t ? t.mins : 0);
}, 0);
var names = queue.map(function (id) {
var t = findTale(id); return t ? t.title : "";
});
var label = queue.length === 1 ? "1 tale" : queue.length + " tales";
queueStatus.textContent =
label + " queued · about " + totalMins + " min of bedtime — " + names.join(", ");
}
if (grid) {
grid.addEventListener("click", function (e) {
var btn = e.target.closest(".tale-add");
var card = e.target.closest(".tale-card");
if (!card) return;
if (btn) {
e.stopPropagation();
toggleQueue(card, btn);
} else {
// tapping the cover/title loads it into the reader
loadTaleIntoReader(card.dataset.id);
}
});
// keyboard: Enter/Space on a focused card loads it into the reader
grid.addEventListener("keydown", function (e) {
if (e.target.classList && e.target.classList.contains("tale-card") &&
(e.key === "Enter" || e.key === " ")) {
e.preventDefault();
loadTaleIntoReader(e.target.dataset.id);
}
});
}
/* ================================================================
READ-ALOUD PLAYER — word highlighting + progress
================================================================ */
var STORIES = {
fox: "Once upon a frosty night, a little fox lit a lantern to guide lost travellers home.",
castle: "High on a hill of clouds there stood a castle made entirely of morning light.",
dragon: "Ember the dragon never breathed fire — only warm puffs that toasted marshmallows.",
moon: "The moon leaned close and hummed a lullaby until the whole valley fell asleep.",
rose: "In the royal garden bloomed a rose that sang to anyone who was feeling sad.",
swan: "The smallest swan was the bravest, and she taught the river not to be afraid.",
lamp: "Whoever polished the old lamp was granted not a wish, but a kinder heart.",
forest: "The whispering wood remembered every story ever told beneath its leaves."
};
var TALE_NAMES = {
fox: "The Lantern Fox", castle: "The Cloud Castle", dragon: "Ember the Kind Dragon",
moon: "The Moon's Lullaby", rose: "The Singing Rose", swan: "Brave Little Swan",
lamp: "The Wishing Lamp", forest: "The Whispering Wood"
};
var readerText = document.getElementById("readerText");
var readerTale = document.getElementById("readerTale");
var readerProgress = document.getElementById("readerProgress");
var playBtn = document.getElementById("playBtn");
var rewindBtn = document.getElementById("rewindBtn");
var speedRange = document.getElementById("speedRange");
var voiceSelect = document.getElementById("voiceSelect");
var words = [];
var wordIndex = 0;
var playing = false;
var timer = null;
function buildWords(text) {
if (!readerText) return;
readerText.innerHTML = "";
var parts = text.split(/\s+/);
words = [];
parts.forEach(function (w, i) {
var span = document.createElement("span");
span.className = "word";
span.textContent = w;
readerText.appendChild(span);
if (i < parts.length - 1) readerText.appendChild(document.createTextNode(" "));
words.push(span);
});
}
function loadTaleIntoReader(id) {
var text = STORIES[id];
if (!text) return;
stopPlayback();
buildWords(text);
if (readerTale) readerTale.textContent = TALE_NAMES[id] || "A Tale";
setActive(-1);
if (readerProgress) readerProgress.style.width = "0%";
wordIndex = 0;
toast("Loaded “" + (TALE_NAMES[id] || "tale") + "” into the read-aloud player");
// scroll the reader into view gently
var ra = document.getElementById("read-aloud");
if (ra) ra.scrollIntoView({ behavior: prefersReduced ? "auto" : "smooth", block: "center" });
}
function setActive(idx) {
words.forEach(function (w, i) {
w.classList.toggle("active", i === idx);
});
if (readerProgress && words.length) {
var pct = idx < 0 ? 0 : Math.round(((idx + 1) / words.length) * 100);
readerProgress.style.width = pct + "%";
}
}
function stepDelay() {
var speed = speedRange ? parseFloat(speedRange.value) : 1;
return Math.round(420 / speed); // base ~420ms per word
}
function scheduleNext() {
clearTimeout(timer);
if (!playing) return;
if (wordIndex >= words.length) {
finishPlayback();
return;
}
setActive(wordIndex);
wordIndex++;
timer = setTimeout(scheduleNext, stepDelay());
}
function startPlayback() {
if (!words.length) return;
if (wordIndex >= words.length) wordIndex = 0;
playing = true;
setPlayUI(true);
scheduleNext();
}
function stopPlayback() {
playing = false;
clearTimeout(timer);
setPlayUI(false);
}
function finishPlayback() {
playing = false;
clearTimeout(timer);
setPlayUI(false);
setActive(words.length - 1);
if (readerProgress) readerProgress.style.width = "100%";
wordIndex = words.length;
toast("The end. Sweet dreams 💤");
}
function setPlayUI(isPlaying) {
if (!playBtn) return;
playBtn.setAttribute("aria-pressed", String(isPlaying));
playBtn.setAttribute("aria-label", isPlaying ? "Pause read-aloud" : "Play read-aloud");
var ico = playBtn.querySelector(".play-icon");
if (ico) ico.textContent = isPlaying ? "❚❚" : "▶";
}
if (playBtn) {
playBtn.addEventListener("click", function () {
if (playing) stopPlayback();
else startPlayback();
});
}
if (rewindBtn) {
rewindBtn.addEventListener("click", function () {
stopPlayback();
wordIndex = 0;
setActive(-1);
if (readerProgress) readerProgress.style.width = "0%";
toast("Back to the first page");
});
}
if (speedRange) {
speedRange.addEventListener("input", function () {
// if currently playing, the next scheduled step picks up the new speed automatically
var v = parseFloat(speedRange.value);
speedRange.setAttribute("aria-valuetext", v.toFixed(1) + "× speed");
});
}
if (voiceSelect) {
voiceSelect.addEventListener("change", function () {
toast("Now narrated by " + voiceSelect.value);
});
}
// initialise reader from the markup-provided words
if (readerText) {
words = Array.prototype.slice.call(readerText.querySelectorAll(".word"));
}
/* ================================================================
EASY-READ (dyslexia-friendly) TOGGLE
================================================================ */
var dysBtn = document.querySelector(".dyslexia-toggle");
if (dysBtn) {
dysBtn.addEventListener("click", function () {
var on = document.body.classList.toggle("easy-read");
dysBtn.setAttribute("aria-pressed", String(on));
toast(on ? "Easy-Read mode on — friendlier font & spacing" : "Easy-Read mode off");
});
}
/* ================================================================
SURPRISE ME — random tale into reader
================================================================ */
var surpriseBtn = document.getElementById("surpriseBtn");
if (surpriseBtn) {
surpriseBtn.addEventListener("click", function () {
var t = TALES[Math.floor(Math.random() * TALES.length)];
loadTaleIntoReader(t.id);
});
}
/* ================================================================
CTA FORM — validation
================================================================ */
var ctaForm = document.getElementById("ctaForm");
var ctaEmail = document.getElementById("ctaEmail");
var ctaMsg = document.getElementById("ctaMsg");
function validEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}
if (ctaForm) {
ctaForm.addEventListener("submit", function (e) {
e.preventDefault();
var v = (ctaEmail.value || "").trim();
if (!validEmail(v)) {
ctaEmail.classList.add("invalid");
ctaEmail.focus();
if (ctaMsg) { ctaMsg.textContent = "Please enter a valid email to open the book."; ctaMsg.classList.remove("ok"); }
toast("Hmm — that email doesn't look right ✦");
return;
}
ctaEmail.classList.remove("invalid");
var first = v.split("@")[0];
if (ctaMsg) {
ctaMsg.textContent = "Welcome, " + first + "! Your storybook is opening — check your inbox.";
ctaMsg.classList.add("ok");
}
ctaForm.reset();
toast("Once upon a time… your trial begins! 📖✨");
});
ctaEmail.addEventListener("input", function () {
if (ctaEmail.classList.contains("invalid") && validEmail(ctaEmail.value.trim())) {
ctaEmail.classList.remove("invalid");
}
});
}
/* ================================================================
INIT
================================================================ */
renderTales();
updateQueueStatus();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Once Upon a Tale — Classic Fairytale 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=Cinzel:wght@500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to story</a>
<!-- ============ HEADER ============ -->
<header class="site-header" role="banner">
<div class="shell header-inner">
<a class="brand" href="#top" aria-label="Once Upon a Tale, home">
<span class="brand-crest" aria-hidden="true">
<svg viewBox="0 0 48 48" width="40" height="40">
<defs>
<linearGradient id="crestGold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffe39a"/>
<stop offset="1" stop-color="#c9962e"/>
</linearGradient>
</defs>
<path d="M24 3l5 6 7-2-1 8 7 4-6 6 3 8-8-1-3 8-4-6-4 6-3-8-8 1 3-8-6-6 7-4-1-8 7 2z" fill="url(#crestGold)" stroke="#8a6212" stroke-width="1"/>
<text x="24" y="29" text-anchor="middle" font-family="Cinzel, serif" font-size="14" font-weight="700" fill="#5a3d12">T</text>
</svg>
</span>
<span class="brand-text">
<span class="brand-line1">Once Upon</span>
<span class="brand-line2">a Tale</span>
</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="#tales">Tales</a>
<a href="#read-aloud">Read-Aloud</a>
<a href="#voices">Voices</a>
<a href="#cta">Join</a>
</nav>
<div class="header-actions">
<button class="ghost-btn dyslexia-toggle" type="button" aria-pressed="false">
<span aria-hidden="true">🔤</span>
<span class="dyslexia-label">Easy-Read</span>
</button>
<a class="pill-btn pill-primary" href="#cta">Open the Book</a>
</div>
</div>
</header>
<main id="main">
<span id="top"></span>
<!-- ============ HERO ============ -->
<section class="hero" aria-labelledby="hero-title">
<!-- floating ornaments -->
<div class="floaties" aria-hidden="true">
<span class="floaty f1">✦</span>
<span class="floaty f2">★</span>
<span class="floaty f3">✧</span>
<span class="floaty f4">❀</span>
<span class="floaty f5">✦</span>
</div>
<div class="shell hero-inner">
<div class="hero-copy">
<p class="kicker"><span class="flourish-mini" aria-hidden="true">❦</span> A library of timeless stories</p>
<h1 id="hero-title">
<span class="once">Once upon a time…</span>
<span class="big">every bedtime became a little adventure.</span>
</h1>
<p class="lede">
Wander into a cream-and-gold storybook of classic fairytales — castles, kind dragons,
and brave hearts. Read along, or let a gentle voice carry each tale while the pages
glow softly in the dark.
</p>
<div class="hero-cta">
<a class="pill-btn pill-primary big-cta" href="#tales">Begin the Adventure</a>
<button class="pill-btn pill-ghost" id="surpriseBtn" type="button">
<span aria-hidden="true">🎲</span> Surprise me
</button>
</div>
<ul class="hero-stats" aria-label="Storybook highlights">
<li><strong>240+</strong><span>classic tales</span></li>
<li><strong>12</strong><span>cosy voices</span></li>
<li><strong>4.9★</strong><span>parent loved</span></li>
</ul>
</div>
<!-- Illustrated castle & forest scene -->
<div class="hero-scene" role="img" aria-label="Illustration of a golden castle on a hill among a moonlit forest">
<svg viewBox="0 0 420 380" class="scene-svg" preserveAspectRatio="xMidYMid meet">
<defs>
<radialGradient id="moonGlow" cx="50%" cy="50%" r="50%">
<stop offset="0" stop-color="#fff6da"/>
<stop offset="0.55" stop-color="#ffe9a8"/>
<stop offset="1" stop-color="#ffe9a8" stop-opacity="0"/>
</radialGradient>
<linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fef3da"/>
<stop offset="1" stop-color="#fbe1c4"/>
</linearGradient>
<linearGradient id="hillGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#a9d8a0"/>
<stop offset="1" stop-color="#7cc07a"/>
</linearGradient>
<linearGradient id="castleGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fff0c2"/>
<stop offset="1" stop-color="#e9c267"/>
</linearGradient>
<linearGradient id="roofGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e0607c"/>
<stop offset="1" stop-color="#b23a57"/>
</linearGradient>
</defs>
<!-- sky -->
<rect x="0" y="0" width="420" height="380" rx="26" fill="url(#skyGrad)"/>
<!-- moon -->
<circle cx="318" cy="92" r="86" fill="url(#moonGlow)"/>
<circle cx="318" cy="92" r="40" fill="#fff7e0" stroke="#f1d98a" stroke-width="2"/>
<!-- stars -->
<g fill="#e9b94f" class="twinkle-group">
<path class="twk" d="M70 60l3 7 7 3-7 3-3 7-3-7-7-3 7-3z"/>
<path class="twk" d="M140 38l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/>
<path class="twk" d="M250 50l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/>
<path class="twk" d="M360 178l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/>
</g>
<!-- back forest -->
<g fill="#5aa86f" opacity="0.8">
<path d="M0 300c30-60 50-70 64-70s34 10 64 70z"/>
<path d="M40 300c26-52 44-62 56-62s30 10 56 62z"/>
<path d="M380 300c-26-58-44-66-56-66s-32 8-58 66z"/>
</g>
<!-- hill -->
<path d="M0 300c80-44 150-58 210-58s130 14 210 58v80H0z" fill="url(#hillGrad)"/>
<!-- castle -->
<g class="castle">
<!-- towers -->
<rect x="150" y="158" width="34" height="118" rx="5" fill="url(#castleGrad)" stroke="#c79a3d" stroke-width="2"/>
<rect x="236" y="158" width="34" height="118" rx="5" fill="url(#castleGrad)" stroke="#c79a3d" stroke-width="2"/>
<rect x="186" y="132" width="48" height="144" rx="5" fill="url(#castleGrad)" stroke="#c79a3d" stroke-width="2"/>
<!-- roofs -->
<path d="M150 158l17-34 17 34z" fill="url(#roofGrad)"/>
<path d="M236 158l17-34 17 34z" fill="url(#roofGrad)"/>
<path d="M186 132l24-44 24 44z" fill="url(#roofGrad)"/>
<!-- flags -->
<g stroke="#8a6212" stroke-width="2">
<line x1="167" y1="124" x2="167" y2="104"/>
<line x1="253" y1="124" x2="253" y2="104"/>
<line x1="210" y1="88" x2="210" y2="62"/>
</g>
<path class="flag" d="M167 104l14 4-14 5z" fill="#ffd23f"/>
<path class="flag" d="M253 104l14 4-14 5z" fill="#ffd23f"/>
<path class="flag" d="M210 62l16 5-16 6z" fill="#ff6f9c"/>
<!-- door -->
<path d="M198 276v-32a12 12 0 0124 0v32z" fill="#8a5a2b" stroke="#5e3c18" stroke-width="2"/>
<!-- windows (glow) -->
<rect class="window" x="160" y="186" width="14" height="20" rx="7" fill="#ffe9a8"/>
<rect class="window" x="246" y="186" width="14" height="20" rx="7" fill="#ffe9a8"/>
<rect class="window" x="200" y="166" width="18" height="24" rx="9" fill="#ffe9a8"/>
</g>
<!-- front shrubs -->
<g fill="#4f9a64">
<ellipse cx="60" cy="356" rx="60" ry="26"/>
<ellipse cx="360" cy="356" rx="64" ry="28"/>
</g>
<!-- a tiny owl on a branch -->
<g class="owl" transform="translate(78 250)">
<ellipse cx="0" cy="0" rx="13" ry="16" fill="#b07a44"/>
<circle cx="-5" cy="-4" r="5" fill="#fff8ef"/>
<circle cx="5" cy="-4" r="5" fill="#fff8ef"/>
<circle cx="-5" cy="-4" r="2.2" fill="#2c2350"/>
<circle cx="5" cy="-4" r="2.2" fill="#2c2350"/>
<path d="M-2 1l2 3 2-3z" fill="#e9a23f"/>
</g>
</svg>
<!-- floating open-book badge -->
<div class="scene-badge" aria-hidden="true">
<span class="badge-emoji">📖</span>
<span class="badge-text">Page 1</span>
</div>
</div>
</div>
<!-- ornamental divider -->
<div class="divider" aria-hidden="true">
<span class="rule"></span>
<span class="orn">❦</span>
<span class="rule"></span>
</div>
</section>
<!-- ============ FEATURED TALES ============ -->
<section id="tales" class="section tales" aria-labelledby="tales-title">
<div class="shell">
<header class="section-head">
<p class="eyebrow">From the golden shelf</p>
<h2 id="tales-title">Featured Tales</h2>
<p class="section-sub">Tap a cover to peek inside — and add it to tonight's bedtime queue.</p>
</header>
<div class="tale-grid" id="taleGrid" role="list">
<!-- cards injected by script.js -->
</div>
<p class="queue-line" aria-live="polite">
<span aria-hidden="true">🌙</span>
<span id="queueStatus">No tales queued yet — choose a favourite to begin.</span>
</p>
</div>
</section>
<!-- ============ READ-ALOUD STRIP ============ -->
<section id="read-aloud" class="section read-aloud" aria-labelledby="read-title">
<div class="shell read-inner">
<div class="read-copy">
<p class="eyebrow gold">The cosy feature</p>
<h2 id="read-title">A gentle voice for every page</h2>
<p class="section-sub">
Press play and a warm narrator reads along, highlighting each line so little readers
can follow the words. Slow it down, speed it up, or let it loop into a lullaby.
</p>
<ul class="feature-list">
<li><span class="tick" aria-hidden="true">✦</span> Word-by-word highlighting</li>
<li><span class="tick" aria-hidden="true">✦</span> 12 cosy storyteller voices</li>
<li><span class="tick" aria-hidden="true">✦</span> Sleep timer & soft night mode</li>
</ul>
</div>
<!-- working read-aloud player demo -->
<figure class="reader" aria-label="Read-aloud demo player">
<figcaption class="reader-cap">
<span class="reader-tale" id="readerTale">The Lantern Fox</span>
<span class="reader-voice">
<label for="voiceSelect" class="vis-hidden">Narrator voice</label>
<select id="voiceSelect">
<option>Grandma Hazel</option>
<option>Old Tom the Bard</option>
<option>Lady Lark</option>
<option>Whispering Willow</option>
</select>
</span>
</figcaption>
<p class="reader-text" id="readerText" aria-live="polite">
<span class="word">Once</span> <span class="word">upon</span> <span class="word">a</span>
<span class="word">frosty</span> <span class="word">night,</span> <span class="word">a</span>
<span class="word">little</span> <span class="word">fox</span> <span class="word">lit</span>
<span class="word">a</span> <span class="word">lantern</span> <span class="word">to</span>
<span class="word">guide</span> <span class="word">lost</span> <span class="word">travellers</span>
<span class="word">home.</span>
</p>
<div class="reader-controls">
<button class="round-btn" id="rewindBtn" type="button" aria-label="Restart from beginning">
<span aria-hidden="true">⏮</span>
</button>
<button class="round-btn play" id="playBtn" type="button" aria-label="Play read-aloud" aria-pressed="false">
<span class="play-icon" aria-hidden="true">▶</span>
</button>
<div class="speed">
<label for="speedRange" class="vis-hidden">Reading speed</label>
<span aria-hidden="true">🐢</span>
<input type="range" id="speedRange" min="0.6" max="1.6" step="0.1" value="1" />
<span aria-hidden="true">🐇</span>
</div>
</div>
<div class="reader-bar" aria-hidden="true">
<span class="reader-progress" id="readerProgress"></span>
</div>
</figure>
</div>
<div class="divider" aria-hidden="true">
<span class="rule"></span>
<span class="orn">✦</span>
<span class="rule"></span>
</div>
</section>
<!-- ============ TESTIMONIALS ============ -->
<section id="voices" class="section voices" aria-labelledby="voices-title">
<div class="shell">
<header class="section-head">
<p class="eyebrow">From the firesides</p>
<h2 id="voices-title">What parents are saying</h2>
</header>
<div class="quote-grid">
<figure class="quote-card">
<div class="quote-mark" aria-hidden="true">“</div>
<blockquote>Our nightly battle over bedtime turned into the part of the day my twins ask for first. The read-aloud voices are pure magic.</blockquote>
<figcaption>
<span class="avatar" aria-hidden="true" style="--a:#ffd23f">M</span>
<span><strong>Marisol P.</strong><span class="meta">Parent of two, Seville</span></span>
<span class="stars" aria-label="5 out of 5 stars">★★★★★</span>
</figcaption>
</figure>
<figure class="quote-card">
<div class="quote-mark" aria-hidden="true">“</div>
<blockquote>The Easy-Read mode helped my dyslexic son finally enjoy reading on his own. He finished four tales last week — by choice!</blockquote>
<figcaption>
<span class="avatar" aria-hidden="true" style="--a:#5ec5d6">J</span>
<span><strong>Jonah R.</strong><span class="meta">Dad & teacher, Leeds</span></span>
<span class="stars" aria-label="5 out of 5 stars">★★★★★</span>
</figcaption>
</figure>
<figure class="quote-card">
<div class="quote-mark" aria-hidden="true">“</div>
<blockquote>Beautiful illustrations, no ads, nothing scary. It feels like a real heirloom storybook — just glowing softly on the tablet.</blockquote>
<figcaption>
<span class="avatar" aria-hidden="true" style="--a:#ff6f9c">A</span>
<span><strong>Aiko T.</strong><span class="meta">Grandparent, Kyoto</span></span>
<span class="stars" aria-label="5 out of 5 stars">★★★★★</span>
</figcaption>
</figure>
</div>
</div>
</section>
<!-- ============ CTA ============ -->
<section id="cta" class="section cta" aria-labelledby="cta-title">
<div class="shell cta-card">
<div class="floaties cta-floaties" aria-hidden="true">
<span class="floaty f1">★</span>
<span class="floaty f3">✧</span>
<span class="floaty f5">✦</span>
</div>
<span class="cta-crown" aria-hidden="true">👑</span>
<h2 id="cta-title">Turn the first page tonight</h2>
<p>Start a free 14-night trial. Unlimited tales, all voices, every cosy feature — and a new story added each week.</p>
<form class="cta-form" id="ctaForm" novalidate>
<label class="vis-hidden" for="ctaEmail">Email address</label>
<input type="email" id="ctaEmail" name="email" placeholder="[email protected]" autocomplete="email" required />
<button class="pill-btn pill-primary" type="submit">Open the Storybook</button>
</form>
<p class="cta-fine" id="ctaMsg">No card needed · cancel anytime · ad-free, always.</p>
</div>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer class="site-footer" role="contentinfo">
<div class="shell footer-inner">
<p class="footer-brand"><span aria-hidden="true">❦</span> Once Upon a Tale</p>
<nav class="footer-nav" aria-label="Footer">
<a href="#tales">Tales</a>
<a href="#read-aloud">Read-Aloud</a>
<a href="#voices">Voices</a>
<a href="#cta">Join</a>
</nav>
<p class="footer-fine">A fictional storybook demo · Stories & characters imagined for show.</p>
</div>
</footer>
<!-- toast -->
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Classic Fairytale Landing
A timeless, heirloom-style landing page for a children’s fairytale app. The cream-and-gold palette, ornate Cinzel and Cormorant Garamond serifs, and a hand-built inline-SVG hero scene — a golden castle on a moonlit hill with a blinking owl, waving flags, glowing windows, and twinkling stars — set a whimsical, storybook mood. Gold flourishes and ornamental dividers frame each section, while gentle float, wave, and twinkle animations bring the page to life (all suspended under prefers-reduced-motion).
The featured-tales grid renders from data: each cover can be tapped to load its story into the read-aloud player, or queued for tonight’s bedtime with a live status line that tallies the running minutes. The read-aloud strip is genuinely interactive — press play and a warm narrator walks word by word with highlighting and a progress bar, adjustable from turtle to hare, with a restart button and four selectable storyteller voices. A “Surprise me” button drops a random tale into the player.
For accessibility, an Easy-Read toggle swaps the display serifs for a high-legibility sans with looser letter, word, and line spacing for dyslexia-friendly reading. The page is keyboard-navigable with visible focus rings, landmark roles and ARIA throughout, a validated email call to action, and a layout that collapses gracefully to a single column down to about 360px.
Illustrative kids’ UI only — fictional stories, characters, and audio.