Creator — Author Landing
A warm, bookish personal-brand landing page for a literary novelist, built in vanilla HTML, CSS, and JavaScript. Features a serif hero with the latest book cover and buy CTA, a four-book shelf grid, an about-the-author bio with portrait, praise and press quotes, a readings and events list with RSVP, a slow-newsletter signup, and social links. Interactions include a book quick-look modal, an accessible mobile nav, scroll reveal animations, and toast confirmations on every call to action.
MCP
Code
:root {
--cream: #f6f1e7;
--cream-deep: #efe6d4;
--paper: #fbf8f1;
--ink: #23211c;
--ink-soft: #4b463d;
--ink-faint: #837b6c;
--accent: #9a3b2e;
--accent-deep: #7c2f24;
--line: #d9cfba;
--shadow: 24px 24px 60px rgba(35, 33, 28, 0.18);
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--body: "Newsreader", Georgia, serif;
--r: 14px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--cream);
color: var(--ink);
font-family: var(--body);
font-size: 1.06rem;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
h1, h2, h3 { font-family: var(--serif); font-weight: 500; line-height: 1.08; margin: 0; letter-spacing: -0.01em; }
em { font-style: italic; }
.wrap { width: min(1140px, 92vw); margin-inline: auto; }
.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: 50%; transform: translateX(-50%) translateY(-200%);
background: var(--ink); color: var(--cream); padding: 0.6rem 1.2rem;
border-radius: 0 0 10px 10px; z-index: 200; transition: transform 0.2s;
}
.skip-link:focus { transform: translateX(-50%) translateY(0); }
.eyebrow {
font-family: var(--serif);
text-transform: uppercase;
letter-spacing: 0.28em;
font-size: 0.72rem;
color: var(--accent);
margin: 0 0 0.9rem;
font-weight: 600;
}
/* ---------- buttons ---------- */
.btn {
display: inline-flex; align-items: center; gap: 0.5rem;
font-family: var(--serif); font-weight: 600; font-size: 1rem;
padding: 0.85rem 1.6rem; border-radius: 999px; border: 1.5px solid var(--ink);
text-decoration: none; cursor: pointer; transition: transform 0.18s ease, background 0.18s, color 0.18s, box-shadow 0.18s;
background: transparent; color: var(--ink);
}
.btn--solid { background: var(--ink); color: var(--cream); border-color: var(--ink); }
.btn--solid:hover { background: var(--accent); border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 10px 24px rgba(154,59,46,0.3); }
.btn--ghost:hover { background: var(--ink); color: var(--cream); transform: translateY(-2px); }
.btn--sm { padding: 0.55rem 1.1rem; font-size: 0.9rem; }
.btn:focus-visible, a:focus-visible, button:focus-visible, [tabindex]:focus-visible {
outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 6px;
}
/* ---------- nav ---------- */
.nav {
position: sticky; top: 0; z-index: 100;
background: rgba(246, 241, 231, 0.86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.nav__inner { display: flex; align-items: center; justify-content: space-between; padding: 0.9rem 0; }
.logo { display: flex; align-items: center; gap: 0.7rem; text-decoration: none; }
.logo__mark {
font-family: var(--serif); font-weight: 600; font-size: 1.1rem;
width: 38px; height: 38px; display: grid; place-items: center;
border: 1.5px solid var(--ink); border-radius: 50%; color: var(--ink);
}
.logo__mark span { color: var(--accent); }
.logo__name { font-family: var(--serif); font-size: 1.25rem; font-weight: 500; letter-spacing: -0.01em; }
.nav__links { display: flex; align-items: center; gap: 1.8rem; }
.nav__links a { text-decoration: none; font-family: var(--serif); font-weight: 500; color: var(--ink-soft); position: relative; transition: color 0.18s; }
.nav__links a:not(.nav__cta)::after {
content: ""; position: absolute; left: 0; bottom: -4px; height: 1.5px; width: 0;
background: var(--accent); transition: width 0.25s ease;
}
.nav__links a:hover { color: var(--ink); }
.nav__links a:not(.nav__cta):hover::after { width: 100%; }
.nav__cta { background: var(--accent); color: var(--cream) !important; padding: 0.5rem 1.15rem; border-radius: 999px; }
.nav__cta:hover { background: var(--accent-deep); transform: translateY(-1px); }
.nav__toggle { display: none; flex-direction: column; gap: 5px; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav__toggle span { width: 26px; height: 2px; background: var(--ink); transition: transform 0.25s, opacity 0.25s; }
.nav.open .nav__toggle span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav.open .nav__toggle span:nth-child(2) { opacity: 0; }
.nav.open .nav__toggle span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* ---------- hero ---------- */
.hero { padding: clamp(3rem, 8vw, 6.5rem) 0 clamp(3rem, 7vw, 5.5rem); position: relative; }
.hero::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(60% 80% at 78% 22%, rgba(154,59,46,0.07), transparent 70%);
pointer-events: none;
}
.hero__inner { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: clamp(2rem, 6vw, 5rem); align-items: center; }
.hero h1 { font-size: clamp(2.6rem, 6.2vw, 4.6rem); letter-spacing: -0.02em; }
.hero h1 em { color: var(--accent); }
.hero__lede { font-size: 1.22rem; color: var(--ink-soft); max-width: 34ch; margin: 1.4rem 0 2rem; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-bottom: 2.3rem; }
.hero__meta { list-style: none; display: flex; gap: 2.2rem; padding: 0; margin: 0; border-top: 1px solid var(--line); padding-top: 1.4rem; }
.hero__meta li { font-size: 0.95rem; color: var(--ink-faint); }
.hero__meta strong { display: block; font-family: var(--serif); font-size: 1.6rem; color: var(--ink); font-weight: 600; }
.hero__visual { position: relative; display: grid; place-items: center; }
.hero__sticker {
position: absolute; bottom: -6px; right: 4%; background: var(--accent); color: var(--cream);
font-family: var(--serif); font-size: 0.82rem; font-weight: 600; padding: 0.55rem 1rem;
border-radius: 999px; transform: rotate(-5deg); box-shadow: 0 8px 18px rgba(154,59,46,0.35);
}
/* ---------- book cover ---------- */
.book { margin: 0; cursor: pointer; position: relative; transition: transform 0.3s ease; outline-offset: 6px; }
.book:hover, .book:focus-visible { transform: translateY(-6px) rotate(-1deg); }
.book__cover {
--c1: #3a4a52; --c2: #1f2a30;
aspect-ratio: 2 / 3; width: min(280px, 70vw);
background: linear-gradient(160deg, var(--c1), var(--c2));
border-radius: 4px 8px 8px 4px;
box-shadow: var(--shadow), inset 8px 0 14px rgba(0,0,0,0.28), inset -1px 0 0 rgba(255,255,255,0.08);
padding: clamp(1.4rem, 3vw, 2.2rem); color: #f4ede0;
display: flex; flex-direction: column; gap: 0.8rem; position: relative; overflow: hidden;
}
.book__cover::before {
content: ""; position: absolute; left: 14px; top: 0; bottom: 0; width: 1px;
background: rgba(255,255,255,0.12);
}
.book--hero .book__cover { width: min(320px, 78vw); }
.book__author { font-family: var(--serif); letter-spacing: 0.22em; text-transform: uppercase; font-size: 0.7rem; opacity: 0.85; }
.book__title { font-family: var(--serif); font-weight: 600; font-size: clamp(1.5rem, 4.5vw, 2.3rem); line-height: 1.05; margin-top: auto; }
.book__rule { height: 1px; width: 42px; background: rgba(255,255,255,0.5); }
.book__sub { font-style: italic; font-size: 0.95rem; opacity: 0.85; }
.book__peek {
position: absolute; inset: auto 0 0 0; text-align: center; padding: 0.6rem;
background: rgba(35,33,28,0.82); color: var(--cream); font-family: var(--serif);
font-size: 0.82rem; letter-spacing: 0.1em; text-transform: uppercase;
transform: translateY(100%); transition: transform 0.28s ease;
}
.book:hover .book__peek, .book:focus-visible .book__peek { transform: translateY(0); }
/* ---------- sections ---------- */
.section { padding: clamp(3.5rem, 8vw, 6rem) 0; }
.section__head { max-width: 60ch; margin-bottom: clamp(2rem, 5vw, 3rem); }
.section__head h2 { font-size: clamp(2rem, 4.6vw, 3.1rem); margin-bottom: 0.7rem; }
.section__sub { color: var(--ink-soft); font-size: 1.1rem; margin: 0; }
/* ---------- books grid ---------- */
.books-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(1.4rem, 3vw, 2.4rem); align-items: end; }
.books-grid .book__cover { width: 100%; }
.books-grid .bookcard { display: flex; flex-direction: column; gap: 0.9rem; }
.books-grid .bookcard__meta h3 { font-size: 1.25rem; }
.books-grid .bookcard__meta p { margin: 0.2rem 0 0; color: var(--ink-faint); font-size: 0.95rem; }
.books-grid .bookcard--feature { grid-column: span 1; }
.tag-new {
display: inline-block; background: var(--accent); color: var(--cream);
font-family: var(--serif); font-size: 0.62rem; letter-spacing: 0.2em; text-transform: uppercase;
padding: 0.2rem 0.55rem; border-radius: 999px; margin-bottom: 0.3rem; font-weight: 600;
}
/* ---------- about ---------- */
.section--about { background: var(--cream-deep); border-block: 1px solid var(--line); }
.about__inner { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: clamp(2rem, 6vw, 4.5rem); align-items: center; }
.portrait {
aspect-ratio: 4 / 5; border-radius: var(--r); position: relative; overflow: hidden;
background:
radial-gradient(120% 80% at 30% 20%, #c2a98a, transparent 60%),
linear-gradient(150deg, #b89a76, #6f5a44);
box-shadow: var(--shadow); display: grid; place-items: center;
}
.portrait__initials { font-family: var(--serif); font-size: clamp(4rem, 12vw, 7rem); color: rgba(255,255,255,0.7); font-weight: 600; }
.portrait__cap { font-size: 0.85rem; color: var(--ink-faint); font-style: italic; margin: 0.8rem 0 0; text-align: center; }
.about__text h2 { font-size: clamp(2rem, 4.6vw, 3rem); margin-bottom: 1.1rem; }
.about__text p { color: var(--ink-soft); margin: 0 0 1.1rem; }
.about__quote {
margin: 1.6rem 0 0; padding-left: 1.4rem; border-left: 3px solid var(--accent);
font-family: var(--serif); font-style: italic; font-size: 1.3rem; color: var(--ink); line-height: 1.4;
}
/* ---------- praise ---------- */
.praise-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(1.2rem, 3vw, 2rem); }
.quote {
margin: 0; background: var(--paper); border: 1px solid var(--line); border-radius: var(--r);
padding: clamp(1.6rem, 3vw, 2.2rem); position: relative; transition: transform 0.25s, box-shadow 0.25s;
}
.quote::before { content: "\201C"; font-family: var(--serif); position: absolute; top: -0.2rem; left: 1rem; font-size: 4rem; color: var(--line); line-height: 1; }
.quote:hover { transform: translateY(-4px); box-shadow: 0 16px 36px rgba(35,33,28,0.1); }
.quote blockquote { margin: 0.6rem 0 1rem; font-size: 1.18rem; font-family: var(--serif); line-height: 1.4; }
.quote figcaption { color: var(--accent); font-family: var(--serif); font-weight: 600; font-size: 0.95rem; }
/* ---------- events ---------- */
.section--events { background: var(--ink); color: var(--cream); }
.section--events .eyebrow { color: #e7a99c; }
.section--events .section__sub { color: rgba(246,241,231,0.7); }
.events { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.event {
display: grid; grid-template-columns: auto 1fr auto; gap: 1.6rem; align-items: center;
padding: 1.5rem 0; border-top: 1px solid rgba(246,241,231,0.18); transition: padding-left 0.25s;
}
.event:last-child { border-bottom: 1px solid rgba(246,241,231,0.18); }
.event:hover { padding-left: 0.6rem; }
.event__date { text-align: center; line-height: 1; }
.event__day { font-family: var(--serif); font-size: 2.1rem; font-weight: 600; display: block; }
.event__mon { text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.75rem; color: #e7a99c; }
.event__body h3 { font-size: 1.35rem; margin-bottom: 0.25rem; }
.event__body p { margin: 0; color: rgba(246,241,231,0.72); font-size: 0.98rem; }
.section--events .btn--ghost { border-color: var(--cream); color: var(--cream); }
.section--events .btn--ghost:hover { background: var(--cream); color: var(--ink); }
/* ---------- newsletter ---------- */
.newsletter { background: var(--accent); color: var(--cream); }
.newsletter .eyebrow { color: #f3d3cc; }
.newsletter__inner { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 6vw, 4rem); align-items: center; }
.newsletter__text h2 { font-size: clamp(2rem, 4.5vw, 3rem); margin-bottom: 0.9rem; }
.newsletter__text p { margin: 0; color: rgba(246,241,231,0.86); font-size: 1.1rem; }
.newsletter__form { display: grid; grid-template-columns: 1fr auto; gap: 0.7rem; }
.newsletter__form input {
font-family: var(--body); font-size: 1.05rem; padding: 0.95rem 1.2rem;
border-radius: 999px; border: 1.5px solid rgba(246,241,231,0.5); background: rgba(255,255,255,0.95); color: var(--ink);
}
.newsletter__form input:focus { outline: 3px solid var(--cream); outline-offset: 2px; }
.newsletter__form .btn--solid { background: var(--ink); border-color: var(--ink); }
.newsletter__form .btn--solid:hover { background: var(--cream); color: var(--accent); border-color: var(--cream); }
.newsletter__note { grid-column: 1 / -1; margin: 0.3rem 0 0; font-size: 0.9rem; color: rgba(246,241,231,0.85); }
.newsletter__note.ok { font-weight: 600; }
/* ---------- footer ---------- */
.footer { background: var(--cream-deep); border-top: 1px solid var(--line); padding: 3rem 0 1.6rem; }
.footer__inner { display: flex; flex-wrap: wrap; gap: 2rem; justify-content: space-between; align-items: flex-start; }
.footer__brand { max-width: 28ch; }
.footer__brand p { margin: 0.7rem 0 0; color: var(--ink-faint); font-style: italic; }
.footer__links { display: flex; flex-wrap: wrap; gap: 1.3rem; }
.footer__links a { text-decoration: none; color: var(--ink-soft); font-family: var(--serif); }
.footer__links a:hover { color: var(--accent); }
.socials { list-style: none; display: flex; gap: 0.7rem; padding: 0; margin: 0; }
.socials a {
width: 42px; height: 42px; display: grid; place-items: center; border: 1.5px solid var(--ink-faint);
border-radius: 50%; text-decoration: none; font-family: var(--serif); font-size: 0.85rem; color: var(--ink-soft);
transition: background 0.2s, color 0.2s, transform 0.2s, border-color 0.2s;
}
.socials a:hover { background: var(--accent); color: var(--cream); border-color: var(--accent); transform: translateY(-3px); }
.footer__fine {
display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center;
margin-top: 2.4rem; padding-top: 1.4rem; border-top: 1px solid var(--line);
font-size: 0.88rem; color: var(--ink-faint);
}
.footer__fine a { text-decoration: none; color: var(--accent); font-family: var(--serif); }
/* ---------- modal ---------- */
.modal { position: fixed; inset: 0; z-index: 300; display: none; }
.modal.open { display: block; }
.modal__overlay { position: absolute; inset: 0; background: rgba(35,33,28,0.6); backdrop-filter: blur(3px); }
.modal__panel {
position: relative; width: min(720px, 92vw); margin: 6vh auto 0; background: var(--paper);
border-radius: var(--r); padding: clamp(1.6rem, 4vw, 2.6rem); box-shadow: var(--shadow);
transform: translateY(16px); opacity: 0; transition: transform 0.3s, opacity 0.3s;
}
.modal.open .modal__panel { transform: translateY(0); opacity: 1; }
.modal__close {
position: absolute; top: 0.8rem; right: 1rem; background: none; border: 0; font-size: 2rem;
line-height: 1; color: var(--ink-faint); cursor: pointer; transition: color 0.2s, transform 0.2s;
}
.modal__close:hover { color: var(--accent); transform: rotate(90deg); }
.modal__grid { display: grid; grid-template-columns: 0.7fr 1.3fr; gap: clamp(1.4rem, 4vw, 2.4rem); align-items: center; }
.modal__cover .book__cover { width: 100%; }
.modal__body h3 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin-bottom: 0.6rem; }
.modal__meta { color: var(--ink-faint); font-style: italic; margin: 0 0 0.9rem; }
.modal__desc { color: var(--ink-soft); margin: 0 0 1.6rem; }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%) translateY(140%);
background: var(--ink); color: var(--cream); padding: 0.85rem 1.4rem; border-radius: 999px;
font-family: var(--serif); font-size: 0.95rem; box-shadow: 0 14px 30px rgba(0,0,0,0.3); z-index: 400;
transition: transform 0.35s cubic-bezier(.2,.9,.3,1.2); max-width: 90vw;
}
.toast.show { transform: translateX(-50%) translateY(0); }
/* ---------- reveal ---------- */
.reveal { opacity: 0; transform: translateY(26px); transition: opacity 0.7s ease, transform 0.7s ease; }
.reveal.in { opacity: 1; transform: none; }
/* ---------- responsive ---------- */
@media (max-width: 920px) {
.books-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 720px) {
.nav__toggle { display: flex; }
.nav__links {
position: fixed; inset: 64px 0 auto 0; flex-direction: column; gap: 0; align-items: stretch;
background: var(--cream); border-bottom: 1px solid var(--line); padding: 0.5rem 6vw 1.4rem;
transform: translateY(-130%); transition: transform 0.3s ease; box-shadow: var(--shadow);
}
.nav.open .nav__links { transform: translateY(0); }
.nav__links a { padding: 0.9rem 0; border-bottom: 1px solid var(--line); }
.nav__cta { text-align: center; margin-top: 0.8rem; padding: 0.8rem; }
.hero__inner { grid-template-columns: 1fr; }
.hero__visual { order: -1; }
.about__inner, .newsletter__inner, .praise-grid { grid-template-columns: 1fr; }
.about__portrait { max-width: 320px; }
.modal__grid { grid-template-columns: 1fr; }
.modal__cover { max-width: 200px; margin-inline: auto; }
.event { grid-template-columns: auto 1fr; }
.event .btn { grid-column: 1 / -1; justify-self: start; }
}
@media (max-width: 520px) {
body { font-size: 1rem; }
.books-grid { grid-template-columns: 1fr; max-width: 320px; margin-inline: auto; }
.hero__meta { gap: 1.4rem; flex-wrap: wrap; }
.newsletter__form { grid-template-columns: 1fr; }
.footer__fine { flex-direction: column; align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
* { scroll-behavior: auto !important; }
.reveal { opacity: 1; transform: none; transition: none; }
}(function () {
"use strict";
/* ---------- data: the shelf ---------- */
var BOOKS = [
{
title: "The Salt Almanac",
sub: "a novel",
year: "2026 · New",
isNew: true,
meta: "352 pages · Harbour & Vane Press",
desc: "A lighthouse keeper inherits her late mother's almanac of tides and discovers, between the dates, the record of a love she was never told about. A luminous study of inheritance, weather, and the things we keep to ourselves.",
c1: "#3a4a52", c2: "#1f2a30"
},
{
title: "Winter Lantern",
sub: "a novel",
year: "2023 · Booker longlist",
meta: "298 pages · Harbour & Vane Press",
desc: "Three siblings return to the family inn for one last winter before it is sold. Over a week of snow and old grievances, a buried secret thaws. Vane's most quietly devastating book.",
c1: "#5b3a4a", c2: "#2e1c26"
},
{
title: "The Quiet Hours",
sub: "a novel · debut",
year: "2020 · Debut",
meta: "276 pages · Harbour & Vane Press",
desc: "A night-shift nurse and an insomniac fisherman cross paths in the small hours of a coastal town. The word-of-mouth phenomenon that started it all.",
c1: "#3f5040", c2: "#202b21"
},
{
title: "Field of Small Wonders",
sub: "stories",
year: "2021 · Stories",
meta: "212 pages · Harbour & Vane Press",
desc: "Twelve short stories about ordinary miracles — a found dog, a returned letter, a kettle that finally clicks off. Vane's only collection, and a perfect doorway into her world.",
c1: "#5a4a2c", c2: "#2e2614"
}
];
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 3200);
}
/* ---------- build book cards ---------- */
function coverHtml(b, hero) {
var newTag = b.isNew ? '<span class="tag-new">New</span>' : "";
return (
'<figure class="book' + (hero ? " book--hero" : "") + '" tabindex="0" role="button" aria-label="Quick look: ' + b.title + '">' +
'<div class="book__cover" style="--c1:' + b.c1 + ";--c2:" + b.c2 + '">' +
'<span class="book__author">Eleanor Vane</span>' +
'<span class="book__title">' + b.title.replace(/ /g, "<br>") + "</span>" +
'<span class="book__rule"></span>' +
'<span class="book__sub">' + b.sub + "</span>" +
"</div>" +
'<figcaption class="book__peek">Quick look</figcaption>' +
"</figure>" +
'<div class="bookcard__meta">' + newTag + "<h3>" + b.title + "</h3><p>" + b.meta + "</p></div>"
);
}
var grid = document.getElementById("booksGrid");
if (grid) {
BOOKS.forEach(function (b, i) {
var card = document.createElement("div");
card.className = "bookcard reveal";
card.innerHTML = coverHtml(b, false);
card.querySelector(".book").dataset.book = String(i);
grid.appendChild(card);
});
}
/* ---------- quick-look modal ---------- */
var modal = document.getElementById("modal");
var lastFocus = null;
function openModal(i) {
var b = BOOKS[i];
if (!b) return;
lastFocus = document.activeElement;
document.getElementById("modalCover").innerHTML =
'<div class="book__cover" style="--c1:' + b.c1 + ";--c2:" + b.c2 + '">' +
'<span class="book__author">Eleanor Vane</span>' +
'<span class="book__title">' + b.title.replace(/ /g, "<br>") + "</span>" +
'<span class="book__rule"></span><span class="book__sub">' + b.sub + "</span></div>";
document.getElementById("modalYear").textContent = b.year;
document.getElementById("modalTitle").textContent = b.title;
document.getElementById("modalMeta").textContent = b.meta;
document.getElementById("modalDesc").textContent = b.desc;
var buy = document.getElementById("modalBuy");
buy.onclick = function (e) { e.preventDefault(); toast("Added “" + b.title + "” to your basket (demo)."); };
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
modal.querySelector(".modal__close").focus();
}
function closeModal() {
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
if (lastFocus) lastFocus.focus();
}
// open from any .book (hero + grid)
document.addEventListener("click", function (e) {
var bookEl = e.target.closest(".book");
if (bookEl && bookEl.dataset.book != null) {
openModal(parseInt(bookEl.dataset.book, 10));
}
if (e.target.closest("[data-close]")) closeModal();
});
document.addEventListener("keydown", function (e) {
var bookEl = e.target.closest && e.target.closest(".book");
if (bookEl && (e.key === "Enter" || e.key === " ") && bookEl.dataset.book != null) {
e.preventDefault();
openModal(parseInt(bookEl.dataset.book, 10));
}
if (e.key === "Escape" && modal.classList.contains("open")) closeModal();
});
/* ---------- mobile nav ---------- */
var nav = document.querySelector(".nav");
var toggle = document.getElementById("navToggle");
toggle.addEventListener("click", function () {
var open = nav.classList.toggle("open");
toggle.setAttribute("aria-expanded", String(open));
toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
});
document.getElementById("navlinks").addEventListener("click", function (e) {
if (e.target.tagName === "A" && nav.classList.contains("open")) {
nav.classList.remove("open");
toggle.setAttribute("aria-expanded", "false");
}
});
/* ---------- CTA buttons / RSVP / socials ---------- */
document.addEventListener("click", function (e) {
var buy = e.target.closest("[data-buy]");
if (buy) { e.preventDefault(); toast("Opening checkout for “" + buy.dataset.buy + "” (demo)."); return; }
var rsvp = e.target.closest("[data-rsvp]");
if (rsvp) { e.preventDefault(); toast("You're on the list for " + rsvp.dataset.rsvp + " ✦"); return; }
var social = e.target.closest("[data-social]");
if (social) { e.preventDefault(); toast("Follow Eleanor on " + social.dataset.social + " (demo)."); }
});
/* ---------- newsletter ---------- */
var form = document.getElementById("newsForm");
var note = document.getElementById("newsNote");
form.addEventListener("submit", function (e) {
e.preventDefault();
var email = document.getElementById("email").value.trim();
var ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (!ok) {
note.textContent = "Please enter a valid email address.";
note.classList.remove("ok");
return;
}
note.textContent = "Thank you — a welcome note is on its way to your inbox.";
note.classList.add("ok");
form.reset();
toast("Subscribed to The Letter ✦");
});
/* ---------- scroll reveal ---------- */
var reveals = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); }
});
}, { threshold: 0.14, rootMargin: "0px 0px -8% 0px" });
reveals.forEach(function (el) { io.observe(el); });
} else {
reveals.forEach(function (el) { el.classList.add("in"); });
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Eleanor Vane — Author</title>
<meta name="description" content="Official site of Eleanor Vane, author of literary fiction. New novel The Salt Almanac out now." />
<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=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400&family=Newsreader:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="nav" id="top">
<div class="wrap nav__inner">
<a class="logo" href="#top" aria-label="Eleanor Vane, home">
<span class="logo__mark">E<span>V</span></span>
<span class="logo__name">Eleanor Vane</span>
</a>
<nav class="nav__links" id="navlinks" aria-label="Primary">
<a href="#books">Books</a>
<a href="#about">About</a>
<a href="#praise">Praise</a>
<a href="#events">Events</a>
<a class="nav__cta" href="#newsletter">Join the list</a>
</nav>
<button class="nav__toggle" id="navToggle" aria-expanded="false" aria-controls="navlinks" aria-label="Open menu">
<span></span><span></span><span></span>
</button>
</div>
</header>
<main id="main">
<!-- HERO -->
<section class="hero" aria-labelledby="hero-title">
<div class="wrap hero__inner">
<div class="hero__text reveal">
<p class="eyebrow">New novel · Out now</p>
<h1 id="hero-title">Stories that linger like<br /><em>woodsmoke and rain.</em></h1>
<p class="hero__lede">Eleanor Vane writes quiet, luminous fiction about memory, the sea, and the small mercies that hold a life together. Her fourth novel, <em>The Salt Almanac</em>, arrives this spring.</p>
<div class="hero__actions">
<a class="btn btn--solid" href="#buy" data-buy="The Salt Almanac">Buy the new book</a>
<a class="btn btn--ghost" href="#books">Explore the shelf</a>
</div>
<ul class="hero__meta">
<li><strong>4</strong> novels</li>
<li><strong>26</strong> languages</li>
<li><strong>Booker</strong> longlisted</li>
</ul>
</div>
<div class="hero__visual reveal">
<figure class="book book--hero" data-book="0" tabindex="0" role="button" aria-label="Quick look: The Salt Almanac">
<div class="book__cover" style="--c1:#3a4a52;--c2:#1f2a30">
<span class="book__author">Eleanor Vane</span>
<span class="book__title">The Salt<br />Almanac</span>
<span class="book__rule"></span>
<span class="book__sub">a novel</span>
</div>
<figcaption class="book__peek">Quick look</figcaption>
</figure>
<div class="hero__sticker" aria-hidden="true">★ Signed copies available</div>
</div>
</div>
</section>
<!-- BOOKS -->
<section class="section" id="books" aria-labelledby="books-title">
<div class="wrap">
<header class="section__head reveal">
<p class="eyebrow">The shelf</p>
<h2 id="books-title">Four novels, one quiet obsession</h2>
<p class="section__sub">Tap any cover for a quick look — synopsis, pages, and where to buy.</p>
</header>
<div class="books-grid" id="booksGrid">
<!-- cards injected for the rest; first is featured -->
</div>
</div>
</section>
<!-- ABOUT -->
<section class="section section--about" id="about" aria-labelledby="about-title">
<div class="wrap about__inner">
<div class="about__portrait reveal" aria-hidden="true">
<div class="portrait">
<span class="portrait__initials">EV</span>
</div>
<p class="portrait__cap">Photographed in Portbel, 2026</p>
</div>
<div class="about__text reveal">
<p class="eyebrow">About the author</p>
<h2 id="about-title">I grew up on the edge of a tide.</h2>
<p>Eleanor Vane was born in a fishing town where the weather wrote the calendar. She studied marine biology before abandoning the lab for the page, and her fiction still carries that close, patient attention to the natural world.</p>
<p>Her debut, <em>The Quiet Hours</em>, was a word-of-mouth phenomenon; <em>Winter Lantern</em> followed two years later to a Booker longlisting. She lives between a flat in the city and a draughty cottage by the harbour, where she writes longhand at a kitchen table that has seen four books and one very persistent cat.</p>
<blockquote class="about__quote">"I'm not interested in plot for its own sake. I'm interested in the moment the kettle clicks off and someone finally says the true thing."</blockquote>
</div>
</div>
</section>
<!-- PRAISE -->
<section class="section" id="praise" aria-labelledby="praise-title">
<div class="wrap">
<header class="section__head reveal">
<p class="eyebrow">Praise</p>
<h2 id="praise-title">What readers & critics say</h2>
</header>
<div class="praise-grid">
<figure class="quote reveal">
<blockquote>"Vane writes sentences you want to press flat between the pages of a hymnal. <em>The Salt Almanac</em> is her finest yet."</blockquote>
<figcaption>— The Harbour Review</figcaption>
</figure>
<figure class="quote reveal">
<blockquote>"A novelist of rare tenderness. She makes the ordinary ache."</blockquote>
<figcaption>— Marisol Adeyemi, novelist</figcaption>
</figure>
<figure class="quote reveal">
<blockquote>"I read it in a single grey afternoon and have not stopped thinking about it since."</blockquote>
<figcaption>— Field Notes Magazine</figcaption>
</figure>
<figure class="quote reveal">
<blockquote>"Quietly devastating, then quietly mending. A gift of a book."</blockquote>
<figcaption>— The Evening Ledger</figcaption>
</figure>
</div>
</div>
</section>
<!-- EVENTS -->
<section class="section section--events" id="events" aria-labelledby="events-title">
<div class="wrap">
<header class="section__head reveal">
<p class="eyebrow">On the road</p>
<h2 id="events-title">Readings & events</h2>
<p class="section__sub">Come say hello. Signed stock at every stop while it lasts.</p>
</header>
<ul class="events" id="eventsList">
<li class="event reveal">
<div class="event__date"><span class="event__day">12</span><span class="event__mon">Apr</span></div>
<div class="event__body">
<h3>Launch night — <em>The Salt Almanac</em></h3>
<p>Harbourside Books, Portbel · 7:00 PM · in conversation with Marisol Adeyemi</p>
</div>
<button class="btn btn--ghost btn--sm" data-rsvp="Launch night, Portbel">RSVP</button>
</li>
<li class="event reveal">
<div class="event__date"><span class="event__day">27</span><span class="event__mon">Apr</span></div>
<div class="event__body">
<h3>Festival of the Page</h3>
<p>Old Mill Theatre, Greywater · 2:00 PM · reading & signing</p>
</div>
<button class="btn btn--ghost btn--sm" data-rsvp="Festival of the Page, Greywater">RSVP</button>
</li>
<li class="event reveal">
<div class="event__date"><span class="event__day">09</span><span class="event__mon">May</span></div>
<div class="event__body">
<h3>An Evening with Eleanor Vane</h3>
<p>City Athenaeum · 6:30 PM · ticketed · Q&A to follow</p>
</div>
<button class="btn btn--ghost btn--sm" data-rsvp="An Evening with Eleanor Vane">RSVP</button>
</li>
</ul>
</div>
</section>
<!-- NEWSLETTER -->
<section class="section newsletter" id="newsletter" aria-labelledby="news-title">
<div class="wrap newsletter__inner reveal">
<div class="newsletter__text">
<p class="eyebrow">The letter</p>
<h2 id="news-title">A slow letter, a few times a year</h2>
<p>Notes from the writing desk, early chapters, and first word of new books and events. No noise — just the occasional candle-lit dispatch.</p>
</div>
<form class="newsletter__form" id="newsForm" novalidate>
<label class="sr-only" for="email">Email address</label>
<input id="email" name="email" type="email" placeholder="you@example.com" autocomplete="email" required />
<button class="btn btn--solid" type="submit">Subscribe</button>
<p class="newsletter__note" id="newsNote" aria-live="polite">Unsubscribe anytime. Read by 18,000 quiet souls.</p>
</form>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap footer__inner">
<div class="footer__brand">
<span class="logo__mark">E<span>V</span></span>
<p>Literary fiction from the edge of the tide.</p>
</div>
<nav class="footer__links" aria-label="Footer">
<a href="#books">Books</a>
<a href="#about">About</a>
<a href="#events">Events</a>
<a href="#newsletter">Newsletter</a>
</nav>
<ul class="socials" aria-label="Social links">
<li><a href="#" data-social="Instagram" aria-label="Instagram">Ig</a></li>
<li><a href="#" data-social="Bluesky" aria-label="Bluesky">Bs</a></li>
<li><a href="#" data-social="Goodreads" aria-label="Goodreads">Gr</a></li>
<li><a href="#" data-social="Newsletter" aria-label="Substack">Sub</a></li>
</ul>
</div>
<div class="wrap footer__fine">
<p>© 2026 Eleanor Vane. A fictional author. Site for demonstration only.</p>
<a href="#top">Back to top ↑</a>
</div>
</footer>
<!-- QUICK LOOK MODAL -->
<div class="modal" id="modal" aria-hidden="true">
<div class="modal__overlay" data-close></div>
<div class="modal__panel" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal__close" data-close aria-label="Close quick look">×</button>
<div class="modal__grid">
<div class="modal__cover" id="modalCover"></div>
<div class="modal__body">
<p class="eyebrow" id="modalYear"></p>
<h3 id="modalTitle"></h3>
<p class="modal__meta" id="modalMeta"></p>
<p class="modal__desc" id="modalDesc"></p>
<div class="hero__actions">
<a class="btn btn--solid" href="#" id="modalBuy">Buy now</a>
<button class="btn btn--ghost" data-close>Keep browsing</button>
</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Author Landing
A full one-page personal-brand site for the fictional literary novelist Eleanor Vane, set in a warm cream-and-ink palette with a literary serif (Fraunces) for headings and Newsreader for body copy. The sticky top nav carries the author’s monogram, section links, and a “Join the list” CTA, collapsing into an animated hamburger menu below 720px. The hero pairs a large display headline and buy CTA with a CSS-rendered book cover for the new novel, complete with a tilted “signed copies” sticker and stat row.
Below the hero, a four-up shelf grid renders every title from a small data array, each cover acting as a button that opens an accessible quick-look modal with synopsis, page count, and a demo buy action. The page continues through an about-the-author section with a generated portrait and pull quote, a two-column praise wall of critic and reader blurbs, a dark readings-and-events list with per-event RSVP buttons, and an accent-colored newsletter band with inline email validation. A footer rounds it out with brand line, nav, and social buttons.
Everything runs on vanilla JavaScript: the book modal (with focus management and Escape-to-close), the mobile menu, an IntersectionObserver scroll-reveal, newsletter validation, and a shared toast() helper that confirms every CTA. The layout is responsive down to ~360px and honors prefers-reduced-motion.
Illustrative UI only — fictional creator, not a real person or brand.