A promo site for Tikhon Makarov's book: a ritual-like preloader, a parallax scene with a video sky and dust particles, content built on questions, four ways to buy, and orders delivered to the author's Telegram via a Vercel serverless function. Vanilla HTML/CSS/JS, zero frameworks.
Tikhon Makarov is a mathematician by training who spent about five years in monasteries, two years on pilgrimages and nearly fifteen years serving as a theologian. His book The Heart of Faith: A Path to Truth is an attempt to find a single core across spiritual traditions without blending or flattening them.
The book needed a site that does three things: sells (print, e-book, wholesale), conveys the book's mood before the first page, and doesn't leave the reader alone after they finish.
"Religion and spiritual search slide easily into either church aesthetics or esoteric kitsch. We needed a calm, expensive-feeling site, neutral toward any tradition, that speaks in questions rather than statements."
The second task was practical: the author has no CRM and no managers. Orders must arrive where he actually is — Telegram — with no admin panels or extra services.
The site doesn't open with a header and a "Buy" button. The first screen is a dark preloader with star dust and one line: "I invite you on a journey. Shall we walk this path together?" The "Perhaps…" button appears only once video, images and fonts are loaded — after the click the scene opens without a stutter.
The click is also the first user gesture that lifts the browser's autoplay restriction: wind and bells fade in.

The main screen — "If God is one, why do we see Him so differently?" — arches over a mountain valley in three layers: sky, mountains, foreground. The layers move at different speeds with scroll and mouse; above them a canvas of dust particles. The sky isn't an image but a video, cross-faded between two copies to hide the loop seam.

Scroll is smoothed: layers follow an interpolated value, and the scene's darkening is synced to that value rather than raw scroll. Without this, on a fast scroll back the black curtain lifted before the layers returned and a seam showed between them.
function parallaxLoop() {
scrollTarget = window.scrollY;
curX += (targetX - curX) * 0.06; // cursor
curY += (targetY - curY) * 0.06;
scrollCur += (scrollTarget - scrollCur) * 0.05; // soft lag → "flow"
layers.forEach((l) => {
const d = parseFloat(l.dataset.depth);
const ty = offset + curY * d * 40 - scrollCur * d * 1.45;
l.style.transform = `translate3d(${curX * d * 60}px, ${ty}px, 0) scale(1.08)`;
});
// the curtain rides the same smoothed scrollCur — otherwise a seam shows between layers
fadeout.style.opacity = clamp((scrollCur - FADE_START) / FADE_RANGE);
requestAnimationFrame(parallaxLoop);
}
Phones get separate portrait layer variants (data-img-mobile), their own offset coefficients (data-offset-mobile) and an earlier darkening so cropped layer edges never show on scroll. Viewport height is pinned with the "jumping" iOS address bar in mind: recalculation only on orientation change or a large height change.

The promo block doesn't persuade — it asks. Then a cover mosaic of faces from different traditions, a scroll with a quote from the book, "This book is for you if…", the book's eight steps as cards, "Lines worth keeping", "What makes it unique", the author, a free PDF sample. Only after all that — "Order".



Typography: Geometria for headings, Manrope for text, near-black ground, warm gold accent. One easing curve for every animation so movement across the site feels like one.

The order block offers a choice: Wildberries, direct from the publisher, the e-book via a bot in Telegram or MAX (the bot takes payment and delivers the file itself), wholesale.

The "publisher" and "wholesale" forms post to a Vercel serverless function that forwards the order to the author's Telegram. Bot token and chat_id live only in environment variables. Fields are sanitised and length-capped, multiple recipients are supported — an order counts as delivered if it reached at least one.
// api/order.js — Vercel Serverless Function
const clean = (v, n) => (v == null ? "" : String(v).trim().slice(0, n));
const name = clean(body.name, 200), phone = clean(body.phone, 50);
const results = await Promise.allSettled(
chatIds.map((id) => fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: id, text, disable_web_page_preview: true }),
}).then((r) => { if (!r.ok) throw new Error("tg " + r.status); }))
);
const ok = results.some((r) => r.status === "fulfilled"); // one recipient is enough
A separate readers.html for those who have read the book: "I'm sincerely glad to see you here." The author's Telegram and MAX channels, direct contact and an introduction to the people who supported the book's release — so readers can find like-minded people without "mass seminars", which the author doesn't hold.

setInterval, not requestAnimationFrame. rAF freezes in a background tab, so the smooth fade-in got stuck at zero. An interval always runs.timeupdate with ended as a fallback — the event ticks even when the tab is unfocused.font-display: swap, video with preload="none" and a poster.// smooth volume ramp — keeps working in a background tab
function rampVolume(audio, to, ms) {
const start = audio.volume, t0 = performance.now();
clearInterval(audio.__rampTimer);
audio.__rampTimer = setInterval(() => {
const k = Math.min(1, (performance.now() - t0) / ms);
audio.volume = start + (to - start) * k;
if (k >= 1) clearInterval(audio.__rampTimer);
}, 40);
}
The site conveys the book's mood within the first ten seconds, runs with no back end or admin panel, and the author receives every order in his Telegram within a second of submission.