feat(此刻): 陪伴版文案 + 7 个头像 + 同周悄悄话(预设句/自由输入、抱抱) (v26100157)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 02:04:39 +00:00
parent d72b40f0d7
commit 9279fd921b
3 changed files with 276 additions and 13 deletions

View File

@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title>
<!-- V:26100156 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26100157 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
@@ -292,13 +292,29 @@
同日三改(用户:「此刻有一点点不够清爽,文字太多了,密密麻麻的」):从 5 行减到 3 行 ——
① 标题行不变;② 「她们在做」两条进度条合成**一句**,只挑你今天还没做的那项(都做了就一句夸奖),去掉进度条;
③ 「今天别忘了」整行删(产检、盲盒在上面工具宫格和盲盒横幅里都有);④ 滚动动态去掉白底卡片,压成一行小字。 -->
<div class="pulse hide" id="pulse" role="region" aria-label="此刻,孕妈们在做什么">
<!-- 2026-10-02 陪伴版(用户:「改成陪伴的语气……头像展示 6 到 8 个……升级成同周妈妈的悄悄话……
不做复杂社区,只做一点陪伴」):
① 头像从标题行挪到第二行,一排 7 个,后面跟「今天 N 位同周妈妈陪宝宝数了胎动」——说人数不说百分比,不和你比;
② 「你今天还没数 ›」改成邀请:「小米粒在等你,陪它数一数 ›」,做过了就是「你今天也陪它数过啦 ✓」;
③ 同周悄悄话 #whWall:横滑的小纸条,只能「抱抱」不能评论,没有昵称头像。数据 GET /whisper(app.js whisperWall())。
拉到悄悄话时滚动动态 #plFeed 让位(一块地方只讲一件事),拉不到就还是滚动动态。 -->
<div class="pulse hide" id="pulse" role="region" aria-label="此刻,同周妈妈们">
<div class="pl-head">
<span class="pl-live"><i></i>此刻</span>
<span class="pp-avs" id="ppAvs" aria-hidden="true"></span>
<span class="pl-title" id="plTitle"></span>
</div>
<div class="pp-crowd">
<span class="pp-avs" id="ppAvs" aria-hidden="true"></span>
<span class="pp-crowd-t" id="ppCrowd"></span>
</div>
<div class="pp-doing hide" id="ppDoing"></div>
<div class="wh-wall hide" id="whWall">
<div class="wh-head">
<span class="wh-t">同周悄悄话<em>匿名 · 只有同孕期妈妈能看到</em></span>
<button class="wh-say" type="button" id="whSay">说一句</button>
</div>
<div class="wh-strip" id="whStrip"></div>
</div>
<div class="pl-feed hide" id="plFeed"><div class="pl-track" id="plTrack"></div></div>
</div>
</section>
@@ -1727,6 +1743,27 @@
<div class="modal-foot"><button class="dl-btn main" id="bxAddrSave">提交</button></div>
</section>
<!-- 此刻 · 说一句悄悄话(2026-10-02)。预设句点一下就填进输入框;自由输入由服务端过微信内容安全审核 -->
<section class="modal wh-modal" id="modalWhisper" data-autofocus="off" role="dialog" aria-modal="true" aria-labelledby="whMTitle">
<div class="modal-head">
<h3 id="whMTitle">说一句悄悄话</h3>
<button class="modal-x" data-modal="close" aria-label="关闭">
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
</div>
<p class="wh-m-lead" id="whMLead">不显示昵称和头像,只有同孕期的妈妈能看到。她们会给你一个抱抱。</p>
<div class="wh-ta-box">
<textarea class="wh-ta" id="whTa" maxlength="40" rows="2" placeholder="今天想对谁说点什么?"></textarea>
<span class="wh-cnt" id="whCnt">0/40</span>
</div>
<div class="wh-m-sub">不知道说什么?点一句</div>
<div class="wh-presets" id="whPresets"></div>
<div class="modal-foot">
<button class="dl-btn main" id="whSend" type="button">悄悄发出</button>
<p class="wh-left" id="whLeft"></p>
</div>
</section>
<!-- 盲盒 · 分段集齐达成的庆祝弹层(内容由 app.js showPrize() 填) -->
<section class="modal bx-prize" id="modalBxPrize" role="dialog" aria-modal="true" aria-labelledby="bxPrizeT">
<div class="bx-prize-ico" aria-hidden="true">🏆</div>

173
js/app.js
View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26100156';
var VER = '26100157';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -3296,6 +3296,146 @@
if (k < 1) requestAnimationFrame(step);
})(t0);
}
/* ================= 此刻 · 同周悄悄话(2026-10-02) =================
用户原话:「升级成同周妈妈的悄悄话,就是匿名一句心愿墙……不做复杂社区,就是只做一点陪伴」;发言 = 预设句 + 自由输入。
GET /api/mp/public/mama-box/whisper?openid=&week= → { show, week, canPost, left, maxLen, presets[], list[] }
POST /api/mp/public/mama-box/whisper?openid= {text} → 新的一条(自由输入由服务端过微信内容安全审核,不通就 code≠200)
POST /api/mp/public/mama-box/whisper/hug?openid=&id=
· 只能「抱抱」,没有评论、昵称、头像 —— 刻意不做成社区。自己说的那句排第一,显示「收到 N 个抱抱」。
· 拉不到(show=false / 失败 / 访客没孕周)→ 整块隐藏,此刻卡退回滚动动态。不在前端造任何内容。
· 由 pulseCard 的 load() 带着孕周触发(孕周回来 / 变了才重拉),另每 5 分钟静默刷新一次。 */
let whLoad = null;
(function whisperWall() {
const wall = $('#whWall');
if (!wall) return;
const API = '/api/mp/public/mama-box/whisper';
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
const HEART = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5s-7.5-4.6-7.5-10.2A4.3 4.3 0 0112 7.6a4.3 4.3 0 017.5 2.7c0 5.6-7.5 10.2-7.5 10.2z"/></svg>';
let data = null, wk = null, sig = '', busy = false;
const oid = () => hashParam('openid');
function card(it, i) {
const tint = 't' + (i % 4);
const meta = '孕 ' + it.week + ' 周 · ' + esc(it.ago);
const foot = it.mine
? '<span class="wh-got">' + HEART + (it.hugs ? '收到 <b>' + it.hugs + '</b> 个抱抱' : '抱抱正在路上') + '</span>'
: '<button class="wh-hug' + (it.hugged ? ' on' : '') + '" type="button" data-hug="' + esc(it.id) + '" aria-label="给她一个抱抱">' +
HEART + '<span>抱抱</span><b>' + (it.hugs || '') + '</b></button>';
return '<div class="wh-card ' + tint + (it.mine ? ' mine' : '') + '">' +
(it.mine ? '<span class="wh-mine">我说的</span>' : '') +
'<p class="wh-txt">' + esc(it.text) + '</p>' +
'<div class="wh-meta"><span>' + meta + '</span>' + foot + '</div></div>';
}
function render(keepScroll) {
const st = $('#whStrip');
const x = st.scrollLeft;
const list = (data && data.list) || [];
st.innerHTML = list.map(card).join('');
if (keepScroll) st.scrollLeft = x; else st.scrollLeft = 0;
}
function show(on) {
wall.classList.toggle('hide', !on);
const sec = $('#pulse');
if (sec) sec.classList.toggle('has-wh', on);
}
function load(w, silent) {
wk = w;
if (!w) { data = null; show(false); return; }
const o = oid();
fetch(API + '?openid=' + encodeURIComponent(o || '') + '&week=' + w)
.then(r => (r.ok ? r.json() : null))
.then(j => {
const r = j && j.code === 200 && j.result;
if (!r || !r.show || !(r.list || []).length) { data = null; show(false); return; }
data = r;
const ns = r.list.map(x => x.id + ':' + x.hugs + (x.hugged ? 'h' : '')).join('|');
if (ns !== sig) { sig = ns; render(silent); }
show(true);
})
.catch(() => { if (!silent) show(false); });
}
whLoad = w => load(w, false);
setInterval(() => { if (wk && !document.hidden && !busy) load(wk, true); }, 300000);
/* 抱抱:先变(心跳一下、数字 +1),失败再退回 */
$('#whStrip').addEventListener('click', e => {
const b = e.target.closest('[data-hug]');
if (!b) return;
const o = oid();
if (!o) { toast('登录后就能给她抱抱啦'); return; }
if (b.classList.contains('on')) { toast('你已经抱过她啦'); return; }
const it = ((data && data.list) || []).find(x => x.id === b.dataset.hug);
haptic();
b.classList.add('on', 'pop');
setTimeout(() => b.classList.remove('pop'), 600);
const nb = $('b', b);
const n0 = it ? it.hugs : 0;
if (it) { it.hugs = n0 + 1; it.hugged = true; }
nb.textContent = n0 + 1;
fetch(API + '/hug?openid=' + encodeURIComponent(o) + '&id=' + encodeURIComponent(b.dataset.hug), { method: 'POST' })
.then(r => r.json())
.then(j => { if (!j || j.code !== 200) throw j; })
.catch(j => {
b.classList.remove('on');
if (it) { it.hugs = n0; it.hugged = false; }
nb.textContent = n0 || '';
toast((j && j.message) || '网络不太好,再抱一次试试');
});
});
/* 说一句 */
const ta = $('#whTa');
const cnt = () => { $('#whCnt').textContent = ta.value.length + '/' + ((data && data.maxLen) || 40); };
ta.addEventListener('input', () => {
cnt();
$$('.wh-pre', $('#whPresets')).forEach(c => c.classList.toggle('on', c.textContent === ta.value));
});
$('#whSay').addEventListener('click', () => {
if (!data) return;
if (!oid() || !data.canPost) { toast('填好预产期,就能和同周妈妈说悄悄话啦'); return; }
if (!data.left) { toast('今天已经说了 3 句啦,明天再来'); return; }
ta.value = '';
ta.maxLength = data.maxLen || 40;
cnt();
$('#whPresets').innerHTML = (data.presets || []).map(t => '<button class="wh-pre" type="button">' + esc(t) + '</button>').join('');
$('#whLeft').textContent = '今天还能说 ' + data.left + ' 句';
openModal('modalWhisper');
});
$('#whPresets').addEventListener('click', e => {
const c = e.target.closest('.wh-pre');
if (!c) return;
haptic();
ta.value = c.textContent;
ta.dispatchEvent(new Event('input'));
});
$('#whSend').addEventListener('click', () => {
const t = ta.value.trim();
if (t.length < 2) { toast('写一两个字,或者点一句吧'); return; }
if (busy) return;
busy = true;
const btn = $('#whSend');
btn.disabled = true;
btn.textContent = '正在发出…';
fetch(API + '?openid=' + encodeURIComponent(oid()), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: t })
})
.then(r => r.json())
.then(j => {
if (!j || j.code !== 200 || !j.result) { toast((j && j.message) || '没发出去,再试一次'); return; }
const it = j.result;
data.left = it.left;
delete it.left;
data.list = [it].concat(data.list || []);
sig = '';
render(false);
closeModal();
toast('发出去啦,同周妈妈会给你抱抱的');
})
.catch(() => toast('网络不太好,再试一次'))
.then(() => { busy = false; btn.disabled = false; btn.textContent = '悄悄发出'; });
});
})();
(function pulseCard() {
const sec = $('#pulse');
if (!sec) return;
@@ -3311,7 +3451,14 @@
};
/* 用户自己今天做没做(足迹 used[].appId 是上游 URL slug)。足迹没回来 / 读不到 → null,不显示「你」那半句 */
const SELF_APP = { kick: 'taidong', weight: 'yunqi-tizhong' };
const SELF_TXT = { kick: ['你今天还没数', '你也数过了'], weight: ['你今天还没记', '你也记过了'] };
/* 2026-10-02 陪伴版:不说「你今天还没数」(像点名),改成宝宝在等你的邀请;做过了就一句肯定。
同周人数那句也不再给百分比(「她们有 18%」是在和你比),只说多少位妈妈今天也在做。 */
const nmB = () => state.babyName || '宝宝';
const SELF_TXT = {
kick: [() => nmB() + '在等你,陪它数一数', () => '你今天也陪' + nmB() + '数过啦'],
weight: [() => '也记一下今天的自己', () => '你今天也记过啦']
};
const CROWD_TXT = { kick: '位妈妈陪宝宝数了胎动', weight: '位妈妈记下了体重' };
function selfDone(k) {
if (_fpMode !== 'rows' && _fpMode !== 'guide') return null;
return ((_fpData && _fpData.used) || []).some(u => u.appId === SELF_APP[k]);
@@ -3343,7 +3490,8 @@
const avs = $('#ppAvs');
if (avs.dataset.seed === String(seed)) return;
avs.dataset.seed = seed;
avs.innerHTML = [3, 11, 7].map(x => '<i>' + momAva(x + seed) + '</i>').join('');
/* 2026-10-02 用户:「头像现在展示 3 个,给我展示 6 到 8 个」→ 7 个 */
avs.innerHTML = [3, 11, 7, 19, 2, 14, 23].map(x => '<i>' + momAva(x + seed) + '</i>').join('');
}
function paintPeer(p) {
paintAvs(p.week);
@@ -3351,14 +3499,16 @@
/* 只说一句:优先挑你今天还没做的那项;都做了(或读不到你的足迹)就取第一项 */
const ds = (p.doing || []).filter(d => SELF_TXT[d.key]);
const pick = ds.find(d => selfDone(d.key) === false) || ds[0];
if (pick) {
const me = selfDone(pick.key);
const meH = me == null ? '' : me
? '<span class="pp-me ok">' + SELF_TXT[pick.key][1] + ' ✓</span>'
: '<button class="pp-me todo" type="button" data-go="' + pick.key + '">' + SELF_TXT[pick.key][0] + ' ›</button>';
$('#ppDoing').innerHTML = '<div class="pp-do"><span class="pp-dt">她们有 <b>' + (+pick.pct || 0) + '%</b> ' + esc(pick.label) + '</span>' + meH + '</div>';
const n = pick ? Math.round((+p.count || 0) * (+pick.pct || 0) / 100) : 0;
$('#ppCrowd').innerHTML = pick && n >= 3 ? '今天 <b>' + fmtN(n) + '</b> ' + CROWD_TXT[pick.key] : '她们和你一样,在等宝宝来';
const me = pick ? selfDone(pick.key) : null;
if (me != null) {
$('#ppDoing').innerHTML = me
? '<span class="pp-me ok"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.4l3 3 6-6.4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' + esc(SELF_TXT[pick.key][1]()) + '</span>'
: '<button class="pp-me todo" type="button" data-go="' + pick.key + '">' + esc(SELF_TXT[pick.key][0]()) +
'<i aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M6 3.5L10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></i></button>';
}
$('#ppDoing').classList.toggle('hide', !pick);
$('#ppDoing').classList.toggle('hide', me == null);
}
function paint() {
const r = last;
@@ -3371,6 +3521,8 @@
/* 全站版(访客 / real 模式):只留一行总人数 + 滚动动态 */
paintAvs(0);
$('#ppDoing').classList.add('hide');
const td = (r.today || [])[0];
$('#ppCrowd').innerHTML = td ? '今天 <b>' + fmtN(td.value) + '</b> ' + esc(td.label) : '她们和你一样,在等宝宝来';
const t = r.total;
/* 前缀「已有」省掉:一行放得下「10,037 位妈妈在这里迎接宝宝」,带上就会折出一个孤零零的「宝宝」 */
$('#plTitle').innerHTML = t ? '<b data-n="' + t.value + '">0</b> ' + esc(t.suffix) : '孕妈们在做什么';
@@ -3407,6 +3559,7 @@
function load() {
const w = myWeek();
loadedWk = w;
if (whLoad) whLoad(w);
return fetch('/api/mp/public/mama-box/pulse' + (w ? '?week=' + w : ''))
.then(r => (r.ok ? r.json() : null))
.then(j => { if (j && j.code === 200 && j.result) { last = j.result; paint(); } })

View File

@@ -3443,3 +3443,76 @@ a.skip:focus{left:16px;top:16px}
.lc-arrow{flex-basis:26px;width:26px;height:26px}
.lc-arrow svg{width:14px;height:14px}
.lc-env-v{left:9px;top:-10px;width:48px;height:36px;filter:drop-shadow(0 6px 8px rgba(150,30,60,.28))}
/* ================= 此刻 · 陪伴版 + 同周悄悄话(2026-10-02) =================
头像一排 7 个单独成行;「还没数」改成邀请式文字链;悄悄话是一条横滑的小纸条带,四种淡色轮换。 */
.pulse .pl-title{white-space:normal}
.pp-crowd{display:flex;align-items:center;gap:9px;margin-top:9px;min-width:0}
.pp-crowd .pp-avs i{width:24px;height:24px;margin-left:-8px;border:2px solid #fff;box-shadow:0 1px 3px rgba(120,70,50,.12)}
.pp-crowd .pp-avs i:first-child{margin-left:0}
.pp-crowd-t{flex:1;min-width:0;font-size:12.5px;color:var(--muted);line-height:1.35}
.pp-crowd-t b{font-size:14px;font-weight:800;color:var(--ink-2);font-variant-numeric:tabular-nums}
.pp-doing{margin-top:8px}
.pp-me{display:inline-flex;align-items:center;gap:5px;padding:0;background:none;border-radius:0;font-size:13px;font-weight:700}
.pp-me.todo{color:#E5533A;background:none}
.pp-me.todo i{display:grid;place-items:center;width:17px;height:17px;border-radius:50%;background:#FFE5DC}
.pp-me.todo i svg{width:11px;height:11px}
.pp-me.todo:active{opacity:.7;transform:none}
.pp-me.ok{color:#4E8A43;background:none;font-weight:600}
.pp-me.ok svg{width:15px;height:15px}
.pulse.has-wh .pl-feed{display:none}
.wh-wall.hide{display:none}
.wh-wall{margin-top:13px}
.wh-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
.wh-t{font-size:14px;font-weight:800;color:var(--ink);display:flex;align-items:baseline;gap:7px;min-width:0}
.wh-t em{font-style:normal;font-size:11px;font-weight:500;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wh-say{flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;font:inherit;font-size:12.5px;font-weight:700;color:#E5533A;
background:none;border:0;padding:4px 0}
.wh-say::before{content:"";width:14px;height:14px;background:currentColor;
-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 12.5l.6-2.6 7-7a1.4 1.4 0 012 2l-7 7z' fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 12.5l.6-2.6 7-7a1.4 1.4 0 012 2l-7 7z' fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat}
.wh-say:active{opacity:.6}
.wh-strip{display:flex;gap:10px;margin:9px -2px 0;padding:4px 2px 6px;overflow-x:auto;scroll-snap-type:x mandatory;
-webkit-overflow-scrolling:touch;scrollbar-width:none}
.wh-strip::-webkit-scrollbar{display:none}
.wh-card{position:relative;flex:0 0 172px;scroll-snap-align:start;display:flex;flex-direction:column;justify-content:space-between;
min-height:104px;padding:12px 12px 10px;border-radius:16px 16px 16px 5px;background:#FFF4EE}
.wh-card.t0{background:#FFF1EC}
.wh-card.t1{background:#F3F0FF}
.wh-card.t2{background:#FFF7E6}
.wh-card.t3{background:#EEF8F2}
.wh-card.mine{background:#fff;box-shadow:inset 0 0 0 1.5px #FFC9B8}
.wh-mine{position:absolute;top:-6px;right:10px;font-size:10px;font-weight:700;color:#fff;background:#F4593C;padding:2px 7px;border-radius:6px}
.wh-txt{font-size:14px;line-height:1.5;color:var(--ink);font-weight:600;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.wh-txt::before{content:"“";color:#E8A08C;font-weight:800;margin-right:1px}
.wh-txt::after{content:"”";color:#E8A08C;font-weight:800;margin-left:1px}
.wh-meta{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:9px;font-size:10.5px;color:var(--muted);white-space:nowrap}
.wh-hug{display:inline-flex;align-items:center;gap:3px;font:inherit;font-size:11px;font-weight:700;color:#C7837A;
background:rgba(255,255,255,.8);border:0;border-radius:999px;padding:3px 8px 3px 6px;transition:color .2s,background .2s}
.wh-hug svg,.wh-got svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2.2}
.wh-hug b{font-weight:800;font-variant-numeric:tabular-nums}
.wh-hug b:empty{display:none}
.wh-hug.on{color:#fff;background:#F4593C}
.wh-hug.on svg{fill:currentColor}
.wh-hug.pop svg{animation:whPop .6s var(--ease)}
@keyframes whPop{0%{transform:scale(1)}30%{transform:scale(1.5)}60%{transform:scale(.9)}100%{transform:scale(1)}}
.wh-got{display:inline-flex;align-items:center;gap:3px;color:#E5533A;font-weight:600}
.wh-got svg{fill:#F4593C;stroke:#F4593C}
.wh-got b{font-weight:800}
.wh-m-lead{margin-top:8px;font-size:13px;color:var(--muted);line-height:1.55}
.wh-ta-box{position:relative;margin-top:14px}
.wh-ta{display:block;width:100%;box-sizing:border-box;resize:none;font:inherit;font-size:15px;line-height:1.55;color:var(--ink);
padding:13px 15px 24px;border:1.5px solid #EADFD8;border-radius:16px;background:#fff}
.wh-ta:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px rgba(244,89,60,.12)}
.wh-cnt{position:absolute;right:12px;bottom:8px;font-size:11px;color:#C9B7AC;font-variant-numeric:tabular-nums}
.wh-m-sub{margin-top:14px;font-size:12px;font-weight:700;color:var(--ink-2)}
.wh-presets{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
.wh-pre{font:inherit;font-size:12.5px;color:var(--ink-2);background:#fff;border:1px solid #EFE4DD;border-radius:12px;padding:7px 11px;text-align:left}
.wh-pre.on{color:#E5533A;border-color:#FFB7A3;background:#FFF3EE}
.wh-pre:active{transform:scale(.97)}
.wh-modal .modal-foot{margin-top:18px}
.wh-left{margin-top:8px;text-align:center;font-size:11.5px;color:var(--muted)}
@media (prefers-reduced-motion: reduce){.wh-hug.pop svg{animation:none}}
.wh-modal .modal-foot{flex-direction:column;align-items:stretch;gap:0}