Files
hunli/vite-tailwindcss/src/views/hb/index.vue
2026-08-03 09:31:32 +08:00

863 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="hb-root">
<!-- 全屏背景婚礼装饰 -->
<div class="hb-bg-decor" aria-hidden="true">
<span class="hb-bg-xi hb-bg-xi-1">囍</span>
<span class="hb-bg-xi hb-bg-xi-2">囍</span>
<span class="hb-bg-xi hb-bg-xi-3">囍</span>
<span class="hb-bg-xi hb-bg-xi-4">囍</span>
<span class="hb-bg-xi hb-bg-xi-5">囍</span>
<span class="hb-bg-xi hb-bg-xi-6">囍</span>
<svg class="hb-bg-knot hb-bg-knot-1" viewBox="0 0 64 64" fill="none"><use href="#hb-icon-knot" /></svg>
<svg class="hb-bg-knot hb-bg-knot-2" viewBox="0 0 64 64" fill="none"><use href="#hb-icon-knot" /></svg>
<svg class="hb-bg-knot hb-bg-knot-3" viewBox="0 0 64 64" fill="none"><use href="#hb-icon-knot" /></svg>
<svg class="hb-bg-knot hb-bg-knot-4" viewBox="0 0 64 64" fill="none"><use href="#hb-icon-knot" /></svg>
<svg class="hb-bg-lantern hb-bg-lantern-1" viewBox="0 0 40 56" fill="none"><use href="#hb-icon-lantern" /></svg>
<svg class="hb-bg-lantern hb-bg-lantern-2" viewBox="0 0 40 56" fill="none"><use href="#hb-icon-lantern" /></svg>
<i class="hb-bg-spark hb-bg-spark-1"></i>
<i class="hb-bg-spark hb-bg-spark-2"></i>
<i class="hb-bg-spark hb-bg-spark-3"></i>
<i class="hb-bg-spark hb-bg-spark-4"></i>
</div>
<!-- 共用 SVG 图标定义 -->
<svg class="hb-sprite" aria-hidden="true">
<symbol id="hb-icon-knot" viewBox="0 0 64 64">
<path d="M32 6c-4 8-14 12-14 22 0 6 4 10 10 12-6 2-10 6-10 12 0 10 10 14 14 22 4-8 14-12 14-22 0-6-4-10-10-12 6-2 10-6 10-12 0-10-10-14-14-22z" stroke="currentColor" stroke-width="2.2" fill="none"/>
<circle cx="32" cy="32" r="5" fill="currentColor" opacity="0.85"/>
<path d="M32 8v10M32 46v10M18 32h10M36 32h10" stroke="currentColor" stroke-width="1.6" opacity="0.7"/>
<path d="M22 18c6 4 14 4 20 0M22 46c6-4 14-4 20 0" stroke="currentColor" stroke-width="1.5" opacity="0.55"/>
</symbol>
<symbol id="hb-icon-lantern" viewBox="0 0 40 56">
<rect x="16" y="2" width="8" height="5" rx="1" fill="currentColor"/>
<ellipse cx="20" cy="28" rx="14" ry="18" stroke="currentColor" stroke-width="2" fill="currentColor" fill-opacity="0.18"/>
<path d="M8 20h24M8 28h24M8 36h24" stroke="currentColor" stroke-width="1.2" opacity="0.55"/>
<path d="M20 46v6M16 52h8" stroke="currentColor" stroke-width="1.6"/>
<circle cx="20" cy="28" r="3" fill="currentColor"/>
</symbol>
<symbol id="hb-icon-cloud" viewBox="0 0 80 36">
<path d="M8 28c0-8 6-14 14-14 2-8 10-12 18-10 4-6 14-8 20-2 8-2 16 4 16 12 0 8-6 12-16 12H18C10 26 8 22 8 28z" stroke="currentColor" stroke-width="1.8" fill="currentColor" fill-opacity="0.12"/>
</symbol>
<symbol id="hb-icon-corner" viewBox="0 0 48 48">
<path d="M4 44V18c0-8 6-14 14-14h26" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M10 44V24c0-6 4-10 10-10h24" stroke="currentColor" stroke-width="1.2" opacity="0.55" fill="none"/>
<circle cx="18" cy="18" r="3" fill="currentColor"/>
<path d="M18 8c6 2 10 6 12 12" stroke="currentColor" stroke-width="1.2" opacity="0.7"/>
</symbol>
</svg>
<!-- Loading -->
<div v-if="phase === 'loading'" class="hb-loading" :class="`hb-loading--${cfg.loadingEffect}`">
<div v-if="cfg.loadingEffect === 'redSeal'" class="hb-seal">囍</div>
<div v-else-if="cfg.loadingEffect === 'goldShimmer'" class="hb-shimmer">婚礼邀请函</div>
<div v-else class="hb-fade-dot"></div>
</div>
<!-- Enter curtain -->
<div
v-if="phase === 'enter'"
class="hb-enter"
:class="[`hb-enter--${cfg.enterEffect}`, { 'is-done': enterLeaving }]"
>
<div class="hb-enter-panel hb-enter-left"></div>
<div class="hb-enter-panel hb-enter-right"></div>
<div class="hb-enter-veil"></div>
</div>
<!-- Poster -->
<div v-show="phase === 'content' || phase === 'enter'" class="hb-stage" :class="{ 'is-visible': contentVisible }">
<div class="hb-frame">
<div class="hb-frame-ornament hb-frame-ornament-tl" aria-hidden="true">
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
</div>
<div class="hb-frame-ornament hb-frame-ornament-tr" aria-hidden="true">
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
</div>
<div class="hb-frame-ornament hb-frame-ornament-bl" aria-hidden="true">
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
</div>
<div class="hb-frame-ornament hb-frame-ornament-br" aria-hidden="true">
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
</div>
<div class="hb-inner">
<!-- 纸面装饰:角花、水印囍、同心结、祥云 -->
<div class="hb-paper-decor" aria-hidden="true">
<span class="hb-paper-watermark">囍</span>
<svg class="hb-paper-knot hb-paper-knot-tl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
<svg class="hb-paper-knot hb-paper-knot-tr" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
<svg class="hb-paper-knot hb-paper-knot-bl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
<svg class="hb-paper-knot hb-paper-knot-br" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
<svg class="hb-paper-cloud hb-paper-cloud-t" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
<svg class="hb-paper-cloud hb-paper-cloud-b" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
<span class="hb-paper-xi hb-paper-xi-l">囍</span>
<span class="hb-paper-xi hb-paper-xi-r">囍</span>
<span class="hb-paper-dot hb-paper-dot-1"></span>
<span class="hb-paper-dot hb-paper-dot-2"></span>
<span class="hb-paper-dot hb-paper-dot-3"></span>
<span class="hb-paper-dot hb-paper-dot-4"></span>
<div class="hb-paper-side hb-paper-side-l"></div>
<div class="hb-paper-side hb-paper-side-r"></div>
</div>
<div class="hb-hero-wrap" :class="{ 'is-show': stage >= 1 }">
<div class="hb-hero">
<img v-if="cfg.heroImg" :src="cfg.heroImg" alt="" class="hb-hero-img" />
<div v-else class="hb-hero-placeholder">婚礼照片</div>
<span class="hb-xi hb-xi-l">囍</span>
<span class="hb-xi hb-xi-r">囍</span>
</div>
</div>
<div class="hb-names" :class="{ 'is-show': stage >= 2 }">
<BlurText
v-if="stage >= 2"
:text="nameLeft"
:active="stage >= 2"
animate-by="letters"
:delay="40"
class-name="hb-name"
@animation-complete="onNamesLeftDone"
/>
<span class="hb-xi-circle">囍</span>
<BlurText
v-if="namesRightReady"
:text="nameRight"
:active="namesRightReady"
animate-by="letters"
:delay="40"
class-name="hb-name"
@animation-complete="bumpStage(3)"
/>
</div>
<div class="hb-title-wrap" :class="{ 'is-show': stage >= 3 }">
<BlurText
v-if="stage >= 3"
:text="cfg.title"
:active="stage >= 3"
animate-by="letters"
direction="bottom"
:delay="70"
:step-duration="0.4"
class-name="hb-title calligraphy-strong"
@animation-complete="bumpStage(4)"
/>
</div>
<div class="hb-body">
<template v-for="(line, i) in cfg.lines" :key="`line-${i}-${line}`">
<TextType
v-if="lineStage > i && i % 2 === 0"
:text="line"
:active="lineStage > i"
:loop="false"
:show-cursor="lineStage === i + 1"
cursor-character="|"
cursor-class-name="hb-cursor"
:typing-speed="48"
:initial-delay="80"
class-name="hb-line"
@animation-complete="onLineDone(i)"
/>
<BlurText
v-else-if="lineStage > i"
:text="line"
:active="lineStage > i"
animate-by="words"
:delay="90"
class-name="hb-line"
@animation-complete="onLineDone(i)"
/>
</template>
</div>
<div class="hb-footer" :class="{ 'is-show': stage >= 5 }">
<BlurText
v-if="stage >= 5"
:text="cfg.footer"
:active="stage >= 5"
animate-by="letters"
:delay="35"
class-name="hb-footer-text"
@animation-complete="bumpStage(6)"
/>
</div>
<div v-if="cfg.showInviteButton && stage >= 6" class="hb-cta is-show">
<button type="button" class="hb-cta-btn" @click="goInvite">
{{ cfg.inviteButtonText }}
</button>
</div>
</div>
</div>
</div>
<div v-if="!cfg.enabled && phase === 'content'" class="hb-disabled">
<p>海报暂未开放</p>
<button type="button" class="hb-cta-btn" @click="goInvite">前往请柬</button>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getPosterConfig } from '@/api/wedding'
import { normalizePosterConfig, resolvePosterSide } from '@/composables/posterDefaults'
import BlurText from '@/components/vue-bits/BlurText.vue'
import TextType from '@/components/vue-bits/TextType.vue'
const route = useRoute()
const router = useRouter()
const posterSide = computed(() => resolvePosterSide(route.meta.posterSide) || 'male')
const cfg = reactive(normalizePosterConfig(null, posterSide.value))
const phase = ref('loading') // loading | enter | content
const enterLeaving = ref(false)
const contentVisible = ref(false)
const stage = ref(0)
const lineStage = ref(0)
const namesRightReady = ref(false)
const nameLeft = computed(() => `${cfg.sonLabel || ''}${cfg.sonName || ''}`)
const nameRight = computed(() => `${cfg.daughterInLawLabel || ''}${cfg.daughterInLawName || ''}`)
let timers = []
function later(fn, ms) {
const id = setTimeout(fn, ms)
timers.push(id)
return id
}
function bumpStage(n) {
if (stage.value < n) stage.value = n
if (n === 4) {
if (!cfg.lines?.length) {
later(() => bumpStage(5), 200)
return
}
lineStage.value = 1
}
}
function onNamesLeftDone() {
namesRightReady.value = true
}
function onLineDone(i) {
if (i + 1 >= cfg.lines.length) {
bumpStage(5)
return
}
if (lineStage.value === i + 1) lineStage.value = i + 2
}
function goInvite() {
const path = cfg.invitePath || '/'
if (path.startsWith('http')) window.location.href = path
else router.push(path)
}
async function runSequence() {
const loadMs = cfg.loadingEffect === 'fade' ? 700 : 1100
await new Promise((r) => later(r, loadMs))
if (!cfg.enabled) {
phase.value = 'content'
contentVisible.value = true
return
}
phase.value = 'enter'
contentVisible.value = false
await nextTick()
const enterMs = { fadeUp: 700, scaleIn: 800, curtain: 1000 }[cfg.enterEffect] || 700
later(() => {
enterLeaving.value = true
contentVisible.value = true
}, 80)
await new Promise((r) => later(r, enterMs))
phase.value = 'content'
later(() => { stage.value = 1 }, 200)
later(() => { stage.value = 2 }, 700)
}
onMounted(async () => {
const side = posterSide.value
try {
const res = await getPosterConfig(side)
Object.assign(cfg, normalizePosterConfig(res.data || {}, side))
} catch {
Object.assign(cfg, normalizePosterConfig(null, side))
}
runSequence()
})
onUnmounted(() => {
timers.forEach(clearTimeout)
timers = []
})
</script>
<style scoped>
.hb-root {
--hb-red: #9b0000;
--hb-red-deep: #7a0000;
--hb-red-bright: #c41e3a;
--hb-gold: #d4af37;
--hb-gold-soft: #f0d78c;
--hb-cream: #f7e7c3;
position: relative;
min-height: 100dvh;
background:
radial-gradient(ellipse at 50% 18%, #c20808 0%, transparent 42%),
radial-gradient(circle at 12% 80%, rgba(212, 175, 55, 0.12), transparent 28%),
radial-gradient(circle at 88% 75%, rgba(212, 175, 55, 0.1), transparent 26%),
radial-gradient(ellipse at 50% 20%, #b30000 0%, var(--hb-red) 45%, var(--hb-red-deep) 100%);
color: var(--hb-gold);
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
box-sizing: border-box;
overflow-x: hidden;
font-family: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", serif;
}
.hb-sprite {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
/* —— 全屏背景装饰 —— */
.hb-bg-decor {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.hb-bg-xi {
position: absolute;
color: rgba(240, 215, 140, 0.14);
font-weight: 700;
line-height: 1;
user-select: none;
animation: hb-pulse-soft 7s ease-in-out infinite;
}
.hb-bg-xi-1 { top: 6%; left: 4%; font-size: 56px; transform: rotate(-12deg); animation-delay: 0s; }
.hb-bg-xi-2 { top: 14%; right: 6%; font-size: 42px; transform: rotate(10deg); animation-delay: 1.2s; color: rgba(196, 30, 58, 0.22); }
.hb-bg-xi-3 { bottom: 18%; left: 8%; font-size: 48px; transform: rotate(8deg); animation-delay: 2s; }
.hb-bg-xi-4 { bottom: 10%; right: 10%; font-size: 64px; transform: rotate(-8deg); animation-delay: 0.6s; color: rgba(196, 30, 58, 0.18); }
.hb-bg-xi-5 { top: 42%; left: 2%; font-size: 36px; opacity: 0.8; animation-delay: 3s; }
.hb-bg-xi-6 { top: 48%; right: 3%; font-size: 40px; opacity: 0.75; animation-delay: 2.4s; }
.hb-bg-knot {
position: absolute;
color: rgba(212, 175, 55, 0.28);
width: 72px;
height: 72px;
animation: hb-float 10s ease-in-out infinite;
}
.hb-bg-knot-1 { top: 8%; left: 14%; width: 64px; height: 64px; animation-delay: 0.4s; }
.hb-bg-knot-2 { top: 22%; right: 12%; width: 56px; height: 56px; color: rgba(196, 30, 58, 0.28); animation-delay: 1.8s; }
.hb-bg-knot-3 { bottom: 22%; left: 10%; width: 60px; height: 60px; animation-delay: 2.6s; }
.hb-bg-knot-4 { bottom: 12%; right: 14%; width: 70px; height: 70px; color: rgba(240, 215, 140, 0.22); animation-delay: 1s; }
.hb-bg-lantern {
position: absolute;
color: rgba(240, 215, 140, 0.35);
width: 36px;
height: 50px;
animation: hb-swing 5.5s ease-in-out infinite;
}
.hb-bg-lantern-1 { top: 4%; left: 22%; }
.hb-bg-lantern-2 { top: 5%; right: 20%; animation-delay: 1.4s; }
.hb-bg-spark {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background: radial-gradient(circle, var(--hb-gold-soft), transparent 70%);
box-shadow: 0 0 10px rgba(240, 215, 140, 0.55);
animation: hb-twinkle 2.8s ease-in-out infinite;
}
.hb-bg-spark-1 { top: 18%; left: 30%; animation-delay: 0s; }
.hb-bg-spark-2 { top: 28%; right: 28%; animation-delay: 0.7s; }
.hb-bg-spark-3 { bottom: 30%; left: 26%; animation-delay: 1.3s; }
.hb-bg-spark-4 { bottom: 24%; right: 24%; animation-delay: 2s; }
.hb-loading {
position: fixed;
inset: 0;
z-index: 40;
display: flex;
align-items: center;
justify-content: center;
background: var(--hb-red-deep);
}
.hb-seal {
width: 88px;
height: 88px;
border-radius: 50%;
border: 3px solid var(--hb-gold);
color: var(--hb-gold);
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
font-weight: 700;
animation: hb-seal-pop 1s ease both;
box-shadow: 0 0 0 6px rgba(212, 175, 55, 0.15);
}
.hb-shimmer {
font-size: 28px;
letter-spacing: 0.35em;
background: linear-gradient(90deg, #8a6a1a, var(--hb-gold-soft), #8a6a1a);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: hb-shimmer 1.2s linear infinite;
}
.hb-fade-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--hb-gold);
animation: hb-fade-pulse 0.9s ease-in-out infinite;
}
.hb-enter {
position: fixed;
inset: 0;
z-index: 30;
pointer-events: none;
}
.hb-enter--curtain .hb-enter-panel {
position: absolute;
top: 0;
bottom: 0;
width: 50%;
background: linear-gradient(180deg, #8b0000, #5c0000);
transition: transform 0.95s cubic-bezier(0.65, 0, 0.35, 1);
}
.hb-enter--curtain .hb-enter-left { left: 0; border-right: 1px solid rgba(212, 175, 55, 0.35); }
.hb-enter--curtain .hb-enter-right { right: 0; border-left: 1px solid rgba(212, 175, 55, 0.35); }
.hb-enter--curtain.is-done .hb-enter-left { transform: translateX(-105%); }
.hb-enter--curtain.is-done .hb-enter-right { transform: translateX(105%); }
.hb-enter--fadeUp .hb-enter-veil,
.hb-enter--scaleIn .hb-enter-veil {
position: absolute;
inset: 0;
background: var(--hb-red-deep);
transition: opacity 0.7s ease, transform 0.8s ease;
}
.hb-enter--fadeUp.is-done .hb-enter-veil {
opacity: 0;
transform: translateY(-24px);
}
.hb-enter--scaleIn.is-done .hb-enter-veil {
opacity: 0;
transform: scale(1.08);
}
.hb-stage {
position: relative;
z-index: 1;
width: min(420px, 100%);
opacity: 0;
transform: translateY(18px) scale(0.98);
transition: opacity 0.6s ease, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.hb-stage.is-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
.hb-frame {
position: relative;
border: 1.5px solid var(--hb-gold);
border-radius: 10px;
padding: 8px;
background:
linear-gradient(135deg, rgba(212, 175, 55, 0.18), transparent 40%),
linear-gradient(180deg, rgba(155, 0, 0, 0.4), rgba(90, 0, 0, 0.25));
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.35),
inset 0 0 0 1px rgba(240, 215, 140, 0.2);
}
.hb-frame-ornament {
position: absolute;
width: 34px;
height: 34px;
color: var(--hb-gold-soft);
z-index: 2;
pointer-events: none;
opacity: 0.9;
}
.hb-frame-ornament svg { width: 100%; height: 100%; display: block; }
.hb-frame-ornament-tl { top: 2px; left: 2px; }
.hb-frame-ornament-tr { top: 2px; right: 2px; transform: scaleX(-1); }
.hb-frame-ornament-bl { bottom: 2px; left: 2px; transform: scaleY(-1); }
.hb-frame-ornament-br { bottom: 2px; right: 2px; transform: scale(-1); }
.hb-inner {
position: relative;
border: 1.5px solid rgba(212, 175, 55, 0.85);
border-radius: 6px;
padding: 14px 14px 20px;
background:
linear-gradient(180deg, rgba(130, 0, 0, 0.58), rgba(110, 0, 0, 0.38)),
radial-gradient(circle at 90% 8%, rgba(255, 255, 255, 0.08), transparent 28%),
radial-gradient(circle at 10% 92%, rgba(196, 30, 58, 0.2), transparent 30%);
min-height: min(780px, calc(100dvh - 40px));
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
.hb-inner::before {
content: '';
position: absolute;
inset: 5px;
border: 1px solid rgba(240, 215, 140, 0.35);
border-radius: 4px;
pointer-events: none;
z-index: 1;
clip-path: polygon(
0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px,
100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px)
);
}
.hb-inner::after {
content: '';
position: absolute;
inset: 10px;
pointer-events: none;
z-index: 1;
border: 1px dashed rgba(212, 175, 55, 0.18);
border-radius: 2px;
opacity: 0.8;
}
/* —— 纸面装饰 —— */
.hb-paper-decor {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.hb-paper-watermark {
position: absolute;
left: 50%;
top: 52%;
transform: translate(-50%, -50%);
font-size: min(220px, 58vw);
font-weight: 700;
color: rgba(196, 30, 58, 0.1);
line-height: 1;
letter-spacing: 0.05em;
text-shadow: 0 0 1px rgba(212, 175, 55, 0.15);
}
.hb-paper-knot {
position: absolute;
width: 42px;
height: 42px;
color: rgba(212, 175, 55, 0.45);
}
.hb-paper-knot-tl { top: 10px; left: 10px; }
.hb-paper-knot-tr { top: 10px; right: 10px; transform: scaleX(-1); color: rgba(240, 215, 140, 0.4); }
.hb-paper-knot-bl { bottom: 12px; left: 10px; transform: scaleY(-1); color: rgba(240, 215, 140, 0.38); }
.hb-paper-knot-br { bottom: 12px; right: 10px; transform: scale(-1); }
.hb-paper-cloud {
position: absolute;
width: 96px;
height: 42px;
color: rgba(212, 175, 55, 0.32);
}
.hb-paper-cloud-t { top: 8px; left: 50%; transform: translateX(-50%); }
.hb-paper-cloud-b { bottom: 8px; left: 50%; transform: translateX(-50%) scaleY(-1); color: rgba(196, 30, 58, 0.28); }
.hb-paper-xi {
position: absolute;
top: 46%;
font-size: 22px;
font-weight: 700;
color: rgba(196, 30, 58, 0.28);
text-shadow: 0 0 8px rgba(212, 175, 55, 0.15);
}
.hb-paper-xi-l { left: 8px; transform: rotate(-18deg); }
.hb-paper-xi-r { right: 8px; transform: rotate(18deg); }
.hb-paper-dot {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
background: rgba(212, 175, 55, 0.45);
box-shadow: 0 0 0 3px rgba(196, 30, 58, 0.12);
}
.hb-paper-dot-1 { top: 22%; left: 14px; }
.hb-paper-dot-2 { top: 22%; right: 14px; }
.hb-paper-dot-3 { bottom: 26%; left: 16px; }
.hb-paper-dot-4 { bottom: 26%; right: 16px; }
.hb-paper-side {
position: absolute;
top: 28%;
bottom: 28%;
width: 10px;
background:
repeating-linear-gradient(
180deg,
transparent 0 10px,
rgba(212, 175, 55, 0.22) 10px 11px,
transparent 11px 20px
);
opacity: 0.7;
}
.hb-paper-side-l {
left: 4px;
border-left: 1px solid rgba(212, 175, 55, 0.25);
}
.hb-paper-side-r {
right: 4px;
border-right: 1px solid rgba(212, 175, 55, 0.25);
}
.hb-hero-wrap,
.hb-names,
.hb-title-wrap,
.hb-body,
.hb-footer,
.hb-cta {
position: relative;
z-index: 2;
}
.hb-hero-wrap {
width: 100%;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.55s ease, transform 0.55s ease;
margin-bottom: 14px;
}
.hb-hero-wrap.is-show {
opacity: 1;
transform: none;
}
.hb-hero {
position: relative;
border: 1.5px solid var(--hb-gold);
padding: 3px;
background: rgba(0, 0, 0, 0.15);
aspect-ratio: 16 / 11;
overflow: hidden;
}
.hb-hero-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.hb-hero-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(240, 215, 140, 0.55);
letter-spacing: 0.3em;
font-size: 13px;
background: rgba(0, 0, 0, 0.2);
}
.hb-xi {
position: absolute;
top: 10%;
font-size: 28px;
color: #c41e3a;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
font-weight: 700;
opacity: 0.92;
pointer-events: none;
}
.hb-xi-l { left: 8%; }
.hb-xi-r { right: 8%; }
.hb-names {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 28px;
margin-bottom: 8px;
opacity: 0;
transition: opacity 0.4s ease;
}
.hb-names.is-show { opacity: 1; }
.hb-name {
color: #f7efe0 !important;
font-size: 12px;
letter-spacing: 0.12em;
justify-content: center;
}
.hb-xi-circle {
width: 22px;
height: 22px;
border-radius: 50%;
border: 1px solid var(--hb-gold);
color: var(--hb-gold);
font-size: 11px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba(155, 0, 0, 0.5);
}
.hb-title-wrap {
margin: 4px 0 12px;
min-height: 48px;
opacity: 0;
transition: opacity 0.35s ease;
}
.hb-title-wrap.is-show { opacity: 1; }
.hb-title {
color: var(--hb-gold-soft) !important;
font-size: clamp(30px, 9vw, 40px);
letter-spacing: 0.18em;
justify-content: center;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.hb-body {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 9px;
padding: 4px 4px 10px;
}
.hb-line {
color: var(--hb-gold-soft) !important;
font-size: 13px;
letter-spacing: 0.14em;
line-height: 1.55;
text-align: center;
justify-content: center;
}
.hb-cursor {
color: var(--hb-gold);
}
.hb-footer {
margin-top: 8px;
min-height: 24px;
opacity: 0;
transition: opacity 0.4s ease;
}
.hb-footer.is-show { opacity: 1; }
.hb-footer-text {
color: #f7efe0 !important;
font-size: 12px;
letter-spacing: 0.2em;
justify-content: center;
}
.hb-cta {
margin-top: 16px;
opacity: 0;
transform: translateY(8px);
animation: hb-cta-in 0.5s ease forwards;
}
.hb-cta.is-show { opacity: 1; }
.hb-cta-btn {
appearance: none;
border: 1px solid var(--hb-gold);
background: linear-gradient(180deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.08));
color: var(--hb-gold-soft);
padding: 8px 22px;
border-radius: 999px;
letter-spacing: 0.28em;
font-size: 13px;
cursor: pointer;
}
.hb-cta-btn:active { transform: scale(0.98); }
.hb-disabled {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: rgba(80, 0, 0, 0.92);
color: var(--hb-cream);
}
@keyframes hb-seal-pop {
0% { opacity: 0; transform: scale(0.4) rotate(-12deg); }
60% { opacity: 1; transform: scale(1.08) rotate(2deg); }
100% { opacity: 1; transform: scale(1) rotate(0); }
}
@keyframes hb-shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
@keyframes hb-fade-pulse {
0%, 100% { opacity: 0.25; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.1); }
}
@keyframes hb-cta-in {
to { opacity: 1; transform: none; }
}
@keyframes hb-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes hb-pulse-soft {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
@keyframes hb-swing {
0%, 100% { transform: rotate(-4deg); }
50% { transform: rotate(4deg); }
}
@keyframes hb-twinkle {
0%, 100% { opacity: 0.25; transform: scale(0.7); }
50% { opacity: 1; transform: scale(1.25); }
}
</style>