Comics — Graphic Novel Landing
A cinematic landing page for the fictional graphic novel The Ashfall Letters, built in HTML, CSS, and vanilla JS. A full-bleed CSS-drawn moody hero with falling ash, grain, and a duotone overlay sets a restrained, painterly tone. Literary serif headings carry a rotating pull-quote of press blurbs, a sticky synopsis with drop cap and edition facts, an inside-the-book spread preview with crossfade and gentle parallax, an awards band, an author note, and a buy-the-hardcover CTA with a 3D cover and cart and wishlist toasts.
MCP
Código
:root {
/* Palette override — muted painterly graphic novel */
--ink: #1c1a17;
--ink-2: #322e28;
--paper: #efe7d8;
--paper-2: #e6ddca;
--panel: #f6f0e3;
--accent: #7a5c3e; /* sepia / umber */
--accent-2: #9c7a52; /* lighter umber */
--accent-blue: #3c4a5e; /* dusk blue */
--muted: #6f6857;
--line: rgba(28, 26, 23, 0.16);
--line-2: rgba(28, 26, 23, 0.30);
--halftone: radial-gradient(circle, rgba(28, 26, 23, 0.10) 1px, transparent 1.6px);
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--serif: "Playfair Display", Georgia, "Times New Roman", serif;
--body: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
--shadow-soft: 0 18px 40px -22px rgba(28, 26, 23, 0.55);
--shadow-card: 0 24px 60px -30px rgba(28, 26, 23, 0.6);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--body);
color: var(--ink);
background-color: var(--paper);
background-image:
radial-gradient(120% 120% at 50% -10%, rgba(122, 92, 62, 0.10), transparent 60%),
var(--halftone);
background-size: auto, 7px 7px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
.wrap {
width: min(1120px, 100% - 3rem);
margin-inline: auto;
}
.serif { font-family: var(--serif); }
.kicker {
font-size: 0.74rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
font-weight: 700;
margin: 0 0 0.7rem;
}
.skip {
position: absolute;
left: -999px;
top: 0;
background: var(--ink);
color: var(--paper);
padding: 0.6rem 1rem;
z-index: 200;
border-radius: 0 0 var(--r-sm) 0;
}
.skip:focus { left: 0; }
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: var(--body);
font-weight: 600;
font-size: 0.95rem;
letter-spacing: 0.01em;
padding: 0.78rem 1.4rem;
border-radius: var(--r-sm);
border: 2px solid var(--ink);
background: transparent;
color: var(--ink);
cursor: pointer;
text-decoration: none;
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.btn--solid {
background: var(--ink);
color: var(--paper);
box-shadow: 5px 5px 0 0 var(--accent);
}
.btn--solid:hover { box-shadow: 7px 7px 0 0 var(--accent); }
.btn--line:hover { background: rgba(28, 26, 23, 0.06); }
.btn--ghost {
border-color: var(--line-2);
padding: 0.5rem 1rem;
font-size: 0.86rem;
}
.btn--ghost:hover { background: rgba(28, 26, 23, 0.06); transform: translateY(-1px); }
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 100;
background: rgba(239, 231, 216, 0.86);
backdrop-filter: saturate(1.1) blur(8px);
border-bottom: 2px solid var(--ink);
}
.topbar__row {
display: flex;
align-items: center;
gap: 1.2rem;
min-height: 64px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
font-weight: 800;
}
.brand__mark {
width: 18px;
height: 18px;
background: var(--accent);
border: 2px solid var(--ink);
transform: rotate(45deg);
}
.brand__name {
font-family: var(--serif);
font-weight: 800;
letter-spacing: 0.02em;
}
.nav {
margin-left: auto;
display: flex;
gap: 1.4rem;
}
.nav a {
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
color: var(--ink-2);
padding-bottom: 2px;
border-bottom: 2px solid transparent;
transition: border-color 0.18s ease, color 0.18s ease;
}
.nav a:hover { color: var(--ink); border-color: var(--accent); }
.topbar__cta { margin-left: 0.4rem; }
/* ---------- Hero ---------- */
.hero {
position: relative;
min-height: clamp(560px, 88vh, 820px);
display: flex;
align-items: flex-end;
overflow: hidden;
border-bottom: 3px solid var(--ink);
}
.hero__scene { position: absolute; inset: 0; z-index: 0; }
.scene__sky {
position: absolute; inset: 0;
background:
linear-gradient(180deg, #2b3340 0%, #4a4537 46%, #6b5a40 78%, #3a3127 100%);
}
.scene__moon {
position: absolute;
top: 12%; left: 64%;
width: 130px; height: 130px;
border-radius: 50%;
background: radial-gradient(circle at 38% 36%, #d9cdb0, #b6a47e 60%, #8f7c58 100%);
box-shadow: 0 0 80px 26px rgba(217, 205, 176, 0.28);
}
.scene__ridge { position: absolute; left: -5%; right: -5%; bottom: 0; }
.scene__ridge--far {
height: 46%;
background: linear-gradient(180deg, transparent, #3e4250 80%);
clip-path: polygon(0 64%, 12% 50%, 26% 60%, 40% 42%, 55% 56%, 70% 40%, 84% 54%, 100% 46%, 100% 100%, 0 100%);
opacity: 0.75;
}
.scene__ridge--mid {
height: 40%;
background: linear-gradient(180deg, #4c4334, #2f281d);
clip-path: polygon(0 70%, 16% 52%, 30% 66%, 48% 46%, 64% 62%, 80% 44%, 100% 60%, 100% 100%, 0 100%);
}
.scene__ridge--near {
height: 30%;
background: linear-gradient(180deg, #241f17, #14110c);
clip-path: polygon(0 60%, 20% 40%, 38% 58%, 58% 36%, 76% 56%, 100% 38%, 100% 100%, 0 100%);
}
.scene__figure {
position: absolute;
bottom: 8%; left: 18%;
width: 70px; height: 150px;
background: linear-gradient(180deg, #14110c, #0c0a07);
clip-path: polygon(40% 0, 60% 0, 64% 18%, 78% 40%, 70% 44%, 60% 30%, 60% 60%, 72% 100%, 54% 100%, 50% 66%, 46% 100%, 28% 100%, 40% 60%, 40% 30%, 30% 44%, 22% 40%, 36% 18%);
filter: drop-shadow(0 0 18px rgba(0, 0, 0, 0.5));
}
.scene__ash {
position: absolute; inset: 0;
background-image:
radial-gradient(circle, rgba(220, 210, 188, 0.5) 0.8px, transparent 1.2px),
radial-gradient(circle, rgba(220, 210, 188, 0.35) 0.8px, transparent 1.2px);
background-size: 90px 90px, 140px 140px;
background-position: 0 0, 40px 60px;
opacity: 0.5;
animation: ashfall 22s linear infinite;
}
@keyframes ashfall {
to { background-position: -30px 90px, 60px 200px; }
}
.scene__grain {
position: absolute; inset: 0;
background-image: var(--halftone);
background-size: 4px 4px;
mix-blend-mode: multiply;
opacity: 0.4;
}
.scene__duotone {
position: absolute; inset: 0;
background: linear-gradient(150deg, rgba(60, 74, 94, 0.32), rgba(122, 92, 62, 0.34));
mix-blend-mode: color;
}
.scene__vignette {
position: absolute; inset: 0;
background:
linear-gradient(180deg, rgba(20, 17, 12, 0.32) 0%, transparent 30%, transparent 50%, rgba(20, 17, 12, 0.78) 100%),
radial-gradient(120% 90% at 50% 40%, transparent 50%, rgba(20, 17, 12, 0.5) 100%);
}
.hero__inner {
position: relative;
z-index: 2;
padding: 0 0 clamp(2.5rem, 6vw, 5rem);
color: var(--paper);
max-width: 720px;
}
.hero__eyebrow {
font-size: 0.82rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--accent-2);
font-weight: 700;
margin: 0 0 0.8rem;
}
.hero__title {
font-family: var(--serif);
font-weight: 800;
font-size: clamp(2.8rem, 9vw, 6rem);
line-height: 0.96;
margin: 0 0 1rem;
letter-spacing: 0.005em;
text-shadow: 0 6px 30px rgba(0, 0, 0, 0.5);
}
.hero__sub {
font-size: clamp(1rem, 2.2vw, 1.25rem);
max-width: 46ch;
color: rgba(239, 231, 216, 0.9);
margin: 0 0 1.4rem;
}
.hero__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
font-size: 0.84rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(239, 231, 216, 0.72);
margin-bottom: 1.6rem;
}
.dot { width: 4px; height: 4px; border-radius: 50%; background: var(--accent-2); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.8rem; }
.hero__actions .btn--line { border-color: var(--paper); color: var(--paper); }
.hero__actions .btn--line:hover { background: rgba(239, 231, 216, 0.12); }
.hero__scroll {
position: absolute;
right: clamp(1rem, 4vw, 3rem);
bottom: 2rem;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
color: rgba(239, 231, 216, 0.7);
font-size: 0.7rem;
letter-spacing: 0.24em;
text-transform: uppercase;
}
.hero__scrollLine {
width: 1px; height: 48px;
background: linear-gradient(180deg, var(--accent-2), transparent);
}
/* ---------- Reveal ---------- */
.reveal {
opacity: 0;
transform: translateY(22px);
transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.reveal.is-in { opacity: 1; transform: none; }
/* ---------- Quote rotator ---------- */
.quote {
background: var(--ink);
color: var(--paper);
padding: clamp(3rem, 7vw, 5.5rem) 0;
border-bottom: 3px solid var(--ink);
}
.quote .wrap { text-align: center; }
.quote__rot { margin: 0; min-height: 5.5em; }
.quote__text {
font-family: var(--serif);
font-style: italic;
font-weight: 500;
font-size: clamp(1.4rem, 4vw, 2.4rem);
line-height: 1.32;
max-width: 22ch;
margin: 0 auto 1.2rem;
transition: opacity 0.5s ease, transform 0.5s ease;
}
.quote__text.is-fading { opacity: 0; transform: translateY(8px); }
.quote__by {
font-size: 0.8rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent-2);
font-weight: 600;
transition: opacity 0.5s ease;
}
.quote__by.is-fading { opacity: 0; }
.quote__dots {
display: flex;
justify-content: center;
gap: 0.6rem;
margin-top: 1.8rem;
}
.quote__dot {
width: 10px; height: 10px;
border-radius: 50%;
border: 2px solid rgba(239, 231, 216, 0.5);
background: transparent;
cursor: pointer;
padding: 0;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.quote__dot:hover { transform: scale(1.2); }
.quote__dot.is-active { background: var(--accent-2); border-color: var(--accent-2); }
.quote__dot:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 3px; }
/* ---------- Synopsis ---------- */
.synopsis { padding: clamp(3.5rem, 8vw, 6.5rem) 0; }
.synopsis__grid {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: clamp(2rem, 5vw, 4rem);
align-items: start;
}
.synopsis__h {
font-weight: 700;
font-size: clamp(1.8rem, 4.5vw, 2.9rem);
line-height: 1.08;
margin: 0;
position: sticky;
top: 92px;
}
.synopsis__body p { margin: 0 0 1.1rem; color: var(--ink-2); font-size: 1.04rem; }
.drop::first-letter {
font-family: var(--serif);
float: left;
font-size: 3.6rem;
line-height: 0.74;
font-weight: 800;
padding: 0.1rem 0.6rem 0 0;
color: var(--accent);
}
.facts {
list-style: none;
margin: 2rem 0 0;
padding: 1.4rem;
border: 2px solid var(--ink);
border-radius: var(--r-md);
background: var(--panel);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem 1.6rem;
box-shadow: var(--shadow-soft);
}
.facts li { display: flex; flex-direction: column; gap: 0.2rem; }
.facts span {
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.facts strong { font-size: 1rem; }
/* ---------- Inside the book ---------- */
.inside {
padding: clamp(3.5rem, 8vw, 6.5rem) 0;
background: var(--paper-2);
border-top: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
}
.inside__head { margin-bottom: 2.4rem; max-width: 40ch; }
.inside__head h2 { font-size: clamp(1.8rem, 4.5vw, 2.9rem); font-weight: 700; margin: 0; }
.inside__layout {
display: grid;
grid-template-columns: 1.4fr 0.85fr;
gap: clamp(1.5rem, 4vw, 3rem);
align-items: center;
}
.spread { margin: 0; }
.spread__frame {
position: relative;
aspect-ratio: 16 / 10;
border: 3px solid var(--ink);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-card);
background: #2b2820;
will-change: transform;
}
.spread__page {
position: absolute; inset: 0;
opacity: 0;
transform: scale(1.04);
transition: opacity 0.7s ease, transform 1.2s ease;
}
.spread__page.is-active { opacity: 1; transform: scale(1); }
/* painterly page art per spread */
.spread__page--0 {
background:
linear-gradient(135deg, rgba(60, 74, 94, 0.5), transparent 60%),
linear-gradient(180deg, #4a4232, #2a2418);
}
.spread__page--0::after {
content: "";
position: absolute; inset: 14% 10%;
background:
repeating-linear-gradient(0deg, rgba(239, 231, 216, 0.10) 0 1px, transparent 1px 22px),
repeating-linear-gradient(90deg, rgba(239, 231, 216, 0.10) 0 1px, transparent 1px 22px),
radial-gradient(60% 50% at 60% 40%, rgba(156, 122, 82, 0.5), transparent);
border: 1px solid rgba(239, 231, 216, 0.25);
}
.spread__page--1 {
background:
radial-gradient(80% 60% at 30% 20%, rgba(122, 92, 62, 0.55), transparent),
linear-gradient(180deg, #5b4d39, #211b12);
}
.spread__page--1::after {
content: "";
position: absolute; inset: 0;
background-image:
radial-gradient(circle, rgba(239, 231, 216, 0.55) 0.9px, transparent 1.3px);
background-size: 60px 60px;
opacity: 0.4;
}
.spread__page--2 {
background:
linear-gradient(110deg, rgba(60, 74, 94, 0.6), transparent 55%),
linear-gradient(180deg, #3a3a3a, #161616);
}
.spread__page--2::after {
content: "";
position: absolute; left: 50%; top: 18%; bottom: 18%;
width: 2px;
background: rgba(156, 122, 82, 0.8);
box-shadow: 0 0 24px 6px rgba(156, 122, 82, 0.35);
transform: rotate(8deg);
}
.spread__gutter {
position: absolute; left: 50%; top: 0; bottom: 0;
width: 26px;
transform: translateX(-50%);
background: linear-gradient(90deg, rgba(0,0,0,0.45), rgba(0,0,0,0) 22%, rgba(0,0,0,0) 78%, rgba(0,0,0,0.45));
z-index: 4;
pointer-events: none;
}
.spread__grain {
position: absolute; inset: 0;
background-image: var(--halftone);
background-size: 5px 5px;
mix-blend-mode: multiply;
opacity: 0.5;
z-index: 5;
pointer-events: none;
}
.spread__cap {
margin-top: 1rem;
font-family: var(--serif);
font-style: italic;
color: var(--muted);
font-size: 1.02rem;
}
.inside__thumbs { display: flex; flex-direction: column; gap: 0.9rem; }
.thumb {
display: flex;
align-items: center;
gap: 0.9rem;
text-align: left;
padding: 0.7rem;
border: 2px solid var(--line-2);
border-radius: var(--r-md);
background: var(--panel);
cursor: pointer;
font-family: var(--body);
transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.thumb:hover { transform: translateX(4px); }
.thumb.is-active { border-color: var(--ink); box-shadow: 4px 4px 0 0 var(--accent); }
.thumb:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.thumb__art {
width: 52px; height: 40px;
flex: none;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
}
.thumb__art--0 { background: linear-gradient(135deg, #3c4a5e, #2a2418); }
.thumb__art--1 { background: radial-gradient(60% 60% at 30% 30%, #9c7a52, #211b12); }
.thumb__art--2 { background: linear-gradient(110deg, #3c4a5e, #161616); }
.thumb__label { font-weight: 600; font-size: 0.92rem; }
/* ---------- Press band ---------- */
.press {
padding: clamp(2.6rem, 6vw, 4rem) 0;
text-align: center;
}
.press__lede {
font-size: 0.74rem;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--accent);
font-weight: 700;
margin: 0 0 1.6rem;
}
.press__band {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: clamp(1rem, 4vw, 3rem);
}
.press__band li {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
padding: 0 0.5rem;
position: relative;
}
.press__band li + li::before {
content: "";
position: absolute; left: calc(-1 * clamp(0.5rem, 2vw, 1.5rem));
top: 10%; bottom: 10%;
width: 1px;
background: var(--line-2);
}
.press__band strong {
font-family: var(--serif);
font-size: 1.3rem;
font-weight: 700;
}
.press__band span { font-size: 0.76rem; color: var(--muted); letter-spacing: 0.04em; }
/* ---------- Author note ---------- */
.author {
padding: clamp(3.5rem, 8vw, 6.5rem) 0;
background: var(--paper-2);
border-top: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
}
.author__grid {
display: grid;
grid-template-columns: 0.7fr 1.3fr;
gap: clamp(2rem, 5vw, 4rem);
align-items: center;
}
.author__portrait {
position: relative;
aspect-ratio: 4 / 5;
border: 3px solid var(--ink);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-card);
}
.portrait__art {
position: absolute; inset: 0;
background:
radial-gradient(50% 38% at 50% 32%, #c9b893 0 36%, transparent 38%),
linear-gradient(180deg, #6f5d44 0%, #3a3025 100%);
}
.portrait__art::after {
content: "";
position: absolute; left: 26%; right: 26%; top: 52%; bottom: 0;
background: linear-gradient(180deg, #2c2519, #4a3e2c);
border-radius: 40% 40% 0 0;
}
.portrait__grain {
position: absolute; inset: 0;
background-image: var(--halftone);
background-size: 5px 5px;
mix-blend-mode: multiply;
opacity: 0.45;
}
.author__note h2 { font-size: clamp(1.7rem, 4vw, 2.6rem); font-weight: 700; margin: 0 0 1.2rem; }
.author__note p {
margin: 0 0 1.1rem;
font-family: var(--serif);
font-size: clamp(1.05rem, 2.2vw, 1.3rem);
font-style: italic;
line-height: 1.5;
color: var(--ink-2);
}
.author__sign { font-style: normal !important; font-family: var(--body) !important; font-size: 0.9rem !important; color: var(--muted) !important; letter-spacing: 0.04em; }
/* ---------- Buy ---------- */
.buy { padding: clamp(3.5rem, 8vw, 6.5rem) 0; }
.buy__grid {
display: grid;
grid-template-columns: 0.8fr 1.2fr;
gap: clamp(2rem, 6vw, 5rem);
align-items: center;
}
.buy__cover {
position: relative;
justify-self: center;
width: min(280px, 70vw);
aspect-ratio: 5 / 7;
perspective: 1200px;
}
.cover__face {
position: relative;
height: 100%;
border: 3px solid var(--ink);
border-radius: 2px 8px 8px 2px;
background: linear-gradient(160deg, #5b4d39 0%, #2a2418 100%);
box-shadow: var(--shadow-card);
padding: 1.6rem 1.4rem;
display: flex;
flex-direction: column;
color: var(--paper);
transform: rotateY(-14deg) rotateX(3deg);
transform-style: preserve-3d;
transition: transform 0.5s ease;
}
.buy__cover:hover .cover__face { transform: rotateY(-6deg) rotateX(1deg); }
.cover__pub { font-size: 0.66rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--accent-2); }
.cover__title {
font-family: var(--serif);
font-weight: 800;
font-size: 1.7rem;
line-height: 1.05;
margin-top: 0.6rem;
}
.cover__art {
flex: 1;
margin: 1rem 0;
border-radius: var(--r-sm);
background:
linear-gradient(150deg, rgba(60, 74, 94, 0.5), transparent),
radial-gradient(60% 50% at 60% 70%, rgba(156, 122, 82, 0.7), transparent),
linear-gradient(180deg, #463a2a, #181410);
border: 1px solid rgba(239, 231, 216, 0.18);
}
.cover__author { font-family: var(--serif); font-style: italic; font-size: 0.95rem; align-self: flex-end; }
.cover__spine {
position: absolute; left: -8px; top: 0; bottom: 0;
width: 10px;
background: linear-gradient(90deg, #14110c, #2a2418);
border: 2px solid var(--ink);
border-right: none;
border-radius: 2px 0 0 2px;
transform: rotateY(-14deg);
transform-origin: right center;
}
.buy__panel h2 { font-size: clamp(1.9rem, 4.5vw, 3rem); font-weight: 700; margin: 0 0 1rem; }
.buy__copy { color: var(--ink-2); margin: 0 0 1.4rem; max-width: 44ch; }
.buy__price { display: flex; align-items: baseline; gap: 0.8rem; margin-bottom: 1.4rem; }
.buy__amt { font-family: var(--serif); font-size: 2.4rem; font-weight: 800; }
.buy__old { color: var(--muted); text-decoration: line-through; font-size: 1.1rem; }
.buy__tag {
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 0.3rem 0.6rem;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
background: var(--accent-2);
font-weight: 700;
}
.buy__actions { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-bottom: 1rem; }
.buy__ship { font-size: 0.82rem; color: var(--muted); margin: 0; }
/* ---------- Footer ---------- */
.foot {
border-top: 3px solid var(--ink);
background: var(--ink);
color: rgba(239, 231, 216, 0.7);
padding: 1.6rem 0;
}
.foot__row { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; font-size: 0.82rem; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 140%);
background: var(--ink);
color: var(--paper);
border: 2px solid var(--accent);
padding: 0.8rem 1.3rem;
border-radius: var(--r-sm);
font-size: 0.9rem;
font-weight: 600;
z-index: 300;
box-shadow: var(--shadow-card);
opacity: 0;
transition: transform 0.34s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.34s ease;
}
.toast.is-show { transform: translate(-50%, 0); opacity: 1; }
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.synopsis__grid,
.inside__layout,
.author__grid,
.buy__grid { grid-template-columns: 1fr; }
.synopsis__h { position: static; }
.nav { display: none; }
.inside__thumbs { flex-direction: row; flex-wrap: wrap; }
.thumb { flex: 1 1 200px; }
}
@media (max-width: 520px) {
.wrap { width: min(1120px, 100% - 2rem); }
.topbar__cta { display: none; }
.hero { min-height: 78vh; }
.hero__actions { flex-direction: column; align-items: stretch; }
.hero__actions .btn { width: 100%; }
.facts { grid-template-columns: 1fr; }
.press__band { gap: 1.2rem 1.6rem; }
.press__band li + li::before { display: none; }
.buy__actions .btn { flex: 1 1 100%; }
.hero__scroll { display: none; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; scroll-behavior: auto !important; }
.reveal { opacity: 1; transform: none; transition: none; }
.spread__page { transition: opacity 0.3s ease; }
.parallax { transform: none !important; }
}(function () {
"use strict";
var prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
/* ---------- Scroll reveal ---------- */
var revealEls = Array.prototype.slice.call(
document.querySelectorAll(".reveal")
);
if (prefersReduced || !("IntersectionObserver" in window)) {
revealEls.forEach(function (el) {
el.classList.add("is-in");
});
} else {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-in");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.16, rootMargin: "0px 0px -8% 0px" }
);
revealEls.forEach(function (el) {
io.observe(el);
});
}
/* ---------- Quote rotator ---------- */
var quotes = [
{
text: "“A hushed, devastating book — the silences land harder than any battle.”",
by: "The Paper Lantern Review",
},
{
text: "“Vale paints distance like weather. You can feel the years between the letters.”",
by: "Margin & Ink Quarterly",
},
{
text: "“The most beautiful war comic in a decade — and barely a war comic at all.”",
by: "Dispatch from the Drawn",
},
{
text: "“A cartography of grief. I read the last spread three times and still wept.”",
by: "Folio & Frame",
},
];
var quoteText = document.querySelector("[data-quote]");
var quoteCite = document.querySelector("[data-cite]");
var quoteDots = Array.prototype.slice.call(
document.querySelectorAll(".quote__dot")
);
var qIndex = 0;
var qTimer = null;
function renderQuote(i, animate) {
qIndex = (i + quotes.length) % quotes.length;
var q = quotes[qIndex];
quoteDots.forEach(function (dot, idx) {
var active = idx === qIndex;
dot.classList.toggle("is-active", active);
dot.setAttribute("aria-selected", active ? "true" : "false");
});
if (!quoteText) return;
if (animate && !prefersReduced) {
quoteText.classList.add("is-fading");
if (quoteCite) quoteCite.classList.add("is-fading");
window.setTimeout(function () {
quoteText.textContent = q.text;
if (quoteCite) quoteCite.textContent = q.by;
quoteText.classList.remove("is-fading");
if (quoteCite) quoteCite.classList.remove("is-fading");
}, 320);
} else {
quoteText.textContent = q.text;
if (quoteCite) quoteCite.textContent = q.by;
}
}
function startQuoteAuto() {
if (prefersReduced) return;
stopQuoteAuto();
qTimer = window.setInterval(function () {
renderQuote(qIndex + 1, true);
}, 6000);
}
function stopQuoteAuto() {
window.clearInterval(qTimer);
}
quoteDots.forEach(function (dot, idx) {
dot.addEventListener("click", function () {
renderQuote(idx, true);
startQuoteAuto();
});
});
if (quotes.length) {
renderQuote(0, false);
startQuoteAuto();
}
/* ---------- Spread preview crossfade ---------- */
var spreadCaps = [
"Spread 1 — “The first letter arrives folded in a contour line.”",
"Spread 2 — “Ash over the northern field; the ink will not dry.”",
"Spread 3 — “The border, at last, drawn between two names.”",
];
var pages = Array.prototype.slice.call(
document.querySelectorAll(".spread__page")
);
var thumbs = Array.prototype.slice.call(document.querySelectorAll(".thumb"));
var capEl = document.querySelector("[data-spread-cap]");
var sIndex = 0;
function goSpread(i) {
sIndex = (i + pages.length) % pages.length;
pages.forEach(function (p, idx) {
p.classList.toggle("is-active", idx === sIndex);
});
thumbs.forEach(function (t, idx) {
var active = idx === sIndex;
t.classList.toggle("is-active", active);
t.setAttribute("aria-selected", active ? "true" : "false");
});
if (capEl && spreadCaps[sIndex]) capEl.textContent = spreadCaps[sIndex];
}
thumbs.forEach(function (t) {
t.addEventListener("click", function () {
var i = parseInt(t.getAttribute("data-go"), 10) || 0;
goSpread(i);
});
});
/* arrow-key navigation across the thumb tablist */
var thumbList = document.querySelector(".inside__thumbs");
if (thumbList) {
thumbList.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
goSpread(sIndex + 1);
if (thumbs[sIndex]) thumbs[sIndex].focus();
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
goSpread(sIndex - 1);
if (thumbs[sIndex]) thumbs[sIndex].focus();
}
});
}
/* auto-advance the spread gently */
if (!prefersReduced && pages.length) {
window.setInterval(function () {
goSpread(sIndex + 1);
}, 8000);
}
/* ---------- Parallax (hero scene + spread) ---------- */
var parallaxEls = Array.prototype.slice.call(
document.querySelectorAll(".parallax")
);
var heroScene = document.querySelector(".hero__scene");
var ticking = false;
function onScroll() {
if (ticking || prefersReduced) return;
ticking = true;
window.requestAnimationFrame(function () {
var y = window.pageYOffset || document.documentElement.scrollTop;
if (heroScene) {
heroScene.style.transform = "translateY(" + y * 0.18 + "px)";
}
parallaxEls.forEach(function (el) {
var rect = el.getBoundingClientRect();
var center = rect.top + rect.height / 2 - window.innerHeight / 2;
var amt = parseFloat(el.getAttribute("data-parallax")) || 0.05;
el.style.transform = "translateY(" + center * -amt + "px)";
});
ticking = false;
});
}
if (!prefersReduced) {
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
}
/* ---------- Buy / wishlist actions ---------- */
var buyBtn = document.querySelector("[data-buy]");
var wishBtn = document.querySelector("[data-wish]");
var inCart = false;
var wished = false;
if (buyBtn) {
buyBtn.addEventListener("click", function () {
inCart = !inCart;
buyBtn.textContent = inCart ? "✓ In cart" : "Add to cart";
toast(
inCart
? "Added “The Ashfall Letters” to your cart."
: "Removed from cart."
);
});
}
if (wishBtn) {
wishBtn.addEventListener("click", function () {
wished = !wished;
wishBtn.textContent = wished ? "♥ Wishlisted" : "Add to wishlist";
toast(wished ? "Saved to your wishlist." : "Removed from wishlist.");
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Ashfall Letters — A Graphic Novel</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=Playfair+Display:ital,wght@0,500;0,600;0,700;0,800;0,900;1,500;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#synopsis">Skip to content</a>
<header class="topbar" aria-label="Primary">
<div class="wrap topbar__row">
<a class="brand" href="#top">
<span class="brand__mark" aria-hidden="true"></span>
<span class="brand__name">Umbra Press</span>
</a>
<nav class="nav" aria-label="Sections">
<a href="#synopsis">Synopsis</a>
<a href="#inside">Inside the Book</a>
<a href="#press">Press</a>
<a href="#author">Author</a>
</nav>
<a class="btn btn--ghost topbar__cta" href="#buy">Buy the Hardcover</a>
</div>
</header>
<main id="top">
<!-- HERO -->
<section class="hero" aria-label="The Ashfall Letters">
<div class="hero__scene" aria-hidden="true">
<div class="scene__sky"></div>
<div class="scene__moon"></div>
<div class="scene__ridge scene__ridge--far"></div>
<div class="scene__ridge scene__ridge--mid"></div>
<div class="scene__ridge scene__ridge--near"></div>
<div class="scene__figure"></div>
<div class="scene__ash"></div>
<div class="scene__grain"></div>
<div class="scene__duotone"></div>
<div class="scene__vignette"></div>
</div>
<div class="hero__inner wrap">
<p class="hero__eyebrow reveal">A Graphic Novel by E. R. Vale</p>
<h1 class="hero__title reveal">The Ashfall<br />Letters</h1>
<p class="hero__sub reveal">
A cartographer and a deserter trade letters across a dying border —
one map, one war, and the long quiet between them.
</p>
<div class="hero__meta reveal">
<span>Hardcover</span><span class="dot" aria-hidden="true"></span>
<span>312 pages</span><span class="dot" aria-hidden="true"></span>
<span>Full color</span><span class="dot" aria-hidden="true"></span>
<span>Mature</span>
</div>
<div class="hero__actions reveal">
<a class="btn btn--solid" href="#buy">Pre-order — $34</a>
<a class="btn btn--line" href="#inside">Read an Excerpt</a>
</div>
</div>
<div class="hero__scroll" aria-hidden="true">
<span>Scroll</span>
<span class="hero__scrollLine"></span>
</div>
</section>
<!-- PULL QUOTE / ROTATOR -->
<section class="quote" aria-label="Critical praise">
<div class="wrap">
<blockquote class="quote__rot" aria-live="polite">
<p class="quote__text" data-quote>
“A hushed, devastating book — the silences land harder than any battle.”
</p>
<footer class="quote__by" data-cite>The Paper Lantern Review</footer>
</blockquote>
<div class="quote__dots" role="tablist" aria-label="Choose a review">
<button class="quote__dot is-active" role="tab" aria-selected="true" aria-label="Review 1"></button>
<button class="quote__dot" role="tab" aria-selected="false" aria-label="Review 2"></button>
<button class="quote__dot" role="tab" aria-selected="false" aria-label="Review 3"></button>
<button class="quote__dot" role="tab" aria-selected="false" aria-label="Review 4"></button>
</div>
</div>
</section>
<!-- SYNOPSIS -->
<section class="synopsis" id="synopsis" aria-label="Synopsis">
<div class="wrap synopsis__grid">
<div class="synopsis__lead reveal">
<p class="kicker">The Story</p>
<h2 class="serif synopsis__h">Two hands, one map, a border that keeps moving.</h2>
</div>
<div class="synopsis__body reveal">
<p class="drop">
In the last winter of the Ashfall War, a military cartographer named Sable is ordered
to redraw a border that no one believes in anymore. Her only correspondent is Cael — a
deserter she has never met — whose letters arrive folded inside the maps she is meant
to correct.
</p>
<p>
What begins as coordinates becomes confession. As the front collapses and the ash keeps
falling, the two strangers build a country out of paper: a place that exists only in the
margins, in the things they cannot say to anyone else.
</p>
<p>
Painted across 312 pages in muted umber and dusk-blue, <em>The Ashfall Letters</em> is a
quiet epic about distance — the kind you can map, and the kind you cannot.
</p>
<ul class="facts" aria-label="Edition details">
<li><span>Format</span><strong>Hardcover, sewn</strong></li>
<li><span>Trim</span><strong>7 × 10 in</strong></li>
<li><span>Print</span><strong>Full color, 312 pp</strong></li>
<li><span>ISBN</span><strong>978-1-9999-0042-7</strong></li>
</ul>
</div>
</div>
</section>
<!-- INSIDE THE BOOK -->
<section class="inside" id="inside" aria-label="Inside the book">
<div class="wrap">
<div class="inside__head reveal">
<p class="kicker">Inside the Book</p>
<h2 class="serif">A look at the page spreads.</h2>
</div>
<div class="inside__layout">
<figure class="spread reveal" aria-label="Page spread preview">
<div class="spread__frame parallax" data-parallax="0.06">
<div class="spread__page spread__page--0 is-active" data-spread="0"></div>
<div class="spread__page spread__page--1" data-spread="1"></div>
<div class="spread__page spread__page--2" data-spread="2"></div>
<div class="spread__gutter" aria-hidden="true"></div>
<div class="spread__grain" aria-hidden="true"></div>
</div>
<figcaption class="spread__cap" data-spread-cap>
Spread 1 — “The first letter arrives folded in a contour line.”
</figcaption>
</figure>
<div class="inside__thumbs" role="tablist" aria-label="Choose a spread">
<button class="thumb is-active" role="tab" aria-selected="true" data-go="0">
<span class="thumb__art thumb__art--0" aria-hidden="true"></span>
<span class="thumb__label">Chapter I · The Map Room</span>
</button>
<button class="thumb" role="tab" aria-selected="false" data-go="1">
<span class="thumb__art thumb__art--1" aria-hidden="true"></span>
<span class="thumb__label">Chapter IV · The Ashfall</span>
</button>
<button class="thumb" role="tab" aria-selected="false" data-go="2">
<span class="thumb__art thumb__art--2" aria-hidden="true"></span>
<span class="thumb__label">Chapter IX · The Border</span>
</button>
</div>
</div>
</div>
</section>
<!-- AWARDS / PRESS BAND -->
<section class="press" id="press" aria-label="Awards and press">
<div class="wrap">
<p class="press__lede reveal">Acclaim & recognition</p>
<ul class="press__band reveal">
<li><strong>Eisner</strong><span>Nominee · Best Graphic Album</span></li>
<li><strong>Harvey</strong><span>Winner · Book of the Year</span></li>
<li><strong>Ignatz</strong><span>Outstanding Artist</span></li>
<li><strong>Eisner-Hale</strong><span>Critics' Selection</span></li>
<li><strong>Lantern</strong><span>Year-End Best</span></li>
</ul>
</div>
</section>
<!-- AUTHOR NOTE -->
<section class="author" id="author" aria-label="About the author">
<div class="wrap author__grid">
<div class="author__portrait reveal" aria-hidden="true">
<div class="portrait__art"></div>
<div class="portrait__grain"></div>
</div>
<div class="author__note reveal">
<p class="kicker">From the Author</p>
<h2 class="serif">A note from E. R. Vale</h2>
<p>
“I started <em>The Ashfall Letters</em> as a single drawing: two hands holding the same
map from opposite edges. Everything else — the war, the winter, the years of
correspondence — grew out of the question that drawing asked. Who do we become when the
only honest country we have is the one we write to a stranger?”
</p>
<p>
“I painted every page by hand in muted earths, because I wanted the book to feel like
something recovered rather than invented — a bundle of letters someone kept too long.”
</p>
<p class="author__sign">— E. R. Vale, 2026</p>
</div>
</div>
</section>
<!-- BUY CTA -->
<section class="buy" id="buy" aria-label="Buy the hardcover">
<div class="wrap buy__grid">
<div class="buy__cover reveal" aria-hidden="true">
<div class="cover__face">
<span class="cover__pub">Umbra Press</span>
<span class="cover__title">The Ashfall Letters</span>
<span class="cover__art"></span>
<span class="cover__author">E. R. Vale</span>
</div>
<div class="cover__spine"></div>
</div>
<div class="buy__panel reveal">
<p class="kicker">Limited First Printing</p>
<h2 class="serif">Buy the hardcover.</h2>
<p class="buy__copy">
Sewn binding, foil-stamped boards, and a 16-page sketchbook section. First printing is
numbered and signed.
</p>
<div class="buy__price">
<span class="buy__amt">$34</span>
<span class="buy__old">$42</span>
<span class="buy__tag">First printing</span>
</div>
<div class="buy__actions">
<button class="btn btn--solid" type="button" data-buy>Add to cart</button>
<button class="btn btn--line" type="button" data-wish>Add to wishlist</button>
</div>
<p class="buy__ship">Ships Sept 2026 · Free shipping over $40</p>
</div>
</div>
</section>
</main>
<footer class="foot">
<div class="wrap foot__row">
<span>© 2026 Umbra Press. A fictional imprint.</span>
<span class="foot__set">Set in Playfair Display & Inter.</span>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Graphic Novel Landing
A mature, cinematic landing page for the fictional graphic novel The Ashfall Letters by E. R. Vale, art-directed in restrained, painterly trade dress rather than loud comic-book flash. The full-bleed hero is drawn entirely in CSS — a dusk sky, layered ridgelines, a lone silhouette, a hanging moon, and a slow drift of falling ash — finished with a film-grain wash and a sepia-and-dusk duotone overlay. A literary Playfair Display title and synopsis sit over the scene with an understated price and excerpt call-to-action.
Below the fold, an inverted band cycles a pull-quote rotator of fictional press blurbs with a fading crossfade and clickable dots. The synopsis section pairs a sticky serif headline with a drop-cap lead paragraph and an edition-facts card, and the Inside the Book section previews three painterly page spreads that crossfade on click, auto-advance, and respond to arrow keys, with a gentle scroll parallax on the framed spread and the hero scene. An awards band, a serif author note with a CSS portrait, and a buy-the-hardcover CTA — featuring a 3D-tilted cover and cart and wishlist buttons with toast feedback — close the page.
Everything reveals on scroll via IntersectionObserver, the controls are keyboard-usable, and all motion (parallax, ash, crossfades, auto-rotation) is disabled under prefers-reduced-motion.
Illustrative UI only — fictional series, characters, and data.