From 06c57a0754bf983ddd71edbbcc23411645eb701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 3 Aug 2026 12:56:40 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=A4=BC=E7=89=A9=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/GiftEffect.vue | 322 +++++++++++++----- .../src/components/PhotoGallery.vue | 14 + vite-tailwindcss/src/views/index/index.vue | 95 +++++- 3 files changed, 341 insertions(+), 90 deletions(-) diff --git a/vite-tailwindcss/src/components/GiftEffect.vue b/vite-tailwindcss/src/components/GiftEffect.vue index d700a7e..c57c135 100644 --- a/vite-tailwindcss/src/components/GiftEffect.vue +++ b/vite-tailwindcss/src/components/GiftEffect.vue @@ -10,6 +10,15 @@ import { isIOS } from '@/composables/useAndroidOptimize' const emit = defineEmits(['play-start']) const canvasRef = ref(null) +// ======================================================== +// [高级特效开关控制] (可自由组合) +// ======================================================== +const ENABLE_BLEND_MODES = true // 特效一:叠加发光 (仙气、刺眼的高亮光感) +const ENABLE_MOTION_BLUR = true // 特效二:物理拉丝拖尾 (运动模糊、极速流星感) +const ENABLE_NEON_GLOW = true // 特效三:[全新] 霓虹流光场 (将原本难看的线条,升级为多层发光的能量轨迹) +const ENABLE_TURBULENCE = true // 特效四:流体力学场 (S型盘旋、引力扰动空间) +// ======================================================== + const MAX_PARTICLES = isIOS ? 72 : 180 const MAX_RINGS = isIOS ? 6 : 12 const MAX_TEXTS = 3 @@ -262,24 +271,35 @@ function resizeCanvas() { if (typeof ctx.direction === 'string') ctx.direction = 'ltr' } +/** + * [优化] 完美心形比例计算:移除原本生硬的锚点, + * 使用更圆润的黄金分割贝塞尔曲线,还原极其逼真的心形 + */ function drawHeartPath(context, cx, cy, size) { - const s = size context.beginPath() - context.moveTo(cx, cy + s * 0.55) - context.bezierCurveTo(cx - s * 0.85, cy + s * 0.25, cx - s * 0.95, cy - s * 0.2, cx - s * 0.45, cy - s * 1.05) - context.bezierCurveTo(cx, cy - s * 0.55, cx + s * 0.45, cy - s * 1.05, cx + s * 0.45, cy - s * 1.05) - context.bezierCurveTo(cx + s * 0.95, cy - s * 0.2, cx + s * 0.85, cy + s * 0.25, cx, cy + s * 0.55) + context.moveTo(cx, cy - size * 0.25) + // 右半心 + context.bezierCurveTo(cx + size * 0.5, cy - size * 0.65, cx + size * 1.1, cy + size * 0.15, cx, cy + size * 0.85) + // 左半心 + context.bezierCurveTo(cx - size * 1.1, cy + size * 0.15, cx - size * 0.5, cy - size * 0.65, cx, cy - size * 0.25) context.closePath() } +/** + * [优化] 真实花瓣比例计算:水滴与樱花的结合体, + * 底部尖锐,顶部圆润,伴有自然凹陷。 + */ function drawPetal(context, x, y, size, rot) { context.save() context.translate(x, y) context.rotate(rot) context.beginPath() - context.moveTo(0, -size) - context.quadraticCurveTo(size * 0.7, -size * 0.2, 0, size) - context.quadraticCurveTo(-size * 0.7, -size * 0.2, 0, -size) + // 花瓣根部 + context.moveTo(0, size * 0.5) + // 左侧弧线 + context.bezierCurveTo(-size * 0.8, size * 0.2, -size * 0.6, -size * 0.8, 0, -size) + // 右侧弧线 + context.bezierCurveTo(size * 0.6, -size * 0.8, size * 0.8, size * 0.2, 0, size * 0.5) context.closePath() context.fill() context.restore() @@ -328,11 +348,41 @@ function scrolledCount(mt, now) { return Math.max(from, Math.round(from + (target - from) * e)) } -const FONT_STACK = '"Ma Shan Zheng", "PingFang SC", "Hiragino Sans GB", sans-serif' +const FONT_STACK_FALLBACK = '"Ma Shan Zheng", "Great Vibes", cursive, serif' +let cachedCalligraphyFontStack = '' + +/** + * Canvas 不解析 CSS 变量。 + * --font-calligraphy 首位是 Great Vibes(拉丁花体,无中文毛笔字形), + * 礼物文案是中文,需把 Ma Shan Zheng 提到最前,否则易落到 cursive/系统字体,没有毛笔感。 + */ +function calligraphyFontStack() { + if (cachedCalligraphyFontStack) return cachedCalligraphyFontStack + let raw = '' + try { + raw = getComputedStyle(document.documentElement) + .getPropertyValue('--font-calligraphy') + .trim() + } catch { + raw = '' + } + const base = raw || FONT_STACK_FALLBACK + // 已含马善政则提到最前;否则插入最前 + if (/ma\s*shan\s*zheng/i.test(base)) { + const rest = base + .split(',') + .map((s) => s.trim()) + .filter((s) => s && !/ma\s*shan\s*zheng/i.test(s)) + cachedCalligraphyFontStack = [`"Ma Shan Zheng"`, ...rest].join(', ') + } else { + cachedCalligraphyFontStack = `"Ma Shan Zheng", ${base}` + } + return cachedCalligraphyFontStack +} function setMilestoneFont(context, fontSize) { - // Ma Shan Zheng 只有 400;用 700 时 iOS 会假粗并打乱字距/排版 - context.font = `400 ${fontSize}px ${FONT_STACK}` + // Ma Shan Zheng 只有 400;用 700 时 iOS 会假粗并打乱字距 + context.font = `400 ${fontSize}px ${calligraphyFontStack()}` if (typeof context.direction === 'string') context.direction = 'ltr' if (typeof context.letterSpacing === 'string') context.letterSpacing = '0px' } @@ -426,6 +476,7 @@ function drawMilestoneLine(context, text, fontSize, glowBlur, gradient, strokeCo context.fillStyle = textGrad if (blur > 0) context.shadowBlur = blur * 1.15 context.fillText(ch, cx, y) + context.shadowBlur = 0 // 细高光 context.globalAlpha *= 0.35 @@ -588,8 +639,8 @@ function spawnParticles(giftType, now, wave = 0) { for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < Math.floor(26 * boost); i += 1) { const a = side < 0 - ? rand(-Math.PI * 0.35, Math.PI * 0.35) - : rand(Math.PI * 0.65, Math.PI * 1.35) + ? rand(-Math.PI * 0.35, Math.PI * 0.35) + : rand(Math.PI * 0.65, Math.PI * 1.35) makeParticle(cx + side * W * 0.28 + rand(-20, 20), cy + rand(-60, 60), now, palette, glowPalette, { angle: a, speed: rand(90, 200), @@ -777,8 +828,8 @@ function playOne(item) { activeMeta = { giftType, name, mt } spawnParticles(giftType, now, 0) const waves = style.shockwaves != null - ? style.shockwaves - : (style.showHeartOutline && !isIOS ? 3 : 2) + ? style.shockwaves + : (style.showHeartOutline && !isIOS ? 3 : 2) spawnShockwave(W / 2, H * 0.46, now, style.glowColor, waves) flashAlpha = isIOS ? Math.min(style.flash, giftType === 'concentric' ? 0.48 : 0.4) : style.flash flashDecay = flashAlpha / Math.max(1, style.flashMs / 16) @@ -825,10 +876,10 @@ function tryBumpSame(payload) { } // 叠加正在播放的特效 if ( - activeMeta - && activeMeta.giftType === payload.giftType - && activeMeta.name === payload.name - && texts.includes(activeMeta.mt) + activeMeta + && activeMeta.giftType === payload.giftType + && activeMeta.name === payload.name + && texts.includes(activeMeta.mt) ) { const mt = activeMeta.mt const now = performance.now() @@ -887,8 +938,8 @@ const play = (item) => { if (!queueTimer) { const active = texts[0] const remain = active - ? Math.max(200, active.duration - (performance.now() - active.birthTime) + 200) - : 400 + ? Math.max(200, active.duration - (performance.now() - active.birthTime) + 200) + : 400 scheduleNextFromQueue(remain) } return @@ -904,9 +955,9 @@ const play = (item) => { const playSequence = (items, opts = {}) => { const max = opts.max != null ? opts.max : 10 const list = (Array.isArray(items) ? items : []) - .map(normalizePlayItem) - .filter(Boolean) - .slice(0, max) + .map(normalizePlayItem) + .filter(Boolean) + .slice(0, max) if (!list.length) return clearTimeout(queueTimer) queueTimer = null @@ -989,15 +1040,30 @@ function animate(timestamp) { const age = (now - p.birthTime) / 1000 // 飘逸减速:速度随时间衰减,更高级 const damp = 1 / (1 + age * 0.55) - const x = p.x + p.vx * age * damp + Math.sin(age * 2.2 + p.rot) * (p.drift || 0) * 0.45 - const y = p.y + p.vy * age * damp + 0.5 * p.gravity * age * age * 0.75 + + let vortexX = 0 + let vortexY = 0 + // 特效四:流体力学场 (Turbulence) + if (ENABLE_TURBULENCE) { + vortexX = Math.sin(age * 3.0 + p.birthTime) * 15 * damp + vortexY = Math.cos(age * 2.5 + p.rot) * 10 * damp + } + + const x = p.x + p.vx * age * damp + Math.sin(age * 2.2 + p.rot) * (p.drift || 0) * 0.45 + vortexX + const y = p.y + p.vy * age * damp + 0.5 * p.gravity * age * age * 0.75 + vortexY const opacity = progress < 0.12 - ? easeOutCubic(progress / 0.12) - : 1 - ((progress - 0.12) / 0.88) ** 1.15 + ? easeOutCubic(progress / 0.12) + : 1 - ((progress - 0.12) / 0.88) ** 1.15 const size = p.size * (progress < 0.15 ? 0.7 + progress * 1.6 : 1 - progress * 0.35) const rot = p.rot + p.rotSpeed * age * 0.65 ctx.save() + + // 特效一:叠加发光混合模式 (Blend Modes) + if (ENABLE_BLEND_MODES && (p.kind === 'spark' || p.kind === 'star' || p.kind === 'dot')) { + ctx.globalCompositeOperation = 'lighter' + } + ctx.globalAlpha = Math.max(0, opacity) ctx.fillStyle = p.color ctx.shadowColor = p.glow || p.color @@ -1024,9 +1090,28 @@ function animate(timestamp) { ctx.closePath() ctx.fill() } else if (p.kind === 'spark') { - ctx.translate(x, y) - ctx.rotate(rot) - ctx.fillRect(-size * 0.28, -size * 1.15, size * 0.56, size * 2.3) + // 特效二:物理拉丝拖尾 (Motion Blur) + if (ENABLE_MOTION_BLUR) { + const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy) * damp; + const stretch = Math.max(1, speed * 0.15); + const angle = Math.atan2(p.vy, p.vx); + + ctx.translate(x, y) + ctx.rotate(angle) + + const tailGrad = ctx.createLinearGradient(0, 0, -size * stretch, 0); + tailGrad.addColorStop(0, p.color); + tailGrad.addColorStop(1, 'rgba(255,255,255,0)'); + + ctx.fillStyle = tailGrad; + ctx.beginPath(); + ctx.ellipse(0, 0, size * stretch, size * 0.4, 0, 0, Math.PI * 2); + ctx.fill(); + } else { + ctx.translate(x, y) + ctx.rotate(rot) + ctx.fillRect(-size * 0.28, -size * 1.15, size * 0.56, size * 2.3) + } } else { ctx.beginPath() ctx.arc(x, y, size, 0, Math.PI * 2) @@ -1052,7 +1137,7 @@ function animate(timestamp) { if (opacity < 0.01) continue const wobble = Math.sin((now - mt.birthTime) / 180) * mt.wobble - // soft aura behind text(iOS 跳过径向渐变,避免卡顿/错乱) + // soft aura behind text if (mt.aura && !SKIP_AURA) { const auraR = Math.max(0.1, Number(fontSize) * 1.8 * Math.max(0, scale) || 0) if (auraR > 0.1) { @@ -1079,20 +1164,20 @@ function animate(timestamp) { const showMult = mt.countRevealed && mt.count > 1 const mainY = showMult ? -fontSize * 0.42 : 0 drawMilestoneLine( - ctx, - mt.baseText || mt.text, - fontSize, - glowBlur, - mt.gradient, - mt.strokeColor, - mt.glowColor, - mainY, - { - underStroke: mt.underStroke, - charGap: mt.charGap, - birthTime: mt.birthTime, - now, - }, + ctx, + mt.baseText || mt.text, + fontSize, + glowBlur, + mt.gradient, + mt.strokeColor, + mt.glowColor, + mainY, + { + underStroke: mt.underStroke, + charGap: mt.charGap, + birthTime: mt.birthTime, + now, + }, ) if (showMult) { const n = scrolledCount(mt, now) @@ -1104,42 +1189,70 @@ function animate(timestamp) { ctx.translate(0, multY) ctx.scale(pop, pop) drawMilestoneLine( - ctx, - `×${n}`, - multSize, - glowBlur * 0.7, - mt.gradient, - mt.strokeColor, - mt.glowColor, - 0, - { - underStroke: mt.underStroke, - charGap: 0.04, - birthTime: mt.countRevealAt || now, - now, - }, + ctx, + `×${n}`, + multSize, + glowBlur * 0.7, + mt.gradient, + mt.strokeColor, + mt.glowColor, + 0, + { + underStroke: mt.underStroke, + charGap: 0.04, + birthTime: mt.countRevealAt || now, + now, + }, ) ctx.restore() } ctx.restore() + // 核心心形轮廓重构:将难看的单线条换成多层高质感的霓虹流光轮廓 if (mt.showHeartOutline && heartOutlineOpacity(mt, now) > 0.01) { const outlineScale = heartOutlineScale(mt, now) - const outlineOpacity = heartOutlineOpacity(mt, now) * 0.7 + const outlineOpacity = heartOutlineOpacity(mt, now) * (ENABLE_NEON_GLOW ? 1.0 : 0.7) const outlineSize = fontSize * 1.55 * outlineScale + ctx.save() - ctx.globalAlpha = outlineOpacity ctx.translate(cx, cy) - ctx.strokeStyle = mt.glowColor - ctx.lineWidth = Math.max(1, outlineSize * 0.022) - ctx.shadowColor = mt.glowColor - ctx.shadowBlur = (isIOS ? 0 : outlineSize * 0.16) * SHADOW_MUL - drawHeartPath(ctx, 0, 0, outlineSize) - ctx.stroke() + + if (ENABLE_NEON_GLOW) { + // [新增] 特效三:惊艳的霓虹流光多层绘制 + ctx.globalCompositeOperation = 'lighter' + + // 渲染 3 层不同的发光和线宽来模拟极具科技感和仙气的霓虹能量管 + const neonLayers = [ + { widthRatio: 0.045, alpha: 0.25, blur: 20 * SHADOW_MUL }, // 底层弥散光晕 + { widthRatio: 0.02, alpha: 0.6, blur: 10 * SHADOW_MUL }, // 中层亮色核心 + { widthRatio: 0.006, alpha: 1.0, blur: 2 * SHADOW_MUL } // 顶层高亮白心 + ] + + neonLayers.forEach((layer, idx) => { + ctx.globalAlpha = outlineOpacity * layer.alpha + // 顶层使用纯白,底层使用礼物的主题发光色 + ctx.strokeStyle = idx === 2 ? '#ffffff' : mt.glowColor + ctx.lineWidth = Math.max(1, outlineSize * layer.widthRatio) + ctx.shadowColor = mt.glowColor + ctx.shadowBlur = layer.blur + drawHeartPath(ctx, 0, 0, outlineSize) + ctx.stroke() + }) + } else { + // 降级旧方案(普通单线条描边) + ctx.globalAlpha = outlineOpacity + ctx.strokeStyle = mt.glowColor + ctx.lineWidth = Math.max(1, outlineSize * 0.022) + ctx.shadowColor = mt.glowColor + ctx.shadowBlur = (isIOS ? 0 : outlineSize * 0.16) * SHADOW_MUL + drawHeartPath(ctx, 0, 0, outlineSize) + ctx.stroke() + } + ctx.restore() } - // 永结同心:双心相扣,慢呼吸而非炫转 + // 永结同心(双心相扣)重构:同样支持霓虹发光材质 if (mt.showInterlockHearts && opacity > 0.12) { const p = textProgress(mt, now) const grow = easeOutCubic(Math.min(1, p / 0.28)) @@ -1147,32 +1260,54 @@ function animate(timestamp) { const breath = Math.sin((now - mt.birthTime) / 900) * 0.04 const hs = fontSize * (1.05 + 0.28 * grow) * scale const ox = fontSize * 0.38 * scale + ctx.save() ctx.translate(cx, cy) - ctx.globalAlpha = opacity * 0.38 * fade - ctx.strokeStyle = mt.glowColor - ctx.lineWidth = Math.max(1, hs * 0.022) - ctx.shadowColor = mt.glowColor - ctx.shadowBlur = isIOS ? 0 : 6 * SHADOW_MUL - ctx.save() - ctx.translate(-ox, 0) - ctx.rotate(-0.2 + breath) - drawHeartPath(ctx, 0, 0, hs) - ctx.stroke() - ctx.restore() - ctx.save() - ctx.translate(ox, 0) - ctx.rotate(0.2 - breath) - drawHeartPath(ctx, 0, 0, hs) - ctx.stroke() - ctx.restore() + + const drawInterlockHeart = (offsetX, rotateAngle) => { + ctx.save() + ctx.translate(offsetX, 0) + ctx.rotate(rotateAngle) + + if (ENABLE_NEON_GLOW) { + ctx.globalCompositeOperation = 'lighter' + const neonLayers = [ + { widthRatio: 0.045, alpha: 0.2, blur: 15 * SHADOW_MUL }, + { widthRatio: 0.02, alpha: 0.5, blur: 8 * SHADOW_MUL }, + { widthRatio: 0.006, alpha: 0.9, blur: 2 * SHADOW_MUL } + ] + neonLayers.forEach((layer, idx) => { + ctx.globalAlpha = opacity * fade * layer.alpha + ctx.strokeStyle = idx === 2 ? '#ffffff' : mt.glowColor + ctx.lineWidth = Math.max(1, hs * layer.widthRatio) + ctx.shadowColor = mt.glowColor + ctx.shadowBlur = layer.blur + drawHeartPath(ctx, 0, 0, hs) + ctx.stroke() + }) + } else { + ctx.globalAlpha = opacity * 0.38 * fade + ctx.strokeStyle = mt.glowColor + ctx.lineWidth = Math.max(1, hs * 0.022) + ctx.shadowColor = mt.glowColor + ctx.shadowBlur = isIOS ? 0 : 6 * SHADOW_MUL + drawHeartPath(ctx, 0, 0, hs) + ctx.stroke() + } + ctx.restore() + } + + // 绘制左心和右心 + drawInterlockHeart(-ox, -0.2 + breath) + drawInterlockHeart(ox, 0.2 - breath) + ctx.restore() } if (mt.orb && opacity > 0.18) { const numOrbs = mt.showInterlockHearts - ? (isIOS ? 8 : 12) - : (mt.showHeartOutline ? 10 : 8) + ? (isIOS ? 8 : 12) + : (mt.showHeartOutline ? 10 : 8) const orbRadius = fontSize * (mt.showInterlockHearts ? 1.15 : 1.0 + 0.12 * glowIntensity) const age = (now - mt.birthTime) / 1000 for (let j = 0; j < numOrbs; j += 1) { @@ -1223,6 +1358,15 @@ const onResize = () => resizeCanvas() onMounted(() => { resizeCanvas() + // 预加载书法字体,避免首帧特效落到系统兜底字体 + if (document.fonts?.load) { + const stack = calligraphyFontStack() + Promise.all([ + document.fonts.load(`400 64px ${stack}`), + document.fonts.load('400 64px "Great Vibes"'), + document.fonts.load('400 64px "Ma Shan Zheng"'), + ]).catch(() => {}) + } window.addEventListener('resize', onResize) window.visualViewport?.addEventListener('resize', onResize) window.visualViewport?.addEventListener('scroll', onResize) @@ -1282,4 +1426,4 @@ defineExpose({ play, playSequence, clear: clearAll, stop: clearAll }) backface-visibility: hidden; -webkit-backface-visibility: hidden; } - + \ No newline at end of file diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 51a9ddb..91194e3 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -696,6 +696,20 @@ function polaroidTransform(i) { border-radius: 10px; scrollbar-width: none; } .g-carousel-slide { flex: 0 0 100%; min-width: 100%; height: 52vh; scroll-snap-align: center; } + +@media (min-width: 768px) { + .gallery { + max-width: 400px; + margin-inline: auto; + } + .overlap-wrap { + max-width: 400px; + height: clamp(340px, 48vh, 460px); + } + .g-carousel-slide { + height: min(52vh, 420px); + } +} .g-carousel-slide img { width: 100%; height: 100%; object-fit: cover; } .g-arrow { position: absolute; top: 50%; transform: translateY(-50%); diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 8535563..1a203e4 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -2472,7 +2472,7 @@ watch( text-shadow: 0 0 6px rgba(253, 251, 247, 0.95), 0 1px 0 rgba(255, 255, 255, 0.8); - transform: translate3d(calc(var(--scroll-p, 0) * (100vw - 1.1em) + 0.55em - 50%), 0, 0); + transform: translate3d(calc(var(--scroll-p, 0) * (100% - 1.1em) + 0.55em - 50%), 0, 0); will-change: transform; font-weight: 700; letter-spacing: 0; @@ -3582,4 +3582,97 @@ watch( 12% { opacity: 1; transform: translateY(4px) scale(.98); filter: blur(0); } 100% { opacity: 0; transform: translateY(-12px) scale(.94); filter: blur(1.4px); } } + +/* ---------- PC:内容列限宽居中(缩略图不宜铺满大屏) ---------- */ +@media (min-width: 768px) { + .wrapper { + --pc-invite-width: 430px; + background: + radial-gradient(ellipse at 50% 12%, rgba(232, 201, 210, 0.12), transparent 42%), + linear-gradient(180deg, #f3eee6 0%, #ebe4d8 100%); + } + .scroll-transform-host { + max-width: var(--pc-invite-width); + margin-left: auto; + margin-right: auto; + background: #fdfbf7; + box-shadow: + 0 0 0 1px rgba(168, 140, 107, 0.12), + 0 28px 80px rgba(95, 70, 39, 0.12); + } + .scroll-progress { + left: 50%; + right: auto; + width: var(--pc-invite-width); + max-width: 100%; + transform: translateX(-50%); + } + .action-dock { + left: 50%; + right: auto; + width: var(--pc-invite-width); + max-width: 100%; + transform: translateX(-50%); + box-sizing: border-box; + } + + /* 日历 PC:略放大卡片与选中爱心,移动端样式不变 */ + .wedding-calendar-page { + min-height: 100dvh; + padding-top: 48px; + padding-bottom: 48px; + } + .calendar-card { + width: min(100%, 392px); + padding: 36px 28px 34px; + } + .calendar-card::before { + inset: 14px; + } + .calendar-kicker { + font-size: 11px; + letter-spacing: 0.5em; + padding-left: 0.5em; + } + .calendar-title { + margin-top: 14px; + font-size: 34px; + gap: 16px; + } + .calendar-title i { + width: 40px; + } + .calendar-date-line { + margin: 12px auto 26px; + font-size: 12px; + } + .calendar-weekdays, + .calendar-grid { + gap: 10px; + } + .calendar-weekdays { + margin-bottom: 11px; + font-size: 11px; + } + .calendar-day { + font-size: 16px; + } + .calendar-day.is-selected .calendar-day-num { + width: 28px; + height: 28px; + font-size: 17px; + } + .heart-ring { + inset: -12px; + } + .heart-ring svg { + width: 74px; + height: 72px; + } + .calendar-note { + margin-top: 28px; + font-size: 14px; + gap: 12px; + } +}