Game — Indie Pixel-Art Landing
A cozy indie pixel-art game landing page built entirely with HTML, CSS and vanilla JS. It features a CSS-drawn pixel campfire scene with an animated fox sprite, a Press Start 2P title under a CRT scanline overlay, a blinking press-start prompt, a wishlist toggle synced across three buttons, a pixel feature trio with hover bounce, a frame-swap animated screenshot strip you can pause, and a chiptune soundtrack teaser with a spinning disc and equalizer.
MCP
Code
/* ==========================================================================
Moss & Ember — Indie Pixel-Art Landing
Palette: retro indie pixel — gold/green/orange on dusk navy, CRT scanlines.
========================================================================== */
:root {
--bg: #1a1c2c;
--bg-2: #141625;
--panel: #262a44;
--panel-2: #2f3457;
--text: #f4f4f4;
--muted: #a3a7c4;
--line: rgba(244, 244, 244, 0.10);
--line-2: rgba(244, 244, 244, 0.20);
--accent: #ffcd75; /* gold */
--accent-2: #41ee9b; /* green */
--accent-3: #ef7d57; /* orange */
--shadow-hard: 4px 4px 0 #0c0d18;
--shadow-gold: 4px 4px 0 #b88a3e;
--shadow-green: 4px 4px 0 #1f9c63;
--r-sm: 0px;
--r-md: 0px;
--r-lg: 0px;
--font-px: "Press Start 2P", monospace;
--font-ui: "Inter", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-ui);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
img, i, .scene, .shot__viewport { image-rendering: pixelated; }
a { color: inherit; }
::selection { background: var(--accent); color: #1a1c2c; }
/* ---- CRT scanline overlay ---------------------------------------------- */
.crt {
position: fixed;
inset: 0;
z-index: 9000;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.18) 3px,
rgba(0, 0, 0, 0.18) 3px
);
mix-blend-mode: multiply;
}
.crt::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(120% 100% at 50% 0%, transparent 60%, rgba(10, 11, 24, 0.55) 100%);
}
/* ---- Buttons ----------------------------------------------------------- */
.btn {
font-family: var(--font-px);
font-size: 9px;
letter-spacing: 0.5px;
line-height: 1.6;
color: #1a1c2c;
background: var(--accent);
border: 3px solid #1a1c2c;
padding: 12px 16px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
box-shadow: var(--shadow-gold);
transition: transform 0.08s steps(2), box-shadow 0.08s steps(2), background 0.1s;
image-rendering: pixelated;
}
.btn:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 #b88a3e; }
.btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 #b88a3e; }
.btn:focus-visible {
outline: 3px solid var(--accent-2);
outline-offset: 3px;
}
.btn--lg { font-size: 11px; padding: 16px 22px; }
.btn--sm { font-size: 8px; padding: 9px 12px; }
.btn--gold { background: var(--accent); color: #1a1c2c; box-shadow: var(--shadow-gold); }
.btn--gold:hover { box-shadow: 6px 6px 0 #b88a3e; }
.btn--gold:active { box-shadow: 1px 1px 0 #b88a3e; }
.btn--ghost {
background: transparent;
color: var(--text);
border-color: var(--line-2);
box-shadow: 4px 4px 0 #0c0d18;
}
.btn--ghost:hover { color: var(--accent-2); border-color: var(--accent-2); box-shadow: 6px 6px 0 #0c0d18; }
.btn--ghost:active { box-shadow: 1px 1px 0 #0c0d18; }
.btn__heart { color: var(--accent-3); }
.btn.is-wished { background: var(--accent-2); box-shadow: var(--shadow-green); }
.btn.is-wished:hover { box-shadow: 6px 6px 0 #1f9c63; }
.btn.is-wished .btn__heart { color: #1a1c2c; }
/* ---- Nav --------------------------------------------------------------- */
.nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 20px;
padding: 14px clamp(16px, 4vw, 40px);
background: rgba(20, 22, 37, 0.92);
border-bottom: 3px solid #0c0d18;
backdrop-filter: blur(6px);
}
.nav__brand {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.nav__sprite {
width: 18px;
height: 18px;
display: inline-block;
background:
/* tiny fox-head sprite from hard box-shadows */
var(--accent-3);
box-shadow:
6px 0 0 var(--accent-3),
0 6px 0 var(--accent-3),
6px 6px 0 var(--accent-3),
0 -6px 0 var(--accent),
12px 0 0 var(--accent-3),
12px 6px 0 var(--accent-3);
margin: 6px 0 0 0;
}
.nav__title {
font-family: var(--font-px);
font-size: 11px;
color: var(--accent);
letter-spacing: 0.5px;
}
.nav__links {
margin-left: auto;
display: flex;
gap: 22px;
}
.nav__links a {
font-family: var(--font-px);
font-size: 8px;
text-decoration: none;
color: var(--muted);
letter-spacing: 0.5px;
padding: 4px 0;
border-bottom: 2px solid transparent;
transition: color 0.12s, border-color 0.12s;
}
.nav__links a:hover { color: var(--accent-2); border-color: var(--accent-2); }
/* ---- Hero -------------------------------------------------------------- */
.hero {
position: relative;
text-align: center;
padding: clamp(28px, 6vw, 64px) clamp(16px, 5vw, 40px) 56px;
background:
linear-gradient(180deg, #2a1f3d 0%, #34243f 30%, var(--bg) 100%);
overflow: hidden;
}
.hero__sky {
position: absolute;
inset: 0;
pointer-events: none;
}
.px-star {
position: absolute;
width: 3px;
height: 3px;
background: var(--accent);
box-shadow: 0 0 0 1px rgba(255, 205, 117, 0.4);
animation: twinkle 2.4s steps(2, jump-none) infinite;
}
.px-star.s1 { top: 12%; left: 18%; }
.px-star.s2 { top: 22%; left: 72%; animation-delay: 0.5s; }
.px-star.s3 { top: 9%; left: 48%; animation-delay: 1.1s; }
.px-star.s4 { top: 30%; left: 30%; animation-delay: 1.6s; }
.px-star.s5 { top: 16%; left: 86%; animation-delay: 0.9s; }
.px-star.s6 { top: 26%; left: 8%; animation-delay: 1.9s; }
@keyframes twinkle { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
.px-moon {
position: absolute;
top: 8%;
right: 12%;
width: 44px;
height: 44px;
background: #fef3d0;
box-shadow:
0 0 0 4px rgba(254, 243, 208, 0.15),
-10px 8px 0 0 #2a1f3d inset;
}
.px-cloud {
position: absolute;
height: 10px;
background: rgba(244, 244, 244, 0.10);
box-shadow: 14px 0 0 rgba(244, 244, 244, 0.10), 28px 0 0 rgba(244, 244, 244, 0.07);
}
.px-cloud.c1 { top: 18%; left: -40px; width: 30px; animation: drift 26s linear infinite; }
.px-cloud.c2 { top: 34%; left: -40px; width: 22px; animation: drift 38s linear infinite; animation-delay: -10s; }
@keyframes drift { from { transform: translateX(0); } to { transform: translateX(120vw); } }
/* CSS pixel scene */
.scene {
position: relative;
width: min(420px, 86vw);
height: 200px;
margin: 8px auto 28px;
z-index: 2;
}
.scene__hills {
position: absolute;
left: 0; right: 0; bottom: 28px;
height: 64px;
background:
radial-gradient(60px 64px at 22% 100%, #2c4a37 0 60%, transparent 61%),
radial-gradient(80px 80px at 70% 100%, #25402f 0 60%, transparent 61%);
}
.scene__ground {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 30px;
background: #3a2a4d;
border-top: 4px solid #4a3960;
box-shadow: 0 6px 0 #221730;
}
.px-tree {
position: absolute;
bottom: 28px;
width: 26px;
height: 56px;
}
.px-tree.t1 { left: 8%; transform: scale(1.1); }
.px-tree.t2 { right: 10%; transform: scale(0.85); }
.px-tree__top {
position: absolute;
top: 0; left: 0;
width: 26px; height: 38px;
background: #2c8a4f;
box-shadow: 0 -8px 0 -3px #36a85f, 0 8px 0 0 #1f6b3c;
}
.px-tree__trunk {
position: absolute;
bottom: 0; left: 9px;
width: 8px; height: 22px;
background: #6b4a2f;
}
/* Fox sprite (animated) */
.px-fox {
position: absolute;
bottom: 30px;
left: 38%;
width: 40px;
height: 34px;
animation: foxBob 1.4s steps(2, jump-none) infinite;
}
.px-fox.is-paused { animation-play-state: paused; }
@keyframes foxBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.px-fox i { position: absolute; image-rendering: pixelated; }
.px-fox__head { top: 4px; left: 8px; width: 22px; height: 16px; background: var(--accent-3); }
.px-fox__ear { top: -2px; width: 8px; height: 8px; background: var(--accent-3); }
.px-fox__ear.l { left: 8px; }
.px-fox__ear.r { left: 24px; }
.px-fox__eye { top: 9px; width: 4px; height: 4px; background: #1a1c2c; }
.px-fox__eye.l { left: 12px; }
.px-fox__eye.r { left: 22px; }
.px-fox__body { top: 16px; left: 4px; width: 30px; height: 14px; background: #f0916c; }
.px-fox__tail { top: 14px; left: 30px; width: 12px; height: 16px; background: var(--accent); box-shadow: inset -4px 0 0 #1a1c2c; }
/* Campfire */
.px-fire {
position: absolute;
bottom: 30px;
right: 30%;
width: 26px;
height: 30px;
}
.px-fire__flame {
position: absolute;
bottom: 8px; left: 6px;
width: 14px; height: 18px;
background: var(--accent-3);
box-shadow: 0 -6px 0 -3px var(--accent);
animation: flicker 0.5s steps(2, jump-none) infinite;
}
.px-fire__core {
position: absolute;
bottom: 8px; left: 9px;
width: 8px; height: 10px;
background: var(--accent);
animation: flicker 0.4s steps(2, jump-none) infinite reverse;
}
.px-fire__log { position: absolute; bottom: 0; left: 0; width: 26px; height: 8px; background: #6b4a2f; }
@keyframes flicker { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.82) translateY(-2px); } }
.px-spark {
position: absolute;
width: 3px; height: 3px;
background: var(--accent);
bottom: 24px;
animation: rise 1.6s steps(4, jump-none) infinite;
}
.px-spark.sp1 { right: 36%; animation-delay: 0s; }
.px-spark.sp2 { right: 30%; animation-delay: 0.6s; }
.px-spark.sp3 { right: 33%; animation-delay: 1.1s; }
@keyframes rise { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-28px); opacity: 0; } }
.hero__studio {
position: relative;
z-index: 2;
font-family: var(--font-px);
font-size: 8px;
letter-spacing: 2px;
color: var(--accent-2);
margin: 0 0 14px;
}
.hero__title {
position: relative;
z-index: 2;
font-family: var(--font-px);
font-size: clamp(24px, 7vw, 54px);
line-height: 1.2;
margin: 0 0 18px;
color: var(--accent);
text-shadow: 4px 4px 0 #1a1c2c, 8px 8px 0 rgba(239, 125, 87, 0.35);
}
.hero__title .amp { color: var(--accent-3); }
.hero__tag {
position: relative;
z-index: 2;
max-width: 560px;
margin: 0 auto 16px;
color: var(--muted);
font-size: 16px;
}
.hero__press {
font-family: var(--font-px);
font-size: 11px;
color: var(--accent);
margin: 0 0 22px;
}
.hero__press.is-off { opacity: 0; }
.hero__cta {
position: relative;
z-index: 2;
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 28px;
}
.hero__meta {
position: relative;
z-index: 2;
list-style: none;
display: flex;
gap: 28px;
justify-content: center;
flex-wrap: wrap;
margin: 0;
padding: 0;
font-size: 13px;
color: var(--muted);
}
.hero__meta strong {
display: block;
font-family: var(--font-px);
font-size: 8px;
color: var(--accent-2);
margin-bottom: 6px;
letter-spacing: 0.5px;
}
/* ---- Sections ---------------------------------------------------------- */
.section {
padding: clamp(36px, 6vw, 72px) clamp(16px, 5vw, 40px);
max-width: 1080px;
margin: 0 auto;
}
.section__title {
font-family: var(--font-px);
font-size: clamp(13px, 2.4vw, 18px);
text-align: center;
color: var(--accent);
letter-spacing: 1px;
margin: 0 0 36px;
}
/* ---- Feature cards ----------------------------------------------------- */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
background: var(--panel);
border: 3px solid #0c0d18;
box-shadow: var(--shadow-hard);
padding: 24px 20px;
outline: none;
transition: transform 0.12s steps(2), box-shadow 0.12s steps(2);
}
.card:hover, .card:focus-visible {
transform: translate(-3px, -3px);
box-shadow: 7px 7px 0 #0c0d18;
}
.card.is-bounce { animation: bounce 0.4s steps(3, jump-none); }
@keyframes bounce {
0% { transform: translate(-3px, -3px); }
40% { transform: translate(-3px, -10px); }
70% { transform: translate(-3px, 0); }
100% { transform: translate(-3px, -3px); }
}
.card:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 3px; }
.card__icon {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border: 3px solid #1a1c2c;
margin-bottom: 16px;
background: var(--panel-2);
}
.card__icon--lantern { background: rgba(255, 205, 117, 0.18); }
.card__icon--friend { background: rgba(65, 238, 155, 0.18); }
.card__icon--craft { background: rgba(239, 125, 87, 0.18); }
.ico {
width: 18px;
height: 18px;
display: block;
image-rendering: pixelated;
}
.ico-lantern { background: var(--accent); box-shadow: 0 -6px 0 -3px #1a1c2c, 0 8px 0 -1px var(--accent-3); }
.ico-heart { background: var(--accent-3); box-shadow: -6px 0 0 var(--accent-3), 6px 0 0 var(--accent-3), 0 6px 0 var(--accent-3); clip-path: none; }
.ico-pot { background: var(--accent-2); box-shadow: 0 -7px 0 -4px var(--accent), 0 7px 0 -1px #1a1c2c; }
.card__title {
font-family: var(--font-px);
font-size: 11px;
color: var(--accent-2);
letter-spacing: 0.5px;
margin: 0 0 12px;
}
.card__body { color: var(--muted); font-size: 14.5px; margin: 0 0 14px; }
.card__tag {
display: inline-block;
font-family: var(--font-px);
font-size: 7px;
color: var(--accent);
border: 2px solid var(--line-2);
padding: 5px 8px;
}
/* ---- Screens strip ----------------------------------------------------- */
.screens__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin: -16px 0 28px;
flex-wrap: wrap;
}
.screens__hint { color: var(--muted); font-size: 13px; margin: 0; }
.strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.shot {
margin: 0;
background: var(--panel);
border: 3px solid #0c0d18;
box-shadow: var(--shadow-hard);
}
.shot__viewport {
position: relative;
height: 150px;
overflow: hidden;
border-bottom: 3px solid #0c0d18;
image-rendering: pixelated;
}
.shot__frame {
position: absolute;
inset: 0;
opacity: 0;
}
/* Frame-swap animation: 3 frames cycle while .is-animated */
.shot.is-animated .shot__frame.f1 { animation: frameCycle 1.2s steps(1, jump-none) infinite; animation-delay: 0s; }
.shot.is-animated .shot__frame.f2 { animation: frameCycle 1.2s steps(1, jump-none) infinite; animation-delay: 0.4s; }
.shot.is-animated .shot__frame.f3 { animation: frameCycle 1.2s steps(1, jump-none) infinite; animation-delay: 0.8s; }
@keyframes frameCycle {
0% { opacity: 1; }
33.3% { opacity: 1; }
33.4% { opacity: 0; }
100% { opacity: 0; }
}
/* default static frame when paused = f1 visible */
.shot .shot__frame.f1 { opacity: 1; }
.shot.is-animated .shot__frame.f1 { opacity: 0; }
/* Forest frames */
[data-frames="forest"] .f1 { background: linear-gradient(180deg, #1f3a2c, #16291f); }
[data-frames="forest"] .f1::after,
[data-frames="forest"] .f2::after,
[data-frames="forest"] .f3::after {
content: "";
position: absolute;
width: 6px; height: 6px;
background: var(--accent-2);
box-shadow: 30px 20px 0 var(--accent), 80px 60px 0 var(--accent-2), 120px 30px 0 var(--accent);
}
[data-frames="forest"] .f2 { background: linear-gradient(180deg, #234131, #182d22); }
[data-frames="forest"] .f2::after { left: 10px; top: 8px; }
[data-frames="forest"] .f3 { background: linear-gradient(180deg, #1c3528, #14271d); }
[data-frames="forest"] .f3::after { left: 20px; top: 4px; }
/* Marsh frames */
[data-frames="marsh"] .f1 { background: linear-gradient(180deg, #233a4a, #18293a); }
[data-frames="marsh"] .f2 { background: linear-gradient(180deg, #2a4252, #1c2f40); }
[data-frames="marsh"] .f3 { background: linear-gradient(180deg, #1f3646, #162636); }
[data-frames="marsh"] .shot__frame::before {
content: "";
position: absolute;
left: 16px; bottom: 24px;
width: 70%; height: 8px;
background: rgba(65, 238, 155, 0.5);
box-shadow: 0 14px 0 rgba(65, 238, 155, 0.25);
}
[data-frames="marsh"] .f2 .shot__frame::before { transform: translateX(8px); }
/* Peak frames */
[data-frames="peak"] .f1 { background: linear-gradient(180deg, #3a2b4d, #2a1f3a); }
[data-frames="peak"] .f2 { background: linear-gradient(180deg, #43314f, #2f223f); }
[data-frames="peak"] .f3 { background: linear-gradient(180deg, #38294a, #281d37); }
[data-frames="peak"] .shot__frame::after {
content: "";
position: absolute;
left: 50%; top: 30px; margin-left: -7px;
width: 14px; height: 18px;
background: var(--accent);
box-shadow: 0 -8px 0 -3px var(--accent-3);
}
.shot figcaption {
padding: 14px 16px 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.shot figcaption strong {
font-family: var(--font-px);
font-size: 9px;
color: var(--accent);
letter-spacing: 0.5px;
}
.shot figcaption span { color: var(--muted); font-size: 13px; }
/* ---- Soundtrack -------------------------------------------------------- */
.ost {
display: grid;
grid-template-columns: 180px 1fr;
gap: 28px;
background: var(--panel);
border: 3px solid #0c0d18;
box-shadow: var(--shadow-hard);
padding: 28px;
}
.ost__cover {
position: relative;
aspect-ratio: 1;
background:
repeating-linear-gradient(45deg, #2a1f3d 0 8px, #34243f 8px 16px);
border: 3px solid #1a1c2c;
display: grid;
place-items: center;
overflow: hidden;
}
.ost__disc {
width: 60%;
aspect-ratio: 1;
border-radius: 50%;
background:
radial-gradient(circle, var(--accent) 0 14%, #1a1c2c 15% 22%, var(--accent-3) 23% 100%);
border: 3px solid #1a1c2c;
animation: spin 4s linear infinite;
animation-play-state: paused;
}
.ost__disc.is-spinning { animation-play-state: running; }
@keyframes spin { to { transform: rotate(360deg); } }
.ost__note {
position: absolute;
font-size: 18px;
color: var(--accent-2);
opacity: 0;
}
.ost.is-playing .ost__note { animation: floatNote 1.8s steps(6, jump-none) infinite; }
.ost__note.n1 { left: 20%; bottom: 16px; animation-delay: 0s; }
.ost__note.n2 { left: 48%; bottom: 16px; animation-delay: 0.6s; }
.ost__note.n3 { left: 72%; bottom: 16px; animation-delay: 1.2s; }
@keyframes floatNote {
0% { opacity: 0; transform: translateY(0); }
20% { opacity: 1; }
100% { opacity: 0; transform: translateY(-60px); }
}
.ost__name {
font-family: var(--font-px);
font-size: 12px;
color: var(--accent);
margin: 0 0 8px;
line-height: 1.5;
}
.ost__by { color: var(--muted); font-size: 13px; margin: 0 0 18px; }
.ost__player {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.eq {
display: flex;
align-items: flex-end;
gap: 3px;
height: 28px;
}
.eq i {
width: 4px;
height: 6px;
background: var(--accent-2);
display: block;
}
.ost.is-playing .eq i { animation: bars 0.6s steps(4, jump-none) infinite; }
.eq i:nth-child(odd) { animation-duration: 0.5s; }
.eq i:nth-child(3n) { animation-duration: 0.7s; background: var(--accent); }
.eq i:nth-child(4n) { animation-duration: 0.45s; background: var(--accent-3); }
@keyframes bars { 0%, 100% { height: 6px; } 50% { height: 26px; } }
.ost__time { font-family: var(--font-px); font-size: 8px; color: var(--muted); }
.ost__tracks { list-style: none; margin: 0; padding: 0; }
.ost__tracks li {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-top: 2px solid var(--line);
font-size: 14px;
color: var(--text);
}
.trk-no { font-family: var(--font-px); font-size: 8px; color: var(--accent-2); }
.trk-len { margin-left: auto; font-family: var(--font-px); font-size: 8px; color: var(--muted); }
/* ---- Finale ------------------------------------------------------------ */
.finale {
text-align: center;
padding: clamp(48px, 8vw, 96px) 20px;
background:
linear-gradient(180deg, var(--bg) 0%, #241a33 100%);
border-top: 3px solid #0c0d18;
}
.finale__kicker {
font-family: var(--font-px);
font-size: 8px;
color: var(--accent-2);
letter-spacing: 1.5px;
margin: 0 0 16px;
}
.finale__title {
font-family: var(--font-px);
font-size: clamp(22px, 6vw, 44px);
color: var(--accent);
text-shadow: 4px 4px 0 #1a1c2c;
margin: 0 0 28px;
}
.finale__count { margin: 22px 0 0; color: var(--muted); font-size: 14px; }
.finale__count strong { color: var(--accent); font-family: var(--font-px); font-size: 12px; }
/* ---- Footer ------------------------------------------------------------ */
.footer {
background: var(--bg-2);
border-top: 3px solid #0c0d18;
padding: 36px clamp(16px, 5vw, 40px) 28px;
}
.footer__inner {
max-width: 1080px;
margin: 0 auto;
display: flex;
justify-content: space-between;
gap: 28px;
flex-wrap: wrap;
}
.footer__brand { display: flex; flex-direction: column; gap: 8px; }
.footer__studio { font-family: var(--font-px); font-size: 11px; color: var(--accent); margin: 4px 0 0; }
.footer__small { color: var(--muted); font-size: 12.5px; margin: 0; }
.footer__links { display: flex; gap: 24px; flex-wrap: wrap; }
.footer__links a {
font-family: var(--font-px);
font-size: 8px;
color: var(--muted);
text-decoration: none;
letter-spacing: 0.5px;
transition: color 0.12s;
}
.footer__links a:hover { color: var(--accent-2); }
.footer__legal {
max-width: 1080px;
margin: 28px auto 0;
padding-top: 20px;
border-top: 2px solid var(--line);
color: var(--muted);
font-size: 12px;
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.footer__insert {
font-family: var(--font-px);
font-size: 8px;
color: var(--accent);
animation: blink 1.06s steps(1, jump-none) infinite;
}
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.15; } }
/* ---- Toast ------------------------------------------------------------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 140%);
z-index: 9500;
background: var(--panel-2);
border: 3px solid var(--accent-2);
box-shadow: var(--shadow-green);
padding: 14px 20px;
font-family: var(--font-px);
font-size: 9px;
color: var(--text);
letter-spacing: 0.5px;
max-width: 88vw;
transition: transform 0.18s steps(3, jump-none);
}
.toast.is-visible { transform: translate(-50%, 0); }
/* ---- Responsive -------------------------------------------------------- */
@media (max-width: 860px) {
.features, .strip { grid-template-columns: 1fr 1fr; }
.ost { grid-template-columns: 140px 1fr; }
}
@media (max-width: 520px) {
.nav__links { display: none; }
.features, .strip { grid-template-columns: 1fr; }
.ost { grid-template-columns: 1fr; }
.ost__cover { max-width: 180px; margin: 0 auto; }
.hero__meta { gap: 18px; }
.hero__cta .btn { width: 100%; justify-content: center; }
.footer__inner { flex-direction: column; }
.footer__legal { flex-direction: column; }
.scene { height: 170px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}/* ==========================================================================
Moss & Ember — Indie Pixel-Art Landing
Vanilla JS: blinking press-start, sprite/anim toggle, card bounce,
wishlist toggle (synced across buttons), chiptune EQ teaser, toasts.
========================================================================== */
(function () {
"use strict";
/* ---- Toast helper ----------------------------------------------------- */
const toastEl = document.getElementById("toast");
let toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("is-visible"), 2400);
}
/* ---- Blinking PRESS START -------------------------------------------- */
const press = document.getElementById("pressStart");
if (press) {
setInterval(() => press.classList.toggle("is-off"), 530);
}
/* ---- Wishlist toggle (shared state) ---------------------------------- */
const WISH_BASE = 48217;
let wished = false;
const wishButtons = [
document.getElementById("navWishlist"),
document.getElementById("heroWishlist"),
document.getElementById("finaleWishlist"),
].filter(Boolean);
const wishCountEl = document.getElementById("wishCount");
function renderWishlist() {
const count = WISH_BASE + (wished ? 1 : 0);
if (wishCountEl) wishCountEl.textContent = count.toLocaleString("en-US");
wishButtons.forEach((btn) => {
btn.classList.toggle("is-wished", wished);
btn.setAttribute("aria-pressed", String(wished));
const label = btn.querySelector("[data-wishlist-label]");
if (label) label.textContent = wished ? "Wishlisted ✓" : "Wishlist on Steam";
});
}
function toggleWishlist() {
wished = !wished;
renderWishlist();
toast(wished ? "ADDED TO WISHLIST — thank you!" : "Removed from wishlist");
}
wishButtons.forEach((btn) => btn.addEventListener("click", toggleWishlist));
renderWishlist();
/* ---- Hero fox sprite tap (bounce / lantern) -------------------------- */
const fox = document.getElementById("heroFox");
if (fox) {
fox.parentElement.style.cursor = "pointer";
fox.parentElement.addEventListener("click", () => {
fox.classList.toggle("is-paused");
toast(fox.classList.contains("is-paused") ? "Ember takes a nap…" : "Ember wakes up!");
});
}
/* ---- Trailer button (fake) ------------------------------------------- */
const trailer = document.getElementById("trailerBtn");
if (trailer) {
trailer.addEventListener("click", () => toast("Trailer is a fictional demo — no video here."));
}
/* ---- Screenshot frame animation toggle ------------------------------- */
const animToggle = document.getElementById("animToggle");
const shots = Array.prototype.slice.call(document.querySelectorAll(".shot"));
let animating = true;
function renderAnim() {
shots.forEach((s) => s.classList.toggle("is-animated", animating));
if (animToggle) {
animToggle.textContent = animating ? "❚❚ Pause animation" : "▶ Play animation";
animToggle.setAttribute("aria-pressed", String(animating));
}
}
if (animToggle) {
animToggle.addEventListener("click", () => {
animating = !animating;
renderAnim();
toast(animating ? "Screens animating" : "Screens paused");
});
}
renderAnim();
/* ---- Card hover/focus bounce ----------------------------------------- */
document.querySelectorAll(".card").forEach((card) => {
const bounce = () => {
card.classList.remove("is-bounce");
// force reflow so the animation can restart
void card.offsetWidth;
card.classList.add("is-bounce");
};
card.addEventListener("mouseenter", bounce);
card.addEventListener("focus", bounce);
card.addEventListener("animationend", () => card.classList.remove("is-bounce"));
});
/* ---- Chiptune teaser player ------------------------------------------ */
const ostBtn = document.getElementById("ostPlay");
const ostWrap = document.querySelector(".ost");
const ostDisc = document.querySelector(".ost__disc");
const ostTime = document.getElementById("ostTime");
const TOTAL = 42; // seconds
let playing = false;
let elapsed = 0;
let clock = null;
function fmt(s) {
const m = Math.floor(s / 60);
const sec = String(s % 60).padStart(2, "0");
return m + ":" + sec;
}
function renderTime() {
if (ostTime) ostTime.textContent = fmt(elapsed) + " / " + fmt(TOTAL);
}
function setPlaying(state) {
playing = state;
if (ostWrap) ostWrap.classList.toggle("is-playing", playing);
if (ostDisc) ostDisc.classList.toggle("is-spinning", playing);
if (ostBtn) {
ostBtn.textContent = playing ? "❚❚ Pause teaser" : "▶ Play teaser";
ostBtn.setAttribute("aria-pressed", String(playing));
}
if (playing) {
clock = setInterval(() => {
elapsed += 1;
if (elapsed >= TOTAL) {
elapsed = 0;
setPlaying(false);
toast("Teaser ended — wishlist for the full 22-track OST!");
return;
}
renderTime();
}, 1000);
} else {
clearInterval(clock);
}
}
if (ostBtn) {
ostBtn.addEventListener("click", () => {
setPlaying(!playing);
if (playing) toast("♪ Now playing: Title Theme (Press Start)");
});
}
renderTime();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Moss & Ember — A Cozy Pixel Adventure by Tinypatch Games</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=Press+Start+2P&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="crt" aria-hidden="true"></div>
<!-- ============ NAV ============ -->
<header class="nav">
<a class="nav__brand" href="#top" aria-label="Moss and Ember home">
<span class="nav__sprite" aria-hidden="true"></span>
<span class="nav__title">MOSS & EMBER</span>
</a>
<nav class="nav__links" aria-label="Primary">
<a href="#features">Features</a>
<a href="#screens">Screens</a>
<a href="#ost">Soundtrack</a>
</nav>
<button class="btn btn--gold btn--sm" id="navWishlist" type="button">
<span class="btn__heart" aria-hidden="true">♥</span> Wishlist
</button>
</header>
<!-- ============ HERO ============ -->
<main id="top">
<section class="hero" aria-labelledby="heroTitle">
<div class="hero__sky" aria-hidden="true">
<span class="px-star s1"></span><span class="px-star s2"></span>
<span class="px-star s3"></span><span class="px-star s4"></span>
<span class="px-star s5"></span><span class="px-star s6"></span>
<span class="px-moon"></span>
<span class="px-cloud c1"></span>
<span class="px-cloud c2"></span>
</div>
<!-- CSS pixel scene -->
<div class="scene" role="img"
aria-label="Pixel-art scene of a small fox spirit holding a lantern beside a campfire under a starry sky">
<div class="scene__hills" aria-hidden="true"></div>
<div class="px-tree t1" aria-hidden="true"><i class="px-tree__top"></i><i class="px-tree__trunk"></i></div>
<div class="px-tree t2" aria-hidden="true"><i class="px-tree__top"></i><i class="px-tree__trunk"></i></div>
<div class="px-fox" id="heroFox" aria-hidden="true">
<i class="px-fox__ear l"></i><i class="px-fox__ear r"></i>
<i class="px-fox__head"></i>
<i class="px-fox__eye l"></i><i class="px-fox__eye r"></i>
<i class="px-fox__body"></i>
<i class="px-fox__tail"></i>
</div>
<div class="px-fire" aria-hidden="true">
<i class="px-fire__flame"></i>
<i class="px-fire__core"></i>
<i class="px-fire__log"></i>
<span class="px-spark sp1"></span>
<span class="px-spark sp2"></span>
<span class="px-spark sp3"></span>
</div>
<div class="scene__ground" aria-hidden="true"></div>
</div>
<p class="hero__studio">TINYPATCH GAMES presents</p>
<h1 class="hero__title" id="heroTitle">
MOSS <span class="amp">&</span> EMBER
</h1>
<p class="hero__tag">
A cozy pixel adventure about a lost fox spirit, a dying campfire, and the
forest that remembers everything. Forage, befriend, and relight the world
— one ember at a time.
</p>
<p class="hero__press" id="pressStart" aria-live="polite">▶ PRESS START</p>
<div class="hero__cta">
<button class="btn btn--gold btn--lg" id="heroWishlist" type="button">
<span class="btn__heart" aria-hidden="true">♥</span>
<span data-wishlist-label>Wishlist on Steam</span>
</button>
<button class="btn btn--ghost btn--lg" id="trailerBtn" type="button">
▷ Watch Trailer
</button>
</div>
<ul class="hero__meta" aria-label="Release info">
<li><strong>Release</strong> Autumn 2026</li>
<li><strong>Platforms</strong> PC · Switch · Deck Verified</li>
<li><strong>Players</strong> 1 (plus your cat)</li>
</ul>
</section>
<!-- ============ FEATURES ============ -->
<section class="section" id="features" aria-labelledby="featuresTitle">
<h2 class="section__title" id="featuresTitle">— FEATURES —</h2>
<div class="features">
<article class="card" tabindex="0">
<div class="card__icon card__icon--lantern" aria-hidden="true">
<i class="ico ico-lantern"></i>
</div>
<h3 class="card__title">RELIGHT THE WILDS</h3>
<p class="card__body">
Carry a single ember across five hand-pixelled biomes. Every lantern
you light wakes a corner of the map — and someone who lives there.
</p>
<span class="card__tag">5 biomes · 60+ lanterns</span>
</article>
<article class="card" tabindex="0">
<div class="card__icon card__icon--friend" aria-hidden="true">
<i class="ico ico-heart"></i>
</div>
<h3 class="card__title">BEFRIEND EVERYONE</h3>
<p class="card__body">
No combat, no fail states. Win over moody mushrooms, a retired knight
beetle, and a frog who runs the only tea shop in the marsh.
</p>
<span class="card__tag">34 villagers · 120 gifts</span>
</article>
<article class="card" tabindex="0">
<div class="card__icon card__icon--craft" aria-hidden="true">
<i class="ico ico-pot"></i>
</div>
<h3 class="card__title">COZY CRAFTING</h3>
<p class="card__body">
Brew soups that change the weather. Knit scarves that unlock dialogue.
Decorate your stump-house until the owls get jealous.
</p>
<span class="card__tag">88 recipes · 200 decor items</span>
</article>
</div>
</section>
<!-- ============ SCREENS ============ -->
<section class="section" id="screens" aria-labelledby="screensTitle">
<h2 class="section__title" id="screensTitle">— SCREENS —</h2>
<div class="screens__head">
<p class="screens__hint">Animated, in-engine pixel mockups.</p>
<button class="btn btn--ghost btn--sm" id="animToggle" type="button" aria-pressed="true">
❚❚ Pause animation
</button>
</div>
<div class="strip" id="screenStrip">
<figure class="shot is-animated" data-frames="forest">
<div class="shot__viewport" aria-hidden="true">
<div class="shot__frame f1"></div>
<div class="shot__frame f2"></div>
<div class="shot__frame f3"></div>
</div>
<figcaption>
<strong>The Mosslight Forest</strong>
<span>Fireflies follow your lantern at dusk.</span>
</figcaption>
</figure>
<figure class="shot is-animated" data-frames="marsh">
<div class="shot__viewport" aria-hidden="true">
<div class="shot__frame f1"></div>
<div class="shot__frame f2"></div>
<div class="shot__frame f3"></div>
</div>
<figcaption>
<strong>Toadmilk Marsh</strong>
<span>Rain ripples across every puddle tile.</span>
</figcaption>
</figure>
<figure class="shot is-animated" data-frames="peak">
<div class="shot__viewport" aria-hidden="true">
<div class="shot__frame f1"></div>
<div class="shot__frame f2"></div>
<div class="shot__frame f3"></div>
</div>
<figcaption>
<strong>Cinderwick Peak</strong>
<span>The last lantern waits above the clouds.</span>
</figcaption>
</figure>
</div>
</section>
<!-- ============ SOUNDTRACK ============ -->
<section class="section" id="ost" aria-labelledby="ostTitle">
<h2 class="section__title" id="ostTitle">— SOUNDTRACK —</h2>
<div class="ost">
<div class="ost__cover" aria-hidden="true">
<span class="ost__note n1">♪</span>
<span class="ost__note n2">♫</span>
<span class="ost__note n3">♪</span>
<i class="ost__disc"></i>
</div>
<div class="ost__info">
<h3 class="ost__name">EMBERS & CHIPTUNES — Original Score</h3>
<p class="ost__by">22 tracks by <strong>Bitsy Hollow</strong> · FM synth + tape hiss</p>
<div class="ost__player" role="group" aria-label="Chiptune teaser player">
<button class="btn btn--gold btn--sm" id="ostPlay" type="button" aria-pressed="false">
▶ Play teaser
</button>
<div class="eq" id="eq" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<span class="ost__time" id="ostTime">0:00 / 0:42</span>
</div>
<ul class="ost__tracks">
<li><span class="trk-no">01</span> Title Theme (Press Start) <span class="trk-len">1:48</span></li>
<li><span class="trk-no">02</span> Mosslight Morning <span class="trk-len">3:12</span></li>
<li><span class="trk-no">03</span> Frog Tea, Two Sugars <span class="trk-len">2:27</span></li>
<li><span class="trk-no">04</span> The Long Climb Home <span class="trk-len">4:05</span></li>
</ul>
</div>
</div>
</section>
<!-- ============ FINAL CTA ============ -->
<section class="finale" aria-label="Wishlist call to action">
<p class="finale__kicker">EVERY WISHLIST FEEDS THE FOX</p>
<h2 class="finale__title">READY, PLAYER?</h2>
<button class="btn btn--gold btn--lg" id="finaleWishlist" type="button">
<span class="btn__heart" aria-hidden="true">♥</span>
<span data-wishlist-label>Wishlist on Steam</span>
</button>
<p class="finale__count"><strong id="wishCount">48,217</strong> players already wishlisted</p>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer class="footer">
<div class="footer__inner">
<div class="footer__brand">
<span class="nav__sprite" aria-hidden="true"></span>
<p class="footer__studio">TINYPATCH GAMES</p>
<p class="footer__small">Three people, one shed, too many cats. Est. 2021.</p>
</div>
<nav class="footer__links" aria-label="Footer">
<a href="#features">Press Kit</a>
<a href="#screens">Discord</a>
<a href="#ost">Bandcamp</a>
<a href="#top">Newsletter</a>
</nav>
</div>
<p class="footer__legal">
© 2026 Tinypatch Games. Moss & Ember is a fictional game. No foxes were lost in the making.
<span class="footer__insert">INSERT COIN TO CONTINUE</span>
</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Indie Pixel-Art Landing
A charming, self-contained landing page for the fictional cozy adventure Moss & Ember by Tinypatch Games. The hero is a CSS-only pixel scene — a lantern-carrying fox spirit beside a flickering campfire under a twinkling, cloud-drifting sky — crowned by a Press Start 2P title and a blinking ▶ PRESS START prompt. A CRT scanline overlay sits above everything to sell the retro feel, while the body text stays in Inter for readability. The limited gold / green / orange palette and hard pixel box-shadows give every button and panel a crisp, stamped look.
Interaction is driven by a small vanilla-JS file with a reusable toast() helper. A single wishlist toggle keeps three buttons (nav, hero, finale) in sync, updates a live counter, and confirms with a toast. The screenshot strip plays a three-frame CSS swap animation per shot that you can pause and resume, the feature cards bounce on hover and focus, the campfire fox can be tapped to nap, and the soundtrack teaser spins a chiptune disc with an animated equalizer and a ticking time readout. Everything is keyboard-usable with visible focus rings, responsive down to 360px, and respects prefers-reduced-motion.
Illustrative UI only — fictional games, studios, characters, and data. Not engine integrations.