Files
Dungeons-Ground/apps/web/pages/index.vue
pavel444-byte 0ade57b5a1
Some checks failed
CI / validate (push) Successful in 19m50s
CI / validate (pull_request) Failing after 1m3s
feat(home): add first-visit intro animation
Co-authored-by: multica-agent <github@multica.ai>
2026-08-22 20:07:25 +05:00

122 lines
12 KiB
Vue

<script setup lang="ts">
const { session, restore } = useDngAuth()
const entering = ref(false)
const introSeen = useCookie<string>('dng-first-visit-intro', {
default: () => '',
maxAge: 60 * 60 * 24 * 365,
sameSite: 'lax',
})
const showFirstVisitIntro = ref(introSeen.value !== 'complete')
let introTimer: number | undefined
function finishFirstVisitIntro() {
showFirstVisitIntro.value = false
if (introTimer) clearTimeout(introTimer)
window.removeEventListener('keydown', handleIntroKeydown)
}
function handleIntroKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') finishFirstVisitIntro()
}
onMounted(() => {
void restore()
if (!showFirstVisitIntro.value) return
// Mark the visit immediately so refreshing or leaving mid-animation cannot
// trap someone in the intro. The animation state stays local to this page.
introSeen.value = 'complete'
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
finishFirstVisitIntro()
return
}
window.addEventListener('keydown', handleIntroKeydown)
introTimer = window.setTimeout(finishFirstVisitIntro, 3400)
})
onBeforeUnmount(() => {
if (introTimer) clearTimeout(introTimer)
window.removeEventListener('keydown', handleIntroKeydown)
})
async function enterAlpha() {
if (entering.value) return
entering.value = true
try {
await restore()
await navigateTo(session.value ? '/dashboard' : '/auth/sign-up')
} finally {
entering.value = false
}
}
</script>
<template>
<div class="landing noise" :class="{ 'landing--introducing': showFirstVisitIntro }">
<div v-if="showFirstVisitIntro" class="first-visit-intro" role="dialog" aria-modal="true" aria-labelledby="intro-title">
<p id="intro-title" class="sr-only">Welcome to Dungeons &amp; Ground</p>
<div class="intro-grid" aria-hidden="true" />
<div class="intro-lockup" aria-hidden="true">
<svg class="intro-wordmark" viewBox="0 0 1120 300" role="img">
<g class="intro-outline">
<text x="560" y="122" text-anchor="middle">DUNGEONS</text>
<text x="560" y="242" text-anchor="middle">&amp; GROUND</text>
</g>
<g class="intro-fill">
<text x="560" y="122" text-anchor="middle">DUNGEONS</text>
<text x="560" y="242" text-anchor="middle">&amp; GROUND</text>
</g>
</svg>
<span class="intro-rule" />
</div>
<button class="intro-skip" type="button" @click="finishFirstVisitIntro">SKIP INTRO <span aria-hidden="true"></span></button>
</div>
<header class="landing-nav">
<AppMark />
<span class="alpha-tag">PRIVATE ALPHA</span>
</header>
<main class="hero">
<section class="hero-copy">
<p class="eyebrow"><span /> ASYNC AI-POWERED TTRPG</p>
<h1>STORIES THAT<br><em>WAIT FOR NO ONE.</em></h1>
<p class="lede">Build an original universe with an AI coauthor. Gather real players and persistent AI companions. Take your turn when life allowsthe Groundkeeper remembers everything.</p>
<div class="hero-actions">
<button class="btn btn-acid" :disabled="entering" @click="enterAlpha">{{ entering ? 'OPENING' : session ? 'OPEN DASHBOARD' : 'ENTER THE ALPHA' }} <span></span></button>
<a href="#how" class="btn btn-ghost">SEE HOW IT WORKS</a>
</div>
<div class="trust-row">
<span>PRIVATE WORLDS</span><i />
<span>SERVER-OWNED DICE</span><i />
<span>13+ ADVENTURES</span>
</div>
</section>
<section class="signal-card" aria-label="Live campaign preview">
<div class="signal-top"><span>LIVE SIGNAL / CAMPAIGN 04</span><b>ROUND 17</b></div>
<div class="orbit" aria-hidden="true"><i /><i /><i /><span>D&G</span></div>
<div class="transmission">
<small>GROUNDKEEPER</small>
<p>The voice in the relay is yoursthree days from now. It tells you not to open the archive.</p>
</div>
<div class="party-status"><span><b>3</b> HUMANS READY</span><span><b>1</b> AI COMPANION THINKING</span></div>
</section>
</main>
<section id="how" class="how-grid">
<article><b>01</b><h2>DESCRIBE THE IMPOSSIBLE</h2><p>A coauthor asks sharp questions, then turns your idea into a playable private world.</p></article>
<article><b>02</b><h2>ASSEMBLE YOUR PARTY</h2><p>Invite friends, add AI companions, and decide who can step in when someone is away.</p></article>
<article><b>03</b><h2>ACT ON YOUR TIME</h2><p>The Groundkeeper resolves each shared round only when the party is ready.</p></article>
<aside class="account-card"><b>ACCOUNT REQUIRED</b><h2>KEEP YOUR WORLDS.</h2><p>Register with your name, email, and password. Continue on any device and recover access when needed.</p><div><NuxtLink to="/auth/sign-up">CREATE ACCOUNT </NuxtLink><NuxtLink to="/auth/sign-in">SIGN IN</NuxtLink></div></aside>
</section>
</div>
</template>
<style scoped>
.landing{min-height:100vh;overflow:hidden}.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}.first-visit-intro{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;overflow:hidden;background:#080808;animation:intro-curtain-away 1.15s cubic-bezier(.76,0,.24,1) 2.15s both;will-change:clip-path,opacity}.intro-grid{position:absolute;inset:-20%;background-image:linear-gradient(rgba(217,247,95,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(217,247,95,.045) 1px,transparent 1px);background-size:clamp(34px,5vw,72px) clamp(34px,5vw,72px);mask-image:radial-gradient(circle at center,#000 0%,transparent 67%);opacity:0;transform:scale(1.08);animation:intro-grid-in 2.2s ease-out both}.intro-lockup{position:relative;width:min(82vw,980px);transform:scale(1.14);animation:intro-lockup-settle 2.1s cubic-bezier(.16,1,.3,1) both;will-change:transform,filter}.intro-wordmark{display:block;width:100%;overflow:visible}.intro-wordmark text{font:600 96px var(--display);letter-spacing:-.065em}.intro-outline text{fill:transparent;stroke:var(--acid);stroke-width:1.35;stroke-dasharray:1450;stroke-dashoffset:1450;paint-order:stroke;animation:intro-outline-draw 1.55s cubic-bezier(.65,0,.35,1) .08s forwards}.intro-fill text{fill:var(--ink);stroke:transparent;opacity:0;animation:intro-fill-in .75s cubic-bezier(.22,1,.36,1) 1.2s forwards}.intro-rule{position:absolute;left:50%;bottom:0;width:min(240px,32vw);height:1px;background:var(--acid);box-shadow:0 0 18px rgba(217,247,95,.7);transform:translateX(-50%) scaleX(0);animation:intro-rule-grow 1.25s cubic-bezier(.16,1,.3,1) .35s forwards}.intro-skip{position:absolute;right:clamp(20px,4vw,58px);bottom:clamp(20px,4vw,48px);padding:10px 0;border:0;background:transparent;color:var(--muted);font:500 9px var(--mono);letter-spacing:.16em;opacity:0;animation:intro-skip-in .45s ease-out .55s forwards}.intro-skip span{margin-left:8px;color:var(--acid)}.landing--introducing>.landing-nav,.landing--introducing>.hero,.landing--introducing>.how-grid{animation:homepage-reveal 1.15s cubic-bezier(.16,1,.3,1) 2.1s both;will-change:transform,opacity,filter}@keyframes intro-outline-draw{0%{stroke-dashoffset:1450;opacity:0}12%{opacity:1}100%{stroke-dashoffset:0;opacity:1}}@keyframes intro-fill-in{0%{opacity:0}100%{opacity:1}}@keyframes intro-lockup-settle{0%{transform:scale(1.14);filter:blur(5px)}18%{filter:blur(0)}100%{transform:scale(1);filter:blur(0)}}@keyframes intro-rule-grow{to{transform:translateX(-50%) scaleX(1)}}@keyframes intro-grid-in{0%{opacity:0;transform:scale(1.08)}55%{opacity:.8}100%{opacity:.35;transform:scale(1)}}@keyframes intro-skip-in{to{opacity:1}}@keyframes intro-curtain-away{0%{clip-path:inset(0 0 0 0);opacity:1}75%{opacity:1}100%{clip-path:inset(0 0 100% 0);opacity:.12;visibility:hidden}}@keyframes homepage-reveal{0%{opacity:0;transform:translateY(18px) scale(.995);filter:blur(7px)}100%{opacity:1;transform:translateY(0) scale(1);filter:blur(0)}}.landing-nav{height:90px;padding:0 clamp(22px,6vw,90px);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line)}.alpha-tag{font:500 9px var(--mono);letter-spacing:.18em;border:1px solid var(--line);padding:10px 14px;color:var(--muted)}.hero{min-height:680px;padding:72px clamp(22px,6vw,90px) 60px;display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);gap:7vw;align-items:center}.eyebrow{font:500 10px var(--mono);letter-spacing:.22em;color:var(--muted)}.eyebrow span{display:inline-block;width:26px;height:1px;background:var(--acid);vertical-align:middle;margin-right:10px}.hero h1{margin:22px 0 28px;font:600 clamp(52px,6.6vw,108px)/.91 var(--display);letter-spacing:-.07em}.hero h1 em{font-style:normal;color:transparent;-webkit-text-stroke:1px var(--acid)}.lede{max-width:650px;font-size:clamp(16px,1.5vw,20px);line-height:1.7;color:#b9b9b2}.hero-actions{display:flex;gap:12px;margin-top:38px}.btn{min-height:52px;padding:0 22px;border:1px solid var(--line);font:600 10px var(--mono);letter-spacing:.13em;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:20px}.btn:disabled{opacity:.7;cursor:wait}.btn-acid{border-color:var(--acid);background:var(--acid);color:#080808}.btn-ghost{color:var(--ink);background:transparent}.trust-row{display:flex;align-items:center;gap:14px;margin-top:42px;font:500 8px var(--mono);letter-spacing:.16em;color:var(--muted)}.trust-row i{width:3px;height:3px;background:var(--acid);border-radius:50%}.signal-card{position:relative;min-height:510px;border:1px solid var(--line);background:linear-gradient(145deg,#111 0%,#090909 60%);box-shadow:0 40px 120px #000;padding:22px;clip-path:polygon(0 0,calc(100% - 20px) 0,100% 20px,100% 100%,20px 100%,0 calc(100% - 20px))}.signal-top,.party-status{display:flex;justify-content:space-between;font:500 8px var(--mono);letter-spacing:.13em;color:var(--muted)}.signal-top b{color:var(--acid)}.orbit{position:relative;width:245px;height:245px;margin:48px auto 30px;border:1px solid #2b2b29;border-radius:50%;display:grid;place-items:center}.orbit:before,.orbit:after{content:"";position:absolute;border:1px solid #262624;border-radius:50%;inset:28px}.orbit:after{inset:61px;border-color:var(--acid-dim)}.orbit i{position:absolute;width:7px;height:7px;border-radius:50%;background:var(--acid);box-shadow:0 0 18px var(--acid)}.orbit i:nth-child(1){top:23px;left:52px}.orbit i:nth-child(2){right:-3px;top:113px}.orbit i:nth-child(3){bottom:20px;left:85px}.orbit span{font:600 28px var(--display);color:var(--acid)}.transmission{border-left:2px solid var(--acid);padding:8px 18px;margin:0 12px 34px}.transmission small{font:500 8px var(--mono);letter-spacing:.18em;color:var(--acid)}.transmission p{font:500 15px/1.6 var(--body);color:#d6d6d0}.party-status{padding:18px 12px 0;border-top:1px solid var(--line)}.party-status b{color:var(--acid)}.how-grid{display:grid;grid-template-columns:repeat(3,1fr) 1.2fr;border-top:1px solid var(--line)}.how-grid article,.account-card{min-height:215px;padding:34px;border-right:1px solid var(--line)}.how-grid article>b,.account-card>b{font:500 9px var(--mono);color:var(--acid)}.how-grid h2{margin:34px 0 12px;font:600 13px var(--display);letter-spacing:-.03em}.how-grid p{font-size:12px;line-height:1.6;color:var(--muted)}.account-card{display:flex;flex-direction:column}.account-card h2{margin-top:25px}.account-card div{display:flex;gap:14px;margin-top:auto}.account-card a{color:var(--muted);font:600 8px var(--mono);text-decoration:none}.account-card a:first-child{color:var(--acid)}@media(max-width:950px){.hero{grid-template-columns:1fr}.signal-card{max-width:620px}.how-grid{grid-template-columns:1fr 1fr}}@media(max-width:620px){.intro-lockup{width:92vw}.intro-wordmark text{font-size:82px}.intro-skip{right:20px;bottom:18px}.alpha-tag{display:none}.hero{padding-top:50px}.hero h1{font-size:48px}.hero-actions{flex-direction:column}.trust-row{flex-wrap:wrap}.signal-card{min-height:470px}.how-grid{grid-template-columns:1fr}.how-grid article,.account-card{border-bottom:1px solid var(--line)}}@media(prefers-reduced-motion:reduce){.first-visit-intro{display:none}.landing--introducing>.landing-nav,.landing--introducing>.hero,.landing--introducing>.how-grid{animation:none;opacity:1;transform:none;filter:none}}
</style>