openvibers/openvibe.design Commits
/brand/explore: votes show the moment you tap; a clean tally (#65)
Changes
@@@ -390,3 +390,15 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}.mx-nudge span{flex:1;min-width:0}.mx-nudge .mx-btn{flex:none;padding:5px 10px}}++++/* The tally, clean: one line while empty; a labelled split bar; short rows of the top picks. */++.mx-tally{gap:8px}++.mx-tally.is-empty{padding:10px 12px}++.mx-tally-n{color:var(--text-secondary,#b4bfcc);font-size:14px}++.mx-split{position:relative;display:flex;justify-content:space-between;align-items:center;height:30px;padding:0 10px;border-radius:999px;overflow:hidden;background:#8b5cf6;color:#fff;font-size:13px;font-weight:700;isolation:isolate}++.mx-split::before{content:"";position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;z-index:-1;transition:width .5s cubic-bezier(.2,.8,.2,1)}++.mx-split span{display:inline-flex;align-items:center;gap:5px}++.mx-split b{font-family:var(--mx-face);font-weight:900}++.mx-split svg{filter:drop-shadow(0 0 2px rgba(0,0,0,.35))}++.mx-tops-k{width:56px}++@media (max-width:640px){.mx-tops-k{width:44px}}@@@ -93,13 +93,15 @@}var saveTimer = 0;function toggle(code) {++ var prev = picks.slice();var i = picks.indexOf(code);if (i === -1) picks.push(code); else picks.splice(i, 1);render();var card = document.getElementById(code);if (card && i === -1 && !still()) { card.classList.remove('just-picked'); void card.offsetWidth; card.classList.add('just-picked'); setTimeout(function () { card.classList.remove('just-picked'); }, 500); }if (i === -1 && typeof offerStep2 === 'function') offerStep2(code);-- clearTimeout(saveTimer); saveTimer = setTimeout(sync, 700);++ countLocally(prev);++ clearTimeout(saveTimer); saveTimer = setTimeout(sync, 250);}function say(btn, words) { var was = btn.textContent; btn.textContent = words; setTimeout(function () { btn.textContent = was; }, 1400); }function copy(text, btn) {@@@ -143,19 +145,45 @@if (!items.length) { var em = document.createElement('em'); em.textContent = 'none yet'; list.appendChild(em); return; }items.forEach(function (t) { var c = chip(t); if (!before[t.code]) c.classList.add('is-new'); list.appendChild(c); });}++ var lastTally = null;function showTally(t) {if (!tally || !t) return;++ lastTally = t;var loop = t.lean.loop, pair = t.lean.pair, sum = loop + pair, pct = sum ? Math.round((loop / sum) * 100) : 50;tally.classList.toggle('is-empty', !t.people);-- tally.querySelector('[data-mx-people]').textContent = t.people ? t.people + (t.people === 1 ? ' person' : ' people') : 'No votes yet. Be the first.';-- tally.querySelector('[data-mx-lead]').textContent = sum ? (loop === pair ? 'Level so far' : loop > pair ? 'The Loop leads' : 'The Pair leads') : 'No picks yet';-- tally.querySelector('.mx-lean').style.setProperty('--l', pct + '%');-- tally.querySelector('[data-mx-loop]').textContent = sum ? pct + '%' : '–';-- tally.querySelector('[data-mx-pair]').textContent = sum ? (100 - pct) + '%' : '–';-- var marks = (t.top.L || []).concat(t.top.P || []).sort(function (a, b) { return b.n - a.n || (a.code < b.code ? -1 : 1); }).slice(0, 6);-- fill(tally.querySelector('[data-mx-tops]'), marks);-- fill(tally.querySelector('[data-mx-tops-m]'), (t.top.M || []).slice(0, 4));-- fill(tally.querySelector('[data-mx-tops-f]'), (t.top.F || []).slice(0, 4));++ tally.querySelector('[data-mx-people]').textContent = t.people ? t.people + (t.people === 1 ? ' person' : ' people') : 'No votes yet. Tap any logo to vote.';++ tally.querySelector('[data-mx-lead]').textContent = sum ? (loop === pair ? 'Tied' : loop > pair ? 'The Loop leads' : 'The Pair leads') : '';++ var split = tally.querySelector('[data-mx-split]');++ split.hidden = !sum;++ split.style.setProperty('--l', pct + '%');++ tally.querySelector('[data-mx-loop]').textContent = pct + '%';++ tally.querySelector('[data-mx-pair]').textContent = (100 - pct) + '%';++ var marks = (t.top.L || []).concat(t.top.P || []).sort(function (a, b) { return b.n - a.n || (a.code < b.code ? -1 : 1); }).slice(0, 5);++ [['[data-mx-tops]', marks], ['[data-mx-tops-m]', (t.top.M || []).slice(0, 3)], ['[data-mx-tops-f]', (t.top.F || []).slice(0, 3)]].forEach(function (pair2) {++ var list = tally.querySelector(pair2[0]);++ if (!list) return;++ list.closest('.mx-tops').hidden = !pair2[1].length;++ fill(list, pair2[1]);++ });++ }++ // The tap shows at once: this person's vote is counted into the last tally on the page before the server answers++ // (then the server's own count replaces it).++ var before = null;++ function countLocally(prev) {++ if (!lastTally) return;++ var t = JSON.parse(JSON.stringify(lastTally));++ var had = prev.length > 0, has = picks.length > 0;++ t.people = Math.max(0, t.people + (has && !had ? 1 : !has && had ? -1 : 0));++ ['L', 'P', 'M', 'F'].forEach(function (k) {++ var list = t.top[k] || (t.top[k] = []);++ prev.filter(function (c) { return c[0] === k && picks.indexOf(c) === -1; }).forEach(function (c) { var e = list.find(function (x) { return x.code === c; }); if (e) e.n -= 1; });++ picks.filter(function (c) { return c[0] === k && prev.indexOf(c) === -1; }).forEach(function (c) { var e = list.find(function (x) { return x.code === c; }); if (e) e.n += 1; else list.push({ code: c, n: 1 }); });++ t.top[k] = list.filter(function (x) { return x.n > 0; }).sort(function (a, b) { return b.n - a.n; });++ });++ var lean = function (arr) { var l = arr.filter(function (c) { return c[0] === 'L'; }).length, p = arr.filter(function (c) { return c[0] === 'P'; }).length; return l > p ? [1, 0] : p > l ? [0, 1] : l ? [0.5, 0.5] : [0, 0]; };++ var a = lean(prev), b = lean(picks);++ t.lean = { loop: Math.max(0, t.lean.loop - a[0] + b[0]), pair: Math.max(0, t.lean.pair - a[1] + b[1]) };++ showTally(t);}var savedNote = root.querySelector('[data-mx-saved]');function sync() {@@@ -707,20 +707,24 @@ function miniArt(code, size = 26, tag = '') {const chipOf = ({ code, n }) => `<span class="mx-top" data-code="${code}">${miniArt(code)}<b>${code}</b><i>${n}</i></span>`;/** What people picked so far (server/brand-picks.js tally), at the top; marks.js redraws it after each save. */function tallyBlock(t) {++ // Clean (the owner, 11-01: "a mess… see which is the most popular easily… everyone else can too"): one line while++ // there are no votes; then who leads, the Loop against the Pair as one labelled bar, and a short row of the top picks++ // for each step that has votes. marks.js redraws it the moment a card is tapped.const people = (t && t.people) || 0;const loop = (t && t.lean && t.lean.loop) || 0, pair = (t && t.lean && t.lean.pair) || 0;const loopPct = loop + pair ? Math.round((loop / (loop + pair)) * 100) : 50;-- const marks = t ? [...(t.top.L || []), ...(t.top.P || [])].sort((a, b) => b.n - a.n || (a.code < b.code ? -1 : 1)).slice(0, 6) : [];-- const menus = t ? (t.top.M || []).slice(0, 4) : [];-- const fonts = t ? (t.top.F || []).slice(0, 4) : [];-- const lead = loop + pair ? (loop === pair ? 'Level so far' : loop > pair ? 'The Loop leads' : 'The Pair leads') : 'No picks yet';-- return `<section class="mx-tally${people ? '' : ' is-empty'}" data-mx-tally aria-label="What people picked so far">--<div class="mx-tally-h"><b>Votes so far</b><span data-mx-people>${people ? `${people} ${people === 1 ? 'person' : 'people'}` : 'No votes yet. Be the first.'}</span><span class="mx-tally-lead" data-mx-lead>${lead}</span></div>--<div class="mx-lean" style="--l:${loopPct}%"><span class="mx-lean-end">${miniArt('L01', 24, 'e')}<span>Loop</span><b data-mx-loop>${loop + pair ? `${loopPct}%` : '–'}</b></span><span class="mx-lean-bar" aria-hidden="true"><i></i></span><span class="mx-lean-end mx-lean-r"><b data-mx-pair>${loop + pair ? `${100 - loopPct}%` : '–'}</b><span>Pair</span>${miniArt('P01', 24, 'e')}</span></div>--<div class="mx-tops"><span class="mx-tops-k">Top logos</span><span class="mx-tops-list" data-mx-tops>${marks.map(chipOf).join('') || '<em>none yet</em>'}</span></div>--<div class="mx-mine" data-mx-mine hidden><span class="mx-tops-k">Your votes</span><b data-mx-picks></b><button type="button" class="mx-btn" data-mx-link>Copy link</button><button type="button" class="mx-btn mx-btn-quiet" data-mx-clear>Clear</button><em class="mx-saved" data-mx-saved>saved</em></div>--<div class="mx-tops"><span class="mx-tops-k">Menu button</span><span class="mx-tops-list" data-mx-tops-m>${menus.map(chipOf).join('') || '<em>none yet</em>'}</span></div>--<div class="mx-tops"><span class="mx-tops-k">Font</span><span class="mx-tops-list" data-mx-tops-f>${fonts.map(chipOf).join('') || '<em>none yet</em>'}</span></div>++ const marks = t ? [...(t.top.L || []), ...(t.top.P || [])].sort((a, b) => b.n - a.n || (a.code < b.code ? -1 : 1)).slice(0, 5) : [];++ const menus = t ? (t.top.M || []).slice(0, 3) : [];++ const fonts = t ? (t.top.F || []).slice(0, 3) : [];++ const lead = loop + pair ? (loop === pair ? 'Tied' : loop > pair ? 'The Loop leads' : 'The Pair leads') : '';++ const row = (label, attr, list) => `<div class="mx-tops"${list.length ? '' : ' hidden'}><span class="mx-tops-k">${label}</span><span class="mx-tops-list" ${attr}>${list.map(chipOf).join('')}</span></div>`;++ return `<section class="mx-tally${people ? '' : ' is-empty'}" data-mx-tally aria-label="Votes so far" aria-live="polite">++<div class="mx-tally-h"><b>Votes so far</b><span class="mx-tally-n" data-mx-people>${people ? `${people} ${people === 1 ? 'person' : 'people'}` : 'No votes yet. Tap any logo to vote.'}</span><span class="mx-tally-lead" data-mx-lead>${lead}</span></div>++<div class="mx-split" data-mx-split style="--l:${loopPct}%"${loop + pair ? '' : ' hidden'}><span class="mx-split-l">${miniArt('L01', 18, 'e')}Loop <b data-mx-loop>${loopPct}%</b></span><span class="mx-split-r"><b data-mx-pair>${100 - loopPct}%</b> Pair${miniArt('P01', 18, 'e')}</span></div>++${row('Logos', 'data-mx-tops', marks)}++${row('Menu', 'data-mx-tops-m', menus)}++${row('Font', 'data-mx-tops-f', fonts)}++<div class="mx-mine" data-mx-mine hidden><span class="mx-tops-k">Yours</span><b data-mx-picks></b><button type="button" class="mx-btn" data-mx-link>Copy link</button><button type="button" class="mx-btn mx-btn-quiet" data-mx-clear>Clear</button><em class="mx-saved" data-mx-saved>saved</em></div></section>`;}
@@ -390,3 +390,15 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@@ -390,3 +390,15 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
.mx-nudge span{flex:1;min-width:0}
.mx-nudge span{flex:1;min-width:0}
.mx-nudge .mx-btn{flex:none;padding:5px 10px}
.mx-nudge .mx-btn{flex:none;padding:5px 10px}
}
}
+
+/* The tally, clean: one line while empty; a labelled split bar; short rows of the top picks. */
+.mx-tally{gap:8px}
+.mx-tally.is-empty{padding:10px 12px}
+.mx-tally-n{color:var(--text-secondary,#b4bfcc);font-size:14px}
+.mx-split{position:relative;display:flex;justify-content:space-between;align-items:center;height:30px;padding:0 10px;border-radius:999px;overflow:hidden;background:#8b5cf6;color:#fff;font-size:13px;font-weight:700;isolation:isolate}
+.mx-split::before{content:"";position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;z-index:-1;transition:width .5s cubic-bezier(.2,.8,.2,1)}
+.mx-split span{display:inline-flex;align-items:center;gap:5px}
+.mx-split b{font-family:var(--mx-face);font-weight:900}
+.mx-split svg{filter:drop-shadow(0 0 2px rgba(0,0,0,.35))}
+.mx-tops-k{width:56px}
+@media (max-width:640px){.mx-tops-k{width:44px}}
@@ -93,13 +93,15 @@
@@ -93,13 +93,15 @@
}
}
var saveTimer = 0;
var saveTimer = 0;
function toggle(code) {
function toggle(code) {
+ var prev = picks.slice();
var i = picks.indexOf(code);
var i = picks.indexOf(code);
if (i === -1) picks.push(code); else picks.splice(i, 1);
if (i === -1) picks.push(code); else picks.splice(i, 1);
render();
render();
var card = document.getElementById(code);
var card = document.getElementById(code);
if (card && i === -1 && !still()) { card.classList.remove('just-picked'); void card.offsetWidth; card.classList.add('just-picked'); setTimeout(function () { card.classList.remove('just-picked'); }, 500); }
if (card && i === -1 && !still()) { card.classList.remove('just-picked'); void card.offsetWidth; card.classList.add('just-picked'); setTimeout(function () { card.classList.remove('just-picked'); }, 500); }
if (i === -1 && typeof offerStep2 === 'function') offerStep2(code);
if (i === -1 && typeof offerStep2 === 'function') offerStep2(code);
- clearTimeout(saveTimer); saveTimer = setTimeout(sync, 700);
+ countLocally(prev);
+ clearTimeout(saveTimer); saveTimer = setTimeout(sync, 250);
}
}
function say(btn, words) { var was = btn.textContent; btn.textContent = words; setTimeout(function () { btn.textContent = was; }, 1400); }
function say(btn, words) { var was = btn.textContent; btn.textContent = words; setTimeout(function () { btn.textContent = was; }, 1400); }
function copy(text, btn) {
function copy(text, btn) {
@@ -143,19 +145,45 @@
@@ -143,19 +145,45 @@
if (!items.length) { var em = document.createElement('em'); em.textContent = 'none yet'; list.appendChild(em); return; }
if (!items.length) { var em = document.createElement('em'); em.textContent = 'none yet'; list.appendChild(em); return; }
items.forEach(function (t) { var c = chip(t); if (!before[t.code]) c.classList.add('is-new'); list.appendChild(c); });
items.forEach(function (t) { var c = chip(t); if (!before[t.code]) c.classList.add('is-new'); list.appendChild(c); });
}
}
+ var lastTally = null;
function showTally(t) {
function showTally(t) {
if (!tally || !t) return;
if (!tally || !t) return;
+ lastTally = t;
var loop = t.lean.loop, pair = t.lean.pair, sum = loop + pair, pct = sum ? Math.round((loop / sum) * 100) : 50;
var loop = t.lean.loop, pair = t.lean.pair, sum = loop + pair, pct = sum ? Math.round((loop / sum) * 100) : 50;
tally.classList.toggle('is-empty', !t.people);
tally.classList.toggle('is-empty', !t.people);
- tally.querySelector('[data-mx-people]').textContent = t.people ? t.people + (t.people === 1 ? ' person' : ' people') : 'No votes yet. Be the first.';
- tally.querySelector('[data-mx-lead]').textContent = sum ? (loop === pair ? 'Level so far' : loop > pair ? 'The Loop leads' : 'The Pair leads') : 'No picks yet';
- tally.querySelector('.mx-lean').style.setProperty('--l', pct + '%');
- tally.querySelector('[data-mx-loop]').textContent = sum ? pct + '%' : '–';
- tally.querySelector('[data-mx-pair]').textContent = sum ? (100 - pct) + '%' : '–';
- var marks = (t.top.L || []).concat(t.top.P || []).sort(function (a, b) { return b.n - a.n || (a.code < b.code ? -1 : 1); }).slice(0, 6);
- fill(tally.querySelector('[data-mx-tops]'), marks);
- fill(tally.querySelector('[data-mx-tops-m]'), (t.top.M || []).slice(0, 4));
- fill(tally.querySelector('[data-mx-tops-f]'), (t.top.F || []).slice(0, 4));
+ tally.querySelector('[data-mx-people]').textContent = t.people ? t.people + (t.people === 1 ? ' person' : ' people') : 'No votes yet. Tap any logo to vote.';
+ tally.querySelector('[data-mx-lead]').textContent = sum ? (loop === pair ? 'Tied' : loop > pair ? 'The Loop leads' : 'The Pair leads') : '';
+ var split = tally.querySelector('[data-mx-split]');
+ split.hidden = !sum;
+ split.style.setProperty('--l', pct + '%');
+ tally.querySelector('[data-mx-loop]').textContent = pct + '%';
+ tally.querySelector('[data-mx-pair]').textContent = (100 - pct) + '%';
+ var marks = (t.top.L || []).concat(t.top.P || []).sort(function (a, b) { return b.n - a.n || (a.code < b.code ? -1 : 1); }).slice(0, 5);
+ [['[data-mx-tops]', marks], ['[data-mx-tops-m]', (t.top.M || []).slice(0, 3)], ['[data-mx-tops-f]', (t.top.F || []).slice(0, 3)]].forEach(function (pair2) {
+ var list = tally.querySelector(pair2[0]);
+ if (!list) return;
+ list.closest('.mx-tops').hidden = !pair2[1].length;
+ fill(list, pair2[1]);
+ });
+ }
+ // The tap shows at once: this person's vote is counted into the last tally on the page before the server answers
+ // (then the server's own count replaces it).
+ var before = null;
+ function countLocally(prev) {
+ if (!lastTally) return;
+ var t = JSON.parse(JSON.stringify(lastTally));
+ var had = prev.length > 0, has = picks.length > 0;
+ t.people = Math.max(0, t.people + (has && !had ? 1 : !has && had ? -1 : 0));
+ ['L', 'P', 'M', 'F'].forEach(function (k) {
+ var list = t.top[k] || (t.top[k] = []);
+ prev.filter(function (c) { return c[0] === k && picks.indexOf(c) === -1; }).forEach(function (c) { var e = list.find(function (x) { return x.code === c; }); if (e) e.n -= 1; });
+ picks.filter(function (c) { return c[0] === k && prev.indexOf(c) === -1; }).forEach(function (c) { var e = list.find(function (x) { return x.code === c; }); if (e) e.n += 1; else list.push({ code: c, n: 1 }); });
+ t.top[k] = list.filter(function (x) { return x.n > 0; }).sort(function (a, b) { return b.n - a.n; });
+ });
+ var lean = function (arr) { var l = arr.filter(function (c) { return c[0] === 'L'; }).length, p = arr.filter(function (c) { return c[0] === 'P'; }).length; return l > p ? [1, 0] : p > l ? [0, 1] : l ? [0.5, 0.5] : [0, 0]; };
+ var a = lean(prev), b = lean(picks);
+ t.lean = { loop: Math.max(0, t.lean.loop - a[0] + b[0]), pair: Math.max(0, t.lean.pair - a[1] + b[1]) };
+ showTally(t);
}
}
var savedNote = root.querySelector('[data-mx-saved]');
var savedNote = root.querySelector('[data-mx-saved]');
function sync() {
function sync() {
@@ -707,20 +707,24 @@ function miniArt(code, size = 26, tag = '') {
@@ -707,20 +707,24 @@ function miniArt(code, size = 26, tag = '') {
const chipOf = ({ code, n }) => `<span class="mx-top" data-code="${code}">${miniArt(code)}<b>${code}</b><i>${n}</i></span>`;
const chipOf = ({ code, n }) => `<span class="mx-top" data-code="${code}">${miniArt(code)}<b>${code}</b><i>${n}</i></span>`;
/** What people picked so far (server/brand-picks.js tally), at the top; marks.js redraws it after each save. */
/** What people picked so far (server/brand-picks.js tally), at the top; marks.js redraws it after each save. */
function tallyBlock(t) {
function tallyBlock(t) {
+ // Clean (the owner, 11-01: "a mess… see which is the most popular easily… everyone else can too"): one line while
+ // there are no votes; then who leads, the Loop against the Pair as one labelled bar, and a short row of the top picks
+ // for each step that has votes. marks.js redraws it the moment a card is tapped.
const people = (t && t.people) || 0;
const people = (t && t.people) || 0;
const loop = (t && t.lean && t.lean.loop) || 0, pair = (t && t.lean && t.lean.pair) || 0;
const loop = (t && t.lean && t.lean.loop) || 0, pair = (t && t.lean && t.lean.pair) || 0;
const loopPct = loop + pair ? Math.round((loop / (loop + pair)) * 100) : 50;
const loopPct = loop + pair ? Math.round((loop / (loop + pair)) * 100) : 50;
- const marks = t ? [...(t.top.L || []), ...(t.top.P || [])].sort((a, b) => b.n - a.n || (a.code < b.code ? -1 : 1)).slice(0, 6) : [];
- const menus = t ? (t.top.M || []).slice(0, 4) : [];
- const fonts = t ? (t.top.F || []).slice(0, 4) : [];
- const lead = loop + pair ? (loop === pair ? 'Level so far' : loop > pair ? 'The Loop leads' : 'The Pair leads') : 'No picks yet';
- return `<section class="mx-tally${people ? '' : ' is-empty'}" data-mx-tally aria-label="What people picked so far">
-<div class="mx-tally-h"><b>Votes so far</b><span data-mx-people>${people ? `${people} ${people === 1 ? 'person' : 'people'}` : 'No votes yet. Be the first.'}</span><span class="mx-tally-lead" data-mx-lead>${lead}</span></div>
-<div class="mx-lean" style="--l:${loopPct}%"><span class="mx-lean-end">${miniArt('L01', 24, 'e')}<span>Loop</span><b data-mx-loop>${loop + pair ? `${loopPct}%` : '–'}</b></span><span class="mx-lean-bar" aria-hidden="true"><i></i></span><span class="mx-lean-end mx-lean-r"><b data-mx-pair>${loop + pair ? `${100 - loopPct}%` : '–'}</b><span>Pair</span>${miniArt('P01', 24, 'e')}</span></div>
-<div class="mx-tops"><span class="mx-tops-k">Top logos</span><span class="mx-tops-list" data-mx-tops>${marks.map(chipOf).join('') || '<em>none yet</em>'}</span></div>
-<div class="mx-mine" data-mx-mine hidden><span class="mx-tops-k">Your votes</span><b data-mx-picks></b><button type="button" class="mx-btn" data-mx-link>Copy link</button><button type="button" class="mx-btn mx-btn-quiet" data-mx-clear>Clear</button><em class="mx-saved" data-mx-saved>saved</em></div>
-<div class="mx-tops"><span class="mx-tops-k">Menu button</span><span class="mx-tops-list" data-mx-tops-m>${menus.map(chipOf).join('') || '<em>none yet</em>'}</span></div>
-<div class="mx-tops"><span class="mx-tops-k">Font</span><span class="mx-tops-list" data-mx-tops-f>${fonts.map(chipOf).join('') || '<em>none yet</em>'}</span></div>
+ const marks = t ? [...(t.top.L || []), ...(t.top.P || [])].sort((a, b) => b.n - a.n || (a.code < b.code ? -1 : 1)).slice(0, 5) : [];
+ const menus = t ? (t.top.M || []).slice(0, 3) : [];
+ const fonts = t ? (t.top.F || []).slice(0, 3) : [];
+ const lead = loop + pair ? (loop === pair ? 'Tied' : loop > pair ? 'The Loop leads' : 'The Pair leads') : '';
+ const row = (label, attr, list) => `<div class="mx-tops"${list.length ? '' : ' hidden'}><span class="mx-tops-k">${label}</span><span class="mx-tops-list" ${attr}>${list.map(chipOf).join('')}</span></div>`;
+ return `<section class="mx-tally${people ? '' : ' is-empty'}" data-mx-tally aria-label="Votes so far" aria-live="polite">
+<div class="mx-tally-h"><b>Votes so far</b><span class="mx-tally-n" data-mx-people>${people ? `${people} ${people === 1 ? 'person' : 'people'}` : 'No votes yet. Tap any logo to vote.'}</span><span class="mx-tally-lead" data-mx-lead>${lead}</span></div>
+<div class="mx-split" data-mx-split style="--l:${loopPct}%"${loop + pair ? '' : ' hidden'}><span class="mx-split-l">${miniArt('L01', 18, 'e')}Loop <b data-mx-loop>${loopPct}%</b></span><span class="mx-split-r"><b data-mx-pair>${100 - loopPct}%</b> Pair${miniArt('P01', 18, 'e')}</span></div>
+${row('Logos', 'data-mx-tops', marks)}
+${row('Menu', 'data-mx-tops-m', menus)}
+${row('Font', 'data-mx-tops-f', fonts)}
+<div class="mx-mine" data-mx-mine hidden><span class="mx-tops-k">Yours</span><b data-mx-picks></b><button type="button" class="mx-btn" data-mx-link>Copy link</button><button type="button" class="mx-btn mx-btn-quiet" data-mx-clear>Clear</button><em class="mx-saved" data-mx-saved>saved</em></div>
</section>`;
</section>`;
}
}