Browse Source
- Added a new photo gallery page with a responsive masonry layout. - Implemented infinite scrolling to load more photo cards dynamically. - Created various card components for different content types (text, image, portfolio, etc.). - Enhanced user experience with loading animations and responsive design adjustments. - Updated styles for improved visual presentation and accessibility.beauty
7 changed files with 1844 additions and 378 deletions
@ -1,15 +1,17 @@ |
|||
<template> |
|||
<TopNav /> |
|||
<TopNav v-if="!isAuthPage" /> |
|||
<div class="page-content"> |
|||
<NuxtPage /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const route = useRoute() |
|||
const isAuthPage = computed(() => route.path.startsWith('/auth')) |
|||
</script> |
|||
|
|||
<style> |
|||
.page-content { |
|||
padding-top: 64px; |
|||
/* TopNav is fixed, no space needed */ |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,648 @@ |
|||
<script setup lang="ts"> |
|||
const route = useRoute() |
|||
const activeTab = ref<'login' | 'register'>(route.query.tab === 'register' ? 'register' : 'login') |
|||
|
|||
const loginForm = reactive({ |
|||
email: '', |
|||
password: '', |
|||
}) |
|||
|
|||
const registerForm = reactive({ |
|||
username: '', |
|||
email: '', |
|||
password: '', |
|||
confirmPassword: '', |
|||
}) |
|||
|
|||
function handleLogin() { |
|||
console.log('Login:', loginForm) |
|||
} |
|||
|
|||
function handleRegister() { |
|||
console.log('Register:', registerForm) |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="auth-page"> |
|||
<!-- Left decorative panel --> |
|||
<div class="auth-panel"> |
|||
<div class="panel-content"> |
|||
<!-- Floating photo cards --> |
|||
<div class="floating-photos"> |
|||
<div class="photo-card photo-card-1"> |
|||
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=300&h=200&fit=crop" alt="" /> |
|||
</div> |
|||
<div class="photo-card photo-card-2"> |
|||
<img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=300&h=200&fit=crop" alt="" /> |
|||
</div> |
|||
<div class="photo-card photo-card-3"> |
|||
<img src="https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=300&h=200&fit=crop" alt="" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="panel-text"> |
|||
<h2>发现美的<br/>收藏世界</h2> |
|||
<p>在这里存储和展示您的收藏,与志同道合的人分享您的视觉故事</p> |
|||
</div> |
|||
|
|||
<div class="panel-deco"> |
|||
<div class="deco-circle deco-circle-1"></div> |
|||
<div class="deco-circle deco-circle-2"></div> |
|||
<div class="deco-circle deco-circle-3"></div> |
|||
<div class="deco-dots"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Right form panel --> |
|||
<div class="auth-form-panel"> |
|||
<div class="auth-card"> |
|||
<div class="auth-header"> |
|||
<h1 class="auth-title">{{ activeTab === 'login' ? '欢迎回来' : '创建账户' }}</h1> |
|||
<p class="auth-subtitle">{{ activeTab === 'login' ? '登录您的账户继续探索' : '加入我们开始探索之旅' }}</p> |
|||
</div> |
|||
|
|||
<div class="auth-tabs"> |
|||
<button |
|||
class="auth-tab" |
|||
:class="{ active: activeTab === 'login' }" |
|||
@click="activeTab = 'login'" |
|||
> |
|||
登录 |
|||
</button> |
|||
<button |
|||
class="auth-tab" |
|||
:class="{ active: activeTab === 'register' }" |
|||
@click="activeTab = 'register'" |
|||
> |
|||
注册 |
|||
</button> |
|||
</div> |
|||
|
|||
<Transition name="form-fade" mode="out-in"> |
|||
<form v-if="activeTab === 'login'" key="login" class="auth-form" @submit.prevent="handleLogin"> |
|||
<div class="form-group"> |
|||
<label for="login-email">邮箱地址</label> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<rect x="2" y="4" width="20" height="16" rx="2"/> |
|||
<path d="M22 6L12 13 2 6"/> |
|||
</svg> |
|||
<input |
|||
id="login-email" |
|||
v-model="loginForm.email" |
|||
type="email" |
|||
placeholder="your@email.com" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<div class="label-row"> |
|||
<label for="login-password">密码</label> |
|||
<a href="#" class="forgot-link">忘记密码?</a> |
|||
</div> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<rect x="3" y="11" width="18" height="11" rx="2"/> |
|||
<path d="M7 11V7a5 5 0 0110 0v4"/> |
|||
</svg> |
|||
<input |
|||
id="login-password" |
|||
v-model="loginForm.password" |
|||
type="password" |
|||
placeholder="输入密码" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<label class="remember-me"> |
|||
<input type="checkbox" /> |
|||
<span>记住我</span> |
|||
</label> |
|||
|
|||
<button type="submit" class="submit-btn"> |
|||
{{ activeTab === 'login' ? '登录' : '创建账户' }} |
|||
</button> |
|||
</form> |
|||
|
|||
<form v-else class="auth-form" @submit.prevent="handleRegister"> |
|||
<div class="form-group"> |
|||
<label for="register-username">用户名</label> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<circle cx="12" cy="8" r="4"/> |
|||
<path d="M4 20c0-4 4-6 8-6s8 2 8 6"/> |
|||
</svg> |
|||
<input |
|||
id="register-username" |
|||
v-model="registerForm.username" |
|||
type="text" |
|||
placeholder="选择一个用户名" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="register-email">邮箱地址</label> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<rect x="2" y="4" width="20" height="16" rx="2"/> |
|||
<path d="M22 6L12 13 2 6"/> |
|||
</svg> |
|||
<input |
|||
id="register-email" |
|||
v-model="registerForm.email" |
|||
type="email" |
|||
placeholder="your@email.com" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="register-password">密码</label> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<rect x="3" y="11" width="18" height="11" rx="2"/> |
|||
<path d="M7 11V7a5 5 0 0110 0v4"/> |
|||
</svg> |
|||
<input |
|||
id="register-password" |
|||
v-model="registerForm.password" |
|||
type="password" |
|||
placeholder="至少 8 位" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="register-confirm">确认密码</label> |
|||
<div class="input-wrapper"> |
|||
<svg class="input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
|||
<rect x="3" y="11" width="18" height="11" rx="2"/> |
|||
<path d="M7 11V7a5 5 0 0110 0v4"/> |
|||
<circle cx="12" cy="16" r="1" fill="currentColor"/> |
|||
</svg> |
|||
<input |
|||
id="register-confirm" |
|||
v-model="registerForm.confirmPassword" |
|||
type="password" |
|||
placeholder="再次输入密码" |
|||
required |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<button type="submit" class="submit-btn"> |
|||
创建账户 |
|||
</button> |
|||
</form> |
|||
</Transition> |
|||
|
|||
<div class="auth-divider"> |
|||
<span>或</span> |
|||
</div> |
|||
|
|||
<button class="social-btn"> |
|||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> |
|||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/> |
|||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/> |
|||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/> |
|||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/> |
|||
</svg> |
|||
使用 Google 继续 |
|||
</button> |
|||
|
|||
<p class="auth-footer"> |
|||
<template v-if="activeTab === 'login'"> |
|||
还没有账户?<a href="#" @click.prevent="activeTab = 'register'">立即注册</a> |
|||
</template> |
|||
<template v-else> |
|||
已有账户?<a href="#" @click.prevent="activeTab = 'login'">立即登录</a> |
|||
</template> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.auth-page { |
|||
min-height: 100vh; |
|||
display: flex; |
|||
} |
|||
|
|||
/* ── Left Panel ── */ |
|||
|
|||
.auth-panel { |
|||
flex: 1; |
|||
display: none; |
|||
background: var(--color-surface-dark); |
|||
padding: 48px; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
@media (min-width: 900px) { |
|||
.auth-panel { |
|||
display: flex; |
|||
} |
|||
} |
|||
|
|||
.panel-content { |
|||
position: relative; |
|||
z-index: 2; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
} |
|||
|
|||
/* ── Floating Photos ── */ |
|||
|
|||
.floating-photos { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 320px; |
|||
height: 240px; |
|||
} |
|||
|
|||
.photo-card { |
|||
position: absolute; |
|||
background: var(--color-surface-dark-elevated); |
|||
border-radius: 12px; |
|||
overflow: hidden; |
|||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); |
|||
} |
|||
|
|||
.photo-card img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
display: block; |
|||
} |
|||
|
|||
.photo-card-1 { |
|||
width: 180px; |
|||
height: 120px; |
|||
top: 0; |
|||
left: 0; |
|||
transform: rotate(-8deg); |
|||
z-index: 3; |
|||
} |
|||
|
|||
.photo-card-2 { |
|||
width: 160px; |
|||
height: 100px; |
|||
top: 20px; |
|||
right: 10px; |
|||
transform: rotate(6deg); |
|||
z-index: 2; |
|||
} |
|||
|
|||
.photo-card-3 { |
|||
width: 140px; |
|||
height: 90px; |
|||
bottom: 10px; |
|||
left: 60px; |
|||
transform: rotate(3deg); |
|||
z-index: 1; |
|||
} |
|||
|
|||
/* ── Dots Pattern ── */ |
|||
|
|||
.deco-dots { |
|||
position: absolute; |
|||
inset: 0; |
|||
background-image: radial-gradient(circle, rgba(255,255,255,0.08) 1px, transparent 1px); |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
.panel-text { |
|||
margin-top: auto; |
|||
margin-bottom: 48px; |
|||
} |
|||
|
|||
.panel-text h2 { |
|||
font-family: var(--font-display); |
|||
font-size: 42px; |
|||
font-weight: 400; |
|||
color: var(--color-on-dark); |
|||
letter-spacing: -1px; |
|||
line-height: 1.15; |
|||
margin: 0 0 20px; |
|||
} |
|||
|
|||
.panel-text p { |
|||
font-family: var(--font-body); |
|||
font-size: 16px; |
|||
color: var(--color-on-dark-soft); |
|||
line-height: 1.6; |
|||
margin: 0; |
|||
max-width: 360px; |
|||
} |
|||
|
|||
.panel-deco { |
|||
position: absolute; |
|||
inset: 0; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.deco-circle { |
|||
position: absolute; |
|||
border-radius: 50%; |
|||
background: linear-gradient(135deg, var(--color-primary) 0%, transparent 60%); |
|||
opacity: 0.15; |
|||
} |
|||
|
|||
.deco-circle-1 { |
|||
width: 400px; |
|||
height: 400px; |
|||
top: -100px; |
|||
right: -100px; |
|||
} |
|||
|
|||
.deco-circle-2 { |
|||
width: 300px; |
|||
height: 300px; |
|||
bottom: 20%; |
|||
left: -80px; |
|||
background: linear-gradient(135deg, var(--color-accent-teal) 0%, transparent 60%); |
|||
opacity: 0.1; |
|||
} |
|||
|
|||
.deco-circle-3 { |
|||
width: 200px; |
|||
height: 200px; |
|||
bottom: -50px; |
|||
right: 20%; |
|||
opacity: 0.08; |
|||
} |
|||
|
|||
/* ── Right Form Panel ── */ |
|||
|
|||
.auth-form-panel { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 48px 24px; |
|||
background: linear-gradient(135deg, var(--color-canvas) 0%, var(--color-surface-soft) 100%); |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.auth-card { |
|||
width: 100%; |
|||
max-width: 400px; |
|||
} |
|||
|
|||
/* ── Form Transition ── */ |
|||
|
|||
.form-fade-enter-active, |
|||
.form-fade-leave-active { |
|||
transition: opacity 0.2s ease, transform 0.2s ease; |
|||
} |
|||
|
|||
.form-fade-enter-from { |
|||
opacity: 0; |
|||
transform: translateX(12px); |
|||
} |
|||
|
|||
.form-fade-leave-to { |
|||
opacity: 0; |
|||
transform: translateX(-12px); |
|||
} |
|||
|
|||
.auth-header { |
|||
margin-bottom: 32px; |
|||
} |
|||
|
|||
.auth-title { |
|||
font-family: var(--font-display); |
|||
font-size: 32px; |
|||
font-weight: 400; |
|||
color: var(--color-ink); |
|||
letter-spacing: -0.5px; |
|||
margin: 0 0 8px; |
|||
} |
|||
|
|||
.auth-subtitle { |
|||
font-family: var(--font-body); |
|||
font-size: 15px; |
|||
color: var(--color-muted); |
|||
margin: 0; |
|||
} |
|||
|
|||
.auth-tabs { |
|||
display: flex; |
|||
gap: 4px; |
|||
margin-bottom: 32px; |
|||
padding: 4px; |
|||
background: var(--color-surface-card); |
|||
border-radius: 12px; |
|||
} |
|||
|
|||
.auth-tab { |
|||
flex: 1; |
|||
padding: 12px 20px; |
|||
font-family: var(--font-body); |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: var(--color-muted); |
|||
background: transparent; |
|||
border: none; |
|||
border-radius: 10px; |
|||
cursor: pointer; |
|||
transition: all 0.25s ease; |
|||
} |
|||
|
|||
.auth-tab.active { |
|||
background: var(--color-canvas); |
|||
color: var(--color-ink); |
|||
box-shadow: 0 2px 8px rgba(20, 20, 19, 0.08); |
|||
} |
|||
|
|||
.auth-form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 20px; |
|||
} |
|||
|
|||
.form-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.form-group label { |
|||
font-family: var(--font-body); |
|||
font-size: 13px; |
|||
font-weight: 500; |
|||
color: var(--color-body-strong); |
|||
letter-spacing: 0.3px; |
|||
} |
|||
|
|||
.input-wrapper { |
|||
position: relative; |
|||
} |
|||
|
|||
.input-icon { |
|||
position: absolute; |
|||
left: 14px; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
color: var(--color-muted-soft); |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.form-group input { |
|||
width: 100%; |
|||
padding: 14px 14px 14px 44px; |
|||
font-family: var(--font-body); |
|||
font-size: 15px; |
|||
color: var(--color-ink); |
|||
background: var(--color-canvas); |
|||
border: 1.5px solid var(--color-hairline); |
|||
border-radius: 12px; |
|||
outline: none; |
|||
transition: border-color 0.2s ease, box-shadow 0.2s ease; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.form-group input::placeholder { |
|||
color: var(--color-muted-soft); |
|||
} |
|||
|
|||
.form-group input:focus { |
|||
border-color: var(--color-primary); |
|||
box-shadow: 0 0 0 4px rgba(204, 120, 92, 0.12); |
|||
} |
|||
|
|||
.label-row { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.forgot-link { |
|||
font-size: 13px; |
|||
color: var(--color-primary); |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.forgot-link:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.remember-me { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
cursor: pointer; |
|||
margin-top: -4px; |
|||
} |
|||
|
|||
.remember-me input { |
|||
width: 18px; |
|||
height: 18px; |
|||
accent-color: var(--color-primary); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.remember-me span { |
|||
font-size: 13px; |
|||
color: var(--color-muted); |
|||
} |
|||
|
|||
.submit-btn { |
|||
width: 100%; |
|||
padding: 16px; |
|||
margin-top: 8px; |
|||
font-family: var(--font-body); |
|||
font-size: 15px; |
|||
font-weight: 500; |
|||
color: var(--color-on-primary); |
|||
background: var(--color-primary); |
|||
border: none; |
|||
border-radius: 12px; |
|||
cursor: pointer; |
|||
transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease; |
|||
box-shadow: 0 4px 14px rgba(204, 120, 92, 0.25); |
|||
} |
|||
|
|||
.submit-btn:hover { |
|||
background: var(--color-primary-active); |
|||
box-shadow: 0 6px 20px rgba(204, 120, 92, 0.35); |
|||
} |
|||
|
|||
.submit-btn:active { |
|||
transform: scale(0.98); |
|||
} |
|||
|
|||
.auth-divider { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
margin: 28px 0; |
|||
} |
|||
|
|||
.auth-divider::before, |
|||
.auth-divider::after { |
|||
content: ''; |
|||
flex: 1; |
|||
height: 1px; |
|||
background: var(--color-hairline); |
|||
} |
|||
|
|||
.auth-divider span { |
|||
font-size: 13px; |
|||
color: var(--color-muted-soft); |
|||
} |
|||
|
|||
.social-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
gap: 12px; |
|||
width: 100%; |
|||
padding: 14px 20px; |
|||
font-family: var(--font-body); |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: var(--color-ink); |
|||
background: var(--color-canvas); |
|||
border: 1.5px solid var(--color-hairline); |
|||
border-radius: 12px; |
|||
cursor: pointer; |
|||
transition: background 0.2s ease, border-color 0.2s ease; |
|||
} |
|||
|
|||
.social-btn:hover { |
|||
background: var(--color-surface-soft); |
|||
border-color: var(--color-hairline-soft); |
|||
} |
|||
|
|||
.auth-footer { |
|||
text-align: center; |
|||
margin-top: 28px; |
|||
font-size: 14px; |
|||
color: var(--color-muted); |
|||
} |
|||
|
|||
.auth-footer a { |
|||
color: var(--color-primary); |
|||
text-decoration: none; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.auth-footer a:hover { |
|||
text-decoration: underline; |
|||
} |
|||
</style> |
|||
@ -1,5 +1,743 @@ |
|||
<script setup lang="ts"> |
|||
const hero = { |
|||
img: 'https://picsum.photos/id/60/1200/900', |
|||
title: '这些是我的', |
|||
sub: '不是最好的,但都是有故事的', |
|||
} |
|||
|
|||
const collections = [ |
|||
{ id: 10, title: '午后', aspect: 'v', size: 'lg' }, |
|||
{ id: 20, title: '城市', aspect: 'h', size: 'sm' }, |
|||
{ id: 30, title: '海岸', aspect: 'v', size: 'md' }, |
|||
{ id: 40, title: '远行', aspect: 'h', size: 'sm' }, |
|||
{ id: 50, title: '静物', aspect: 'sq', size: 'md' }, |
|||
{ id: 60, title: '人像', aspect: 'v', size: 'lg' }, |
|||
{ id: 70, title: '夜色', aspect: 'h', size: 'md' }, |
|||
{ id: 80, title: '山川', aspect: 'v', size: 'sm' }, |
|||
{ id: 100, title: '森林', aspect: 'h', size: 'sm' }, |
|||
{ id: 110, title: '街道', aspect: 'sq', size: 'lg' }, |
|||
{ id: 120, title: '雨季', aspect: 'v', size: 'md' }, |
|||
{ id: 130, title: '晴空', aspect: 'h', size: 'sm' }, |
|||
{ id: 140, title: '咖啡', aspect: 'sq', size: 'md' }, |
|||
{ id: 150, title: '旅途', aspect: 'v', size: 'lg' }, |
|||
{ id: 160, title: '废墟', aspect: 'h', size: 'sm' }, |
|||
{ id: 170, title: '浪潮', aspect: 'v', size: 'md' }, |
|||
] |
|||
|
|||
function imgSrc(id: number, aspect: string, h: number) { |
|||
const sizes: Record<string, number> = { h: 900, v: 600, sq: 700 } |
|||
const w = sizes[aspect] ?? 700 |
|||
return `https://picsum.photos/id/${id}/${w}/${h}` |
|||
} |
|||
|
|||
// ── Animation refs ── |
|||
const heroRef = ref<HTMLElement | null>(null) |
|||
const heroImgRef = ref<HTMLElement | null>(null) |
|||
const bentoItems = ref<HTMLElement[]>([]) |
|||
const dwPhotos = ref<HTMLElement[]>([]) |
|||
const scrollY = ref(0) |
|||
|
|||
function onScroll() { |
|||
scrollY.value = window.scrollY |
|||
if (heroImgRef.value) { |
|||
const progress = scrollY.value * 0.35 |
|||
heroImgRef.value.style.transform = `translateY(${progress}px) scale(1.0)` |
|||
} |
|||
} |
|||
|
|||
// ── Typewriter for hero title ── |
|||
const displayTitle = ref('') |
|||
const titleText = hero.title |
|||
let charIndex = 0 |
|||
|
|||
function startTypewriter() { |
|||
if (charIndex < titleText.length) { |
|||
displayTitle.value += titleText[charIndex] |
|||
charIndex++ |
|||
setTimeout(startTypewriter, 90 + Math.random() * 60) |
|||
} |
|||
} |
|||
|
|||
// ── Bento scroll reveal ── |
|||
let bentoObserver: IntersectionObserver | null = null |
|||
|
|||
function setupBentoObserver() { |
|||
bentoObserver = new IntersectionObserver( |
|||
(entries) => { |
|||
entries.forEach((entry) => { |
|||
if (entry.isIntersecting) { |
|||
const el = entry.target as HTMLElement |
|||
const delay = Number(el.dataset.delay ?? 0) |
|||
setTimeout(() => { |
|||
el.classList.add('is-visible') |
|||
}, delay) |
|||
bentoObserver?.unobserve(el) |
|||
} |
|||
}) |
|||
}, |
|||
{ threshold: 0.08 } |
|||
) |
|||
bentoItems.value.forEach((el, i) => { |
|||
if (el) { |
|||
el.dataset.delay = String(i * 75) |
|||
bentoObserver?.observe(el) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// ── Dark wall reveal ── |
|||
let dwObserver: IntersectionObserver | null = null |
|||
|
|||
function setupDwObserver() { |
|||
dwObserver = new IntersectionObserver( |
|||
(entries) => { |
|||
entries.forEach((entry) => { |
|||
if (entry.isIntersecting) { |
|||
const el = entry.target as HTMLElement |
|||
const delay = Number(el.dataset.delay ?? 0) |
|||
setTimeout(() => { |
|||
el.classList.add('is-visible') |
|||
}, delay) |
|||
dwObserver?.unobserve(el) |
|||
} |
|||
}) |
|||
}, |
|||
{ threshold: 0.1 } |
|||
) |
|||
dwPhotos.value.forEach((el, i) => { |
|||
if (el) { |
|||
el.dataset.delay = String(i * 100) |
|||
dwObserver?.observe(el) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
onMounted(() => { |
|||
window.addEventListener('scroll', onScroll, { passive: true }) |
|||
setupBentoObserver() |
|||
setupDwObserver() |
|||
// Start typewriter after hero text animation begins |
|||
setTimeout(startTypewriter, 800) |
|||
}) |
|||
|
|||
onUnmounted(() => { |
|||
window.removeEventListener('scroll', onScroll) |
|||
bentoObserver?.disconnect() |
|||
dwObserver?.disconnect() |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
asda |
|||
</div> |
|||
</template> |
|||
<main class="canvas"> |
|||
|
|||
<!-- ── Hero ── --> |
|||
<section class="hero" ref="heroRef"> |
|||
<div class="hero-img-wrap" ref="heroImgRef"> |
|||
<img :src="hero.img" :alt="hero.title" class="hero-img" /> |
|||
<div class="hero-vignette" /> |
|||
<div class="hero-sweep" /> |
|||
</div> |
|||
<div class="hero-content"> |
|||
<div class="hero-eyebrow reveal-item">私人收藏 · 随记</div> |
|||
<h1 class="hero-title"><span class="typewriter-text">{{ displayTitle }}</span><span class="typewriter-cursor">|</span></h1> |
|||
<p class="hero-sub reveal-item">{{ hero.sub }}</p> |
|||
</div> |
|||
</section> |
|||
|
|||
<!-- ── Bento grid ── --> |
|||
<section class="bento-section"> |
|||
<div class="bento"> |
|||
<!-- Row 1 --> |
|||
<div class="b-item b-lg reveal-card" :ref="(el) => { if (el) bentoItems[0] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/10/700/1000" alt="午后" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
<div class="b-item b-col-2 reveal-card" :ref="(el) => { if (el) bentoItems[1] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/20/800/500" alt="城市" loading="lazy" /></div> |
|||
<div class="b-img-wrap mt-4"><img src="https://picsum.photos/id/40/800/500" alt="远行" loading="lazy" /></div> |
|||
</div> |
|||
<div class="b-item b-tall reveal-card" :ref="(el) => { if (el) bentoItems[2] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/30/600/1000" alt="海岸" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Row 2 — note card + medium --> |
|||
<div class="b-item b-note reveal-card" :ref="(el) => { if (el) bentoItems[3] = el as HTMLElement }"> |
|||
<p class="note-text">"每件东西都有自己的气味,闻到就穿越了"</p> |
|||
<span class="note-source">— 手记 #1</span> |
|||
</div> |
|||
<div class="b-item b-md reveal-card" :ref="(el) => { if (el) bentoItems[4] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/50/700/700" alt="静物" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Row 3 --> |
|||
<div class="b-item b-tall-right reveal-card" :ref="(el) => { if (el) bentoItems[5] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/60/600/1000" alt="人像" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
<div class="b-item b-wide reveal-card" :ref="(el) => { if (el) bentoItems[6] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/70/1200/600" alt="夜色" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
<div class="b-item b-sm reveal-card" :ref="(el) => { if (el) bentoItems[7] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/80/600/600" alt="山川" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Row 4 — note + grid --> |
|||
<div class="b-item b-note reveal-card" :ref="(el) => { if (el) bentoItems[8] = el as HTMLElement }"> |
|||
<p class="note-text">"收集的过程比结果更重要"</p> |
|||
<span class="note-source">— 手记 #2</span> |
|||
</div> |
|||
<div class="b-item b-4grid reveal-card" :ref="(el) => { if (el) bentoItems[9] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/100/600/400" alt="森林" loading="lazy" /></div> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/110/600/600" alt="街道" loading="lazy" /></div> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/120/600/800" alt="雨季" loading="lazy" /></div> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/130/600/400" alt="晴空" loading="lazy" /></div> |
|||
</div> |
|||
|
|||
<!-- Row 5 --> |
|||
<div class="b-item b-md reveal-card" :ref="(el) => { if (el) bentoItems[10] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/140/700/700" alt="咖啡" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
<div class="b-item b-tall reveal-card" :ref="(el) => { if (el) bentoItems[11] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"> |
|||
<img src="https://picsum.photos/id/150/600/1000" alt="旅途" loading="lazy" /> |
|||
</div> |
|||
</div> |
|||
<div class="b-item b-col-2-bottom reveal-card" :ref="(el) => { if (el) bentoItems[12] = el as HTMLElement }"> |
|||
<div class="b-img-wrap"><img src="https://picsum.photos/id/160/800/500" alt="废墟" loading="lazy" /></div> |
|||
<div class="b-img-wrap mt-4"><img src="https://picsum.photos/id/170/800/500" alt="浪潮" loading="lazy" /></div> |
|||
</div> |
|||
|
|||
<!-- Row 6 — note --> |
|||
<div class="b-item b-note b-note-last reveal-card" :ref="(el) => { if (el) bentoItems[13] = el as HTMLElement }"> |
|||
<p class="note-text">"有些东西扔不掉,不是因为值钱,是因为记得"</p> |
|||
<span class="note-source">— 手记 #3</span> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<!-- ── Dark feature wall ── --> |
|||
<section class="dark-wall"> |
|||
<div class="dark-wall-inner"> |
|||
<div class="dw-label">最珍贵的几件</div> |
|||
<div class="dw-photos"> |
|||
<div |
|||
class="dw-photo reveal-card" |
|||
v-for="(c, i) in collections.slice(0, 6)" |
|||
:key="c.id" |
|||
:ref="(el) => { if (el) dwPhotos[i] = el as HTMLElement }" |
|||
> |
|||
<img :src="imgSrc(c.id, c.aspect, 600)" :alt="c.title" loading="lazy" /> |
|||
<div class="dw-photo-label">{{ c.title }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<!-- ── Footer ── --> |
|||
<footer class="site-footer"> |
|||
<div class="footer-grid"> |
|||
<div class="footer-brand">收藏室</div> |
|||
<div class="footer-links"> |
|||
<NuxtLink to="/photo">全部</NuxtLink> |
|||
<NuxtLink to="/portfolio">作品集</NuxtLink> |
|||
<NuxtLink to="/about">关于</NuxtLink> |
|||
</div> |
|||
</div> |
|||
<p class="footer-copy">© 2024 · 私人收藏 · 不必打扰</p> |
|||
</footer> |
|||
</main> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.canvas { |
|||
background: var(--color-canvas); |
|||
} |
|||
|
|||
/* ── Hero ── */ |
|||
.hero { |
|||
position: relative; |
|||
height: 100vh; |
|||
min-height: 600px; |
|||
display: flex; |
|||
align-items: flex-end; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.hero-img-wrap { |
|||
position: absolute; |
|||
inset: 0; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.hero-img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
transform-origin: center center; |
|||
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1); |
|||
} |
|||
|
|||
.hero-img-wrap:hover .hero-img { |
|||
transform: scale(1.03) translateY(-4px); |
|||
} |
|||
|
|||
.hero-vignette { |
|||
position: absolute; |
|||
inset: 0; |
|||
background: linear-gradient( |
|||
to top, |
|||
rgba(20, 20, 19, 0.85) 0%, |
|||
rgba(20, 20, 19, 0.3) 50%, |
|||
transparent 100% |
|||
); |
|||
} |
|||
|
|||
/* ── Light sweep ── */ |
|||
.hero-sweep { |
|||
position: absolute; |
|||
inset: 0; |
|||
overflow: hidden; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.hero-sweep::after { |
|||
content: ''; |
|||
position: absolute; |
|||
top: -60%; |
|||
left: -80%; |
|||
width: 60%; |
|||
height: 200%; |
|||
background: linear-gradient( |
|||
105deg, |
|||
transparent 30%, |
|||
rgba(250, 249, 245, 0.06) 45%, |
|||
rgba(250, 249, 245, 0.12) 50%, |
|||
rgba(250, 249, 245, 0.06) 55%, |
|||
transparent 70% |
|||
); |
|||
transform: translateX(-100%) translateY(-20%); |
|||
animation: light-sweep 1.6s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards; |
|||
} |
|||
|
|||
@keyframes light-sweep { |
|||
0% { |
|||
transform: translateX(-100%) translateY(-20%); |
|||
opacity: 0; |
|||
} |
|||
20% { |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
transform: translateX(300%) translateY(-20%); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
.hero-content { |
|||
position: relative; |
|||
z-index: 1; |
|||
padding: 0 64px 80px; |
|||
} |
|||
|
|||
.hero-eyebrow { |
|||
font-family: var(--font-body); |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
letter-spacing: 3px; |
|||
text-transform: uppercase; |
|||
color: rgba(250, 249, 245, 0.6); |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.hero-title { |
|||
font-family: var(--font-display); |
|||
font-size: clamp(56px, 9vw, 112px); |
|||
font-weight: 400; |
|||
color: var(--color-on-dark); |
|||
letter-spacing: -3px; |
|||
line-height: 0.95; |
|||
margin: 0 0 20px; |
|||
} |
|||
|
|||
.hero-sub { |
|||
font-family: var(--font-body); |
|||
font-size: 18px; |
|||
color: rgba(250, 249, 245, 0.7); |
|||
margin: 0; |
|||
line-height: 1.6; |
|||
} |
|||
|
|||
/* ── Bento ── */ |
|||
.bento-section { |
|||
padding: 80px 40px 96px; |
|||
max-width: 1400px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.bento { |
|||
display: grid; |
|||
grid-template-columns: repeat(12, 1fr); |
|||
gap: 16px; |
|||
align-items: start; |
|||
} |
|||
|
|||
/* item helpers */ |
|||
.b-img-wrap { |
|||
position: relative; |
|||
overflow: hidden; |
|||
border-radius: 8px; |
|||
background: var(--color-surface-card); |
|||
} |
|||
|
|||
.b-img-wrap img { |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
transition: transform 0.5s var(--ease-out-quint); |
|||
} |
|||
|
|||
.b-img-wrap:hover img { |
|||
transform: scale(1.04); |
|||
} |
|||
|
|||
.mt-4 { margin-top: 16px; } |
|||
|
|||
/* Bento item sizes */ |
|||
.b-lg { grid-column: span 4; grid-row: span 2; } |
|||
.b-lg .b-img-wrap { height: 520px; } |
|||
|
|||
.b-col-2 { grid-column: span 4; } |
|||
.b-col-2 .b-img-wrap { height: 245px; } |
|||
.b-col-2 .mt-4 { margin-top: 16px; } |
|||
|
|||
.b-tall { grid-column: span 3; grid-row: span 2; } |
|||
.b-tall .b-img-wrap { height: 520px; } |
|||
|
|||
.b-tall-right { grid-column: span 3; grid-row: span 2; } |
|||
.b-tall-right .b-img-wrap { height: 520px; } |
|||
|
|||
.b-wide { grid-column: span 5; } |
|||
.b-wide .b-img-wrap { height: 320px; } |
|||
|
|||
.b-sm { grid-column: span 4; } |
|||
.b-sm .b-img-wrap { height: 320px; } |
|||
|
|||
.b-md { grid-column: span 4; } |
|||
.b-md .b-img-wrap { height: 400px; } |
|||
|
|||
.b-note { |
|||
grid-column: span 4; |
|||
background: var(--color-surface-card); |
|||
border-radius: 8px; |
|||
padding: 32px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
min-height: 200px; |
|||
} |
|||
|
|||
.b-note-last { grid-column: span 12; } |
|||
|
|||
.note-text { |
|||
font-family: var(--font-display); |
|||
font-size: clamp(18px, 2.5vw, 26px); |
|||
font-weight: 400; |
|||
color: var(--color-ink); |
|||
letter-spacing: -0.3px; |
|||
line-height: 1.4; |
|||
margin: 0 0 16px; |
|||
font-style: italic; |
|||
} |
|||
|
|||
.note-source { |
|||
font-family: var(--font-body); |
|||
font-size: 12px; |
|||
color: var(--color-muted); |
|||
letter-spacing: 1px; |
|||
text-transform: uppercase; |
|||
} |
|||
|
|||
.b-4grid { grid-column: span 8; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } |
|||
.b-4grid .b-img-wrap { height: 240px; } |
|||
|
|||
.b-col-2-bottom { grid-column: span 4; } |
|||
.b-col-2-bottom .b-img-wrap { height: 245px; } |
|||
.b-col-2-bottom .mt-4 { margin-top: 16px; } |
|||
|
|||
/* ── Dark wall ── */ |
|||
.dark-wall { |
|||
background: var(--color-surface-dark); |
|||
padding: 80px 40px; |
|||
} |
|||
|
|||
.dark-wall-inner { |
|||
max-width: 1400px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.dw-label { |
|||
font-family: var(--font-body); |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
letter-spacing: 3px; |
|||
text-transform: uppercase; |
|||
color: var(--color-on-dark-soft); |
|||
margin-bottom: 32px; |
|||
} |
|||
|
|||
.dw-photos { |
|||
display: grid; |
|||
grid-template-columns: repeat(3, 1fr); |
|||
gap: 16px; |
|||
} |
|||
|
|||
.dw-photo { |
|||
position: relative; |
|||
overflow: hidden; |
|||
border-radius: 8px; |
|||
aspect-ratio: 4/3; |
|||
} |
|||
|
|||
.dw-photo img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
transition: transform 0.5s var(--ease-out-quint); |
|||
} |
|||
|
|||
.dw-photo:hover img { |
|||
transform: scale(1.05); |
|||
} |
|||
|
|||
.dw-photo-label { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
padding: 20px 16px 12px; |
|||
background: linear-gradient(to top, rgba(20, 20, 19, 0.7) 0%, transparent 100%); |
|||
font-family: var(--font-body); |
|||
font-size: 13px; |
|||
color: rgba(250, 249, 245, 0.85); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* ── Footer ── */ |
|||
.site-footer { |
|||
background: var(--color-surface-dark); |
|||
padding: 48px 64px; |
|||
border-top: 1px solid rgba(250, 249, 245, 0.08); |
|||
} |
|||
|
|||
.footer-grid { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 32px; |
|||
} |
|||
|
|||
.footer-brand { |
|||
font-family: var(--font-display); |
|||
font-size: 28px; |
|||
font-weight: 400; |
|||
color: var(--color-on-dark); |
|||
letter-spacing: -1px; |
|||
} |
|||
|
|||
.footer-links { |
|||
display: flex; |
|||
gap: 32px; |
|||
} |
|||
|
|||
.footer-links a { |
|||
font-family: var(--font-body); |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: var(--color-on-dark-soft); |
|||
text-decoration: none; |
|||
transition: color 0.2s; |
|||
} |
|||
|
|||
.footer-links a:hover { |
|||
color: var(--color-on-dark); |
|||
} |
|||
|
|||
.footer-copy { |
|||
font-family: var(--font-body); |
|||
font-size: 13px; |
|||
color: var(--color-on-dark-soft); |
|||
margin: 0; |
|||
padding-top: 24px; |
|||
border-top: 1px solid rgba(250, 249, 245, 0.08); |
|||
} |
|||
|
|||
/* ── Mobile ── */ |
|||
@media (max-width: 768px) { |
|||
.hero-content { |
|||
padding: 0 24px 48px; |
|||
} |
|||
|
|||
.bento-section { |
|||
padding: 48px 16px; |
|||
} |
|||
|
|||
.bento { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.b-img-wrap, |
|||
.b-lg .b-img-wrap, |
|||
.b-tall .b-img-wrap, |
|||
.b-tall-right .b-img-wrap { height: 260px !important; } |
|||
|
|||
.b-note { min-height: 160px; } |
|||
.b-4grid { display: flex; flex-direction: column; } |
|||
.b-col-2 .b-img-wrap, |
|||
.b-col-2-bottom .b-img-wrap { height: 160px !important; } |
|||
.b-col-2 .mt-4, |
|||
.b-col-2-bottom .mt-4 { margin-top: 12px; } |
|||
|
|||
.dark-wall { padding: 48px 16px; } |
|||
.dw-photos { grid-template-columns: 1fr 1fr; gap: 12px; } |
|||
.dw-photo { aspect-ratio: 1; } |
|||
|
|||
.site-footer { padding: 40px 24px; } |
|||
.footer-grid { flex-direction: column; gap: 20px; } |
|||
} |
|||
|
|||
/* ── Animations ── */ |
|||
@keyframes hero-img-reveal { |
|||
from { |
|||
transform: scale(1.08); |
|||
opacity: 0.6; |
|||
} |
|||
to { |
|||
transform: scale(1.0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
@keyframes hero-text-reveal { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(28px); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
@keyframes card-reveal { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(36px); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
/* Hero entrance */ |
|||
.hero-img { |
|||
transform: scale(1.08); |
|||
animation: hero-img-reveal 1.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; |
|||
} |
|||
|
|||
.hero-eyebrow { |
|||
opacity: 0; |
|||
animation: hero-text-reveal 0.8s cubic-bezier(0.25, 1, 0.5, 1) 0.5s forwards; |
|||
} |
|||
|
|||
.hero-title { |
|||
opacity: 0; |
|||
animation: hero-text-reveal 0.6s cubic-bezier(0.25, 1, 0.5, 1) 0.65s forwards; |
|||
} |
|||
|
|||
.hero-sub { |
|||
opacity: 0; |
|||
animation: hero-text-reveal 0.8s cubic-bezier(0.25, 1, 0.5, 1) 0.85s forwards; |
|||
} |
|||
|
|||
/* Bento scroll reveal */ |
|||
.reveal-card { |
|||
opacity: 0; |
|||
transform: translateY(36px); |
|||
transition: opacity 0.65s cubic-bezier(0.25, 1, 0.5, 1), transform 0.65s cubic-bezier(0.25, 1, 0.5, 1); |
|||
} |
|||
|
|||
.reveal-card.is-visible { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
|
|||
/* Dark wall photos reveal */ |
|||
.dw-photo { |
|||
opacity: 0; |
|||
transform: translateY(24px); |
|||
transition: opacity 0.6s cubic-bezier(0.25, 1, 0.5, 1), transform 0.6s cubic-bezier(0.25, 1, 0.5, 1); |
|||
} |
|||
|
|||
.dw-photo.is-visible { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
|
|||
/* ── Reduced motion ── */ |
|||
@media (prefers-reduced-motion: reduce) { |
|||
.hero-img, |
|||
.hero-eyebrow, |
|||
.hero-title, |
|||
.hero-sub { |
|||
animation: none !important; |
|||
opacity: 1; |
|||
transform: none; |
|||
} |
|||
|
|||
.hero-sweep::after { |
|||
animation: none; |
|||
opacity: 0; |
|||
} |
|||
|
|||
.reveal-card, |
|||
.dw-photo { |
|||
opacity: 1; |
|||
transform: none; |
|||
transition: none; |
|||
} |
|||
|
|||
.typewriter-cursor { |
|||
animation: none; |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
/* ── Typewriter ── */ |
|||
.typewriter-cursor { |
|||
display: inline-block; |
|||
color: var(--color-primary); |
|||
animation: cursor-blink 1s step-end infinite; |
|||
margin-left: 2px; |
|||
} |
|||
|
|||
@keyframes cursor-blink { |
|||
0%, 100% { opacity: 1; } |
|||
50% { opacity: 0; } |
|||
} |
|||
</style> |
|||
|
|||
Loading…
Reference in new issue