.timer-page { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding-top: 80rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); min-height: 100vh; box-sizing: border-box; } /* ---- ring wrapper ---- */ .ring-wrapper { position: relative; display: flex; align-items: center; justify-content: center; /* 圆环与下方(状态词/目标卡/提示)整组拉开间距,按钮靠 margin-top:auto 贴底不受影响 */ margin-bottom: 44rpx; } /* breathing guide rings — soft radial glows instead of hard outline circles, so they read as ambient halo rather than a bullseye */ .breathe-ring { position: absolute; border-radius: 50%; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.13) 0%, rgba(var(--primary-rgb), 0) 68%); animation: breathePulse 3.5s ease-in-out infinite; pointer-events: none; z-index: 0; } .breathe-ring-1 { width: 620rpx; height: 620rpx; animation-delay: 0s; } .breathe-ring-2 { width: 540rpx; height: 540rpx; animation-delay: 0.5s; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.17) 0%, rgba(var(--primary-rgb), 0) 66%); } .breathe-ring-3 { width: 460rpx; height: 460rpx; animation-delay: 1s; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.22) 0%, rgba(var(--primary-rgb), 0) 63%); } .breathe-ring.fast { animation: breathePulseFast 1.4s ease-in-out infinite; } .breathe-ring.fast.breathe-ring-1 { animation-delay: 0s; } .breathe-ring.fast.breathe-ring-2 { animation-delay: 0.25s; } .breathe-ring.fast.breathe-ring-3 { animation-delay: 0.5s; } /* ---- 呼吸光环:进度环外侧一圈淡化的描边辉光 ---- */ .breath-aura { position: absolute; /* 直径略大于进度环(420),落在刻度/进度弧外侧,不遮挡中间时钟 */ width: 488rpx; height: 488rpx; border-radius: 50%; /* 细线 + 低透明度:只是隐约的呼吸边界,不跟倒计时环抢视觉 */ border: 4rpx solid rgba(var(--primary-rgb), 0.20); box-shadow: 0 0 30rpx rgba(var(--primary-rgb), 0.22), inset 0 0 20rpx rgba(var(--primary-rgb), 0.10); animation: auraBreathe 4s ease-in-out infinite; pointer-events: none; z-index: 0; } .breath-aura.fast { animation: auraBreatheFast 2.4s ease-in-out infinite; } /* 柔和呼吸:幅度小(scale 0.96↔1.04)、透明度压低(0.3↔0.5),留出呼吸感但始终很淡 */ @keyframes auraBreathe { 0%, 100% { transform: scale(0.96); opacity: 0.30; } 50% { transform: scale(1.04); opacity: 0.50; } } @keyframes auraBreatheFast { 0%, 100% { transform: scale(0.97); opacity: 0.34; } 50% { transform: scale(1.05); opacity: 0.55; } } /* celebration burst */ .celebrate-burst { position: absolute; width: 520rpx; height: 520rpx; pointer-events: none; z-index: 2; } .spark { position: absolute; width: 44rpx; height: 44rpx; animation: sparkBurst 1s ease-out forwards; } .s1 { top: -10rpx; left: 50%; margin-left: -22rpx; animation-delay: 0s; } .s2 { top: 30rpx; right: -10rpx; animation-delay: 0.1s; } .s3 { bottom: -10rpx; right: 50rpx; animation-delay: 0.2s; } .s4 { bottom: 20rpx; left: 30rpx; animation-delay: 0.15s; } .s5 { top: 50%; right: -20rpx; animation-delay: 0.25s; } .s6 { top: -20rpx; left: 40rpx; animation-delay: 0.05s; } @keyframes sparkBurst { 0% { opacity: 0; transform: translate(0, 0) scale(0.3); } 40% { opacity: 1; transform: translate(0, -30rpx) scale(1.3); } 100% { opacity: 0; transform: translate(0, -80rpx) scale(0.4); } } /* ---- 信息气泡·状态行(原环下方 statusText 并入气泡顶部) ---- 状态色:running=主色 / 休息中=绿 / idle、paused=中性灰。 resting 时 status 字段仍是 running,额外用 .info-line--rest 覆盖为绿。 小圆点用 currentColor 跟随状态色,纯 CSS 画(项目 SVG 图标色烤死改不动)。 */ .info-line--status { gap: 12rpx; } .info-status-dot { width: 14rpx; height: 14rpx; border-radius: 50%; background: currentColor; flex-shrink: 0; } .info-status-text { font-size: 32rpx; font-weight: 700; line-height: 1.3; color: var(--text); } .info-line--status.status--running { color: var(--primary); } .info-line--status.status--running .info-status-dot { animation: statusDotBlink 1.6s ease-in-out infinite; } .info-line--status.info-line--rest { color: var(--success); } .info-line--status.info-line--rest .info-status-dot { animation: statusDotBlink 1.8s ease-in-out infinite; } .info-line--status.status--paused, .info-line--status.status--idle { color: var(--text-secondary); } /* 暂停时冻结呼吸,静止本身就是"已暂停"的信号 */ .info-line--status.status--paused .info-status-dot { animation: none; } @keyframes statusDotBlink { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.75); } } /* ---- 信息气泡:状态 + 目标摘要 + 运行提示 合并为一张卡(三模式公用) ---- 三行结构:上=状态(最醒目,状态色) / 中=目标常驻(小字次级) / 下=动态提示。 休息/暂停/待机时提示行为空,分隔线和提示行整体消失,气泡自动收缩。 */ .info-bubble { display: flex; flex-direction: column; box-sizing: border-box; width: 620rpx; max-width: 86%; /* 承接原 timer-status 的 40rpx 间距,圆环与卡片保持呼吸感 */ margin-top: 40rpx; padding: 24rpx 28rpx; background: rgba(var(--primary-rgb), 0.08); border-radius: 26rpx; } .info-line { display: flex; align-items: center; } .info-icon { width: 32rpx; height: 32rpx; flex-shrink: 0; margin-right: 12rpx; } .info-text { flex: 1; min-width: 0; line-height: 1.4; } /* 目标行:静态摘要,主色小字 */ .info-line--goal .info-text { font-size: 26rpx; font-weight: 500; color: var(--primary); } /* 分隔线:提示行出现时才渲染 */ .info-divider { height: 1rpx; background: rgba(var(--primary-rgb), 0.14); margin: 14rpx 0; } /* 提示行:动态信息,比目标行大一号更突出 */ .info-line--tip .info-text { font-size: 30rpx; font-weight: 600; color: var(--text); } .info-line--done .info-text { color: var(--success); } /* ---- controls ---- */ .controls { margin-top: auto; width: 100%; display: flex; justify-content: center; } .ctrl-row { display: flex; justify-content: center; align-items: center; } /* ---- full-screen confetti ---- */ .confetti { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 9999; overflow: hidden; } .confetti-piece { position: absolute; top: -20rpx; border-radius: 4rpx; animation: confettiFall 2.4s ease-out forwards; } /* Higher celebration levels: faster / longer fall */ .confetti-piece--milestone, .confetti-piece--first, .confetti-piece--record { animation-duration: 3.2s; } .confetti-piece--first { animation-duration: 4s; } @keyframes confettiFall { 0% { opacity: 1; transform: translateY(0) rotate(0deg); } 100% { opacity: 0.6; transform: translateY(100vh) rotate(720deg); } } /* ---- achievement banner ---- */ .achievement-banner { position: fixed; top: 160rpx; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; font-size: 32rpx; font-weight: 700; padding: 24rpx 48rpx; border-radius: 48rpx; box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.4); z-index: 10000; animation: achievementIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both, achievementOut 0.4s ease-in 3.5s both; white-space: nowrap; } .achievement-text { display: block; line-height: 1; } @keyframes achievementIn { 0% { opacity: 0; transform: translateX(-50%) scale(0.5); } 100% { opacity: 1; transform: translateX(-50%) scale(1); } } @keyframes achievementOut { 0% { opacity: 1; transform: translateX(-50%) scale(1); } 100% { opacity: 0; transform: translateX(-50%) scale(0.8); } } /* ---- completion modal (full-screen summary after training ends) ---- */ .completion-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.55); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 40rpx; box-sizing: border-box; animation: completionMaskIn 0.3s ease; } @keyframes completionMaskIn { from { opacity: 0; } to { opacity: 1; } } .completion-card { width: 100%; max-width: 640rpx; background: var(--card-bg); border-radius: 32rpx; padding: 56rpx 40rpx 40rpx; display: flex; flex-direction: column; align-items: center; box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25); animation: completionCardIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes completionCardIn { 0% { transform: scale(0.6) translateY(40rpx); opacity: 0; } 100% { transform: scale(1) translateY(0); opacity: 1; } } .completion-emoji-wrap { width: 140rpx; height: 140rpx; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--primary-light)); display: flex; align-items: center; justify-content: center; margin-bottom: 24rpx; box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.4); animation: completionEmojiPulse 1.6s ease-in-out infinite; } @keyframes completionEmojiPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } } .completion-icon { width: 80rpx; height: 80rpx; } .completion-title { font-size: 32rpx; font-weight: 600; color: var(--text); margin-bottom: 32rpx; text-align: center; } .completion-number-wrap { display: flex; align-items: baseline; justify-content: center; /* 8rpx 原间距 + 24rpx 原 completion-label 的 margin-bottom,删除标签后合并到这里 */ margin-bottom: 32rpx; } .completion-number { font-size: 180rpx; font-weight: 800; color: var(--primary); line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -4rpx; text-shadow: 0 4rpx 12rpx rgba(var(--primary-rgb), 0.2); } /* >= 60 秒时数字部分变成「X分Y」,字符变多,缩小字号避免撑破卡片 */ .completion-number--long { font-size: 140rpx; } .completion-unit { font-size: 36rpx; font-weight: 600; color: var(--text-secondary); margin-left: 8rpx; } .completion-overtime { display: flex; align-items: baseline; gap: 6rpx; background: rgba(var(--primary-rgb), 0.12); padding: 12rpx 24rpx; border-radius: 32rpx; margin-bottom: 16rpx; animation: overtimePop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 1.1s both; } @keyframes overtimePop { 0% { transform: scale(0); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } .overtime-plus { font-size: 32rpx; font-weight: 700; color: var(--primary); line-height: 1; } .overtime-label { font-size: 24rpx; color: var(--primary); font-weight: 500; } .completion-streak { display: flex; align-items: center; gap: 8rpx; margin-bottom: 36rpx; font-size: 26rpx; color: var(--text); } .streak-flame { width: 32rpx; height: 32rpx; } /* 未达最低记录时长提示(完成弹窗内):主色淡底信息块,与科学提示同风格 */ .completion-not-recorded { display: flex; flex-direction: column; gap: 10rpx; width: 100%; box-sizing: border-box; background: rgba(var(--primary-rgb), 0.08); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 28rpx; animation: tipIn 0.4s ease 0.5s both; } .completion-not-recorded-main { display: flex; align-items: center; gap: 10rpx; } .completion-not-recorded-icon { width: 32rpx; height: 32rpx; flex-shrink: 0; } .completion-not-recorded-title { flex: 1; font-size: 26rpx; font-weight: 600; line-height: 1.4; color: var(--primary); } .completion-not-recorded-sub { font-size: 24rpx; line-height: 1.5; color: var(--text-secondary); } .streak-text { font-weight: 500; } .completion-tip { width: 100%; box-sizing: border-box; background: rgba(var(--primary-rgb), 0.08); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 28rpx; animation: tipIn 0.4s ease 1.0s both; } @keyframes tipIn { 0% { opacity: 0; transform: translateY(12rpx); } 100% { opacity: 1; transform: translateY(0); } } .completion-tip-value { display: block; font-size: 26rpx; line-height: 1.5; color: var(--text); font-weight: 500; } .completion-tip-risk { display: flex; align-items: flex-start; gap: 8rpx; margin-top: 10rpx; } .completion-tip-risk-tag { flex-shrink: 0; font-size: 22rpx; line-height: 1.4; color: var(--primary); background: rgba(var(--primary-rgb), 0.14); border-radius: 8rpx; padding: 2rpx 10rpx; margin-top: 2rpx; } .completion-tip-risk-text { flex: 1; font-size: 24rpx; line-height: 1.5; color: var(--text-secondary); } .completion-actions { display: flex; gap: 20rpx; width: 100%; margin-bottom: 12rpx; } .completion-btn { flex: 1; height: 88rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; font-size: 30rpx; font-weight: 600; transition: transform 0.15s ease, opacity 0.15s ease, background 0.2s ease; background: var(--bg-soft); color: var(--text); } .completion-btn:active { transform: scale(0.96); opacity: 0.85; } .completion-btn--primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; box-shadow: 0 8rpx 20rpx rgba(var(--primary-rgb), 0.3); } .completion-btn--text { background: transparent; color: var(--text-secondary); font-weight: 500; height: 64rpx; font-size: 26rpx; margin-top: 4rpx; } /* ---- stop confirm modal (替代 wx.showModal,自定义实现) ---- */ .stop-confirm-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.55); z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 40rpx; box-sizing: border-box; animation: completionMaskIn 0.25s ease; } .stop-confirm-card { width: 100%; max-width: 560rpx; background: var(--card-bg); border-radius: 24rpx; padding: 48rpx 40rpx 24rpx; display: flex; flex-direction: column; align-items: center; box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.22); animation: stopConfirmPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes stopConfirmPop { 0% { transform: scale(0.85); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } .stop-confirm-title { font-size: 32rpx; font-weight: 700; color: var(--text); margin-bottom: 20rpx; line-height: 1.3; } .stop-confirm-content { font-size: 28rpx; color: var(--text-secondary); line-height: 1.5; text-align: center; margin-bottom: 40rpx; } .stop-confirm-actions { display: flex; width: 100%; gap: 16rpx; border-top: 1rpx solid var(--border); padding-top: 24rpx; } .stop-confirm-btn { flex: 1; height: 80rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; border-radius: 12rpx; background: var(--bg-soft); color: var(--text); transition: transform 0.15s ease, opacity 0.15s ease, background 0.2s ease; } .stop-confirm-btn:active { transform: scale(0.96); opacity: 0.85; } .stop-confirm-btn--primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; box-shadow: 0 6rpx 16rpx rgba(var(--primary-rgb), 0.3); }