openvibers/openvibe.design Commits
Eleven more menu buttons made for this button; pressing one shows the menu it opens (#237)
Changes
@@@ -179,7 +179,6 @@.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-s:not([stroke-dasharray]){stroke-dashoffset:100}.mx-wrap.mx-live .mx-stage .mx-f{transition:opacity var(--ov-dur-4, .8s) var(--ov-ease-out, ease) calc(var(--d,0s) + .5s)}.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-f{opacity:0}--@media (prefers-reduced-motion:no-preference){.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=breathe]{animation:mxBreathe 5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=drift]{animation:mxDrift 9s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=rock]{animation:mxRock 6.5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}@@@ -187,7 +186,6 @@.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=nod] .mx-b{animation:mxNod 4.2s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=dot] .mx-dot{animation:mxDot 3.4s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=pulse] .mx-glow{animation:mxPulse 3.6s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}--}@keyframes mxBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}@keyframes mxDrift{0%,100%{filter:hue-rotate(0deg)}50%{filter:hue-rotate(-32deg) saturate(1.15)}}@keyframes mxRock{0%,100%{transform:rotate(0deg)}30%{transform:rotate(-3.5deg)}70%{transform:rotate(3.5deg)}}@@@ -233,7 +231,47 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans.mb-btn.is-open svg[data-own=burger] .mb-l2{transform:translateY(-5.5px) rotate(-45deg)}.mb-btn.is-open svg[data-own=burger2] .mb-l0{transform:translateY(3px) rotate(45deg)}.mb-btn.is-open svg[data-own=burger2] .mb-l2{transform:translateY(-3px) rotate(-45deg)}--@media (prefers-reduced-motion:no-preference){++/* 2026-11-10, the buttons drawn from the logo and from the menu. A V is the top half of an ×: its two strokes are++ drawn half way (mb-h) and grow through to the other side as it opens. Two Vs slide until their points meet. Four++ petals turn an eighth. A ring of apps, and the launcher's grid with a V lit in it, drop their edge dots and++ leave an × of dots. */++.mb-btn .mb-h{stroke-dasharray:50 100;transition:stroke-dasharray var(--ov-dur-4, .45s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)),transform var(--ov-dur-4, .45s) var(--ov-ease-spring, cubic-bezier(.2,1.3,.3,1)),opacity var(--ov-dur-2, .25s)}++.mb-btn.is-open svg:is([data-own=grow],[data-own=growdot]) .mb-h{stroke-dasharray:100 100}++.mb-btn.is-open svg[data-own=growdot] .mb-o{transform:translateY(-6.4px) scale(.84)}++.mb-btn.is-open svg[data-own=meet] .mb-u{transform:translateY(2.4px)}++.mb-btn.is-open svg[data-own=meet] .mb-d{transform:translateY(-2.4px)}++.mb-btn.is-open svg:is([data-own=gather],[data-own=vgrid]) .mb-r1{opacity:0;transform:scale(0)}++.mb-btn.is-open svg[data-own=gather] .mb-r2{transform:scale(1.75)}++.mb-btn.is-open svg[data-own=vgrid] .mb-r2{fill:var(--mb-a);opacity:1;transform:scale(1.14)}++.mb-btn .mb-c1{fill:var(--mb-c1,#22d3ee)}++.mb-btn .mb-c3{fill:var(--mb-c3,#a78bfa)}++.mb-light .mb-btn{--mb-c1:#0891b2;--mb-c3:#7c3aed}++.mb-btn .mb-orb{transform-box:view-box;transform-origin:12px 12px}++/* Pressed, a ring leaves the button (once). */++.mb-btn::after{content:"";position:absolute;inset:0;border-radius:50%;border:2px solid var(--mb-a);opacity:0;pointer-events:none}++.mb-btn.is-open::after{animation:mbBurst var(--ov-dur-4, .55s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)) both}++@keyframes mbBurst{from{opacity:.75;transform:scale(.35)}to{opacity:0;transform:scale(1.85)}}++/* What it opens, on the stage behind the button: the menu in small (its search bar, its three tabs, your apps), grown++ out of the button. The button itself goes to the menu's top right and is its close button: the one button the whole++ way (the rule "the button that opens it closes it"). A picture, not a control. */++.mb-card .mx-stage{position:relative;overflow:hidden;container-type:size}++.mb-card .mb-big{position:relative;z-index:2;transition:transform var(--ov-dur-4, .5s) var(--ov-ease-spring, cubic-bezier(.2,1.15,.3,1))}++/* Beside the card's own + (which keeps the corner), on the search bar's line. */++.mb-card .mx-stage:has(.mb-big.is-open) .mb-big{transform:translate(calc(50cqw - 72px),calc(-50cqh + 24px)) scale(.4)}++.mb-peek{position:absolute;inset:0;z-index:1;display:grid;grid-template-rows:auto auto 1fr;gap:8px;padding:12px;box-sizing:border-box;pointer-events:none;opacity:0;++ clip-path:circle(0 at 50% 50%);transition:clip-path var(--ov-dur-4, .5s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)),opacity var(--ov-dur-2, .25s);++ background:linear-gradient(180deg,color-mix(in srgb,#3b82f6 10%,#0d1322),#0b1020)}++.mx-stage:has(.mb-big.is-open) .mb-peek{opacity:1;clip-path:circle(150% at 50% 50%)}++.mb-peek i{display:block;opacity:0;transform:translateY(calc(8px * var(--ov-move, 1))) scale(.94);transition:opacity var(--ov-dur-3, .3s),transform var(--ov-dur-4, .45s) var(--ov-ease-spring, cubic-bezier(.2,1.2,.3,1));transition-delay:0s}++.mx-stage:has(.mb-big.is-open) .mb-peek i{opacity:1;transform:none;transition-delay:calc(90ms + var(--i, 0) * 32ms)}++.mb-peek-s{height:24px;margin-right:88px;border-radius:999px;background:color-mix(in srgb,#e8edf3 7%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,#e8edf3 14%,transparent);position:relative}++.mb-peek-s::before{content:"";position:absolute;left:9px;top:50%;width:8px;height:8px;margin-top:-5px;border-radius:50%;border:1.6px solid #7aa2ff}++.mb-peek-t{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}++.mb-peek-t i{height:16px;border-radius:8px;background:color-mix(in srgb,#e8edf3 8%,transparent)}++.mb-peek-t i:last-child{background:color-mix(in srgb,#3b82f6 34%,#131c2e)}++.mb-peek-g{display:grid;grid-template-columns:repeat(4,1fr);gap:8px 10px;align-content:start;justify-items:center}++.mb-peek-g i{width:min(100%,34px);aspect-ratio:1;border-radius:28%;background:linear-gradient(150deg,hsl(var(--h) 72% 58% / .5),hsl(var(--h) 70% 38% / .28));box-shadow:inset 0 0 0 1px hsl(var(--h) 70% 64% / .32)}++html[data-ov-motion=off] .mb-peek,html[data-ov-motion=off] .mb-peek i{transition:none}.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=ripple] .mb-p{animation:mbRipple var(--ov-dur-4, .75s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .08s)}.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=wave] .mb-p{animation:mbWave var(--ov-dur-4, .7s) var(--ov-ease-in-out, ease-in-out) calc(var(--i,0) * .07s)}.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=bloom] .mb-p{animation:mbBloom var(--ov-dur-4, .7s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .06s)}@@@ -246,7 +284,10 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg:is([data-anim=pop],[data-anim=pulse]) .mb-i{animation:mbPop var(--ov-dur-4, .6s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1))}.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=tilt] .mb-i{animation:mbTilt var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=nod] .mb-i{animation:mbNod var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}--}++ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=apart] .mb-u{animation:mbApart var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}++ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=apart] .mb-d{animation:mbApart var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out) reverse}++ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=orbit] .mb-orb{animation:mbOrbit 1.2s var(--ov-ease-in-out, cubic-bezier(.65,0,.35,1))}++ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=spin] .mb-g{animation:mbSpin .9s var(--ov-ease-in-out, cubic-bezier(.65,0,.35,1))}@keyframes mbRipple{0%,100%{transform:scale(1)}35%{transform:scale(.5)}70%{transform:scale(1.2)}}@keyframes mbWave{0%,100%{transform:translateY(0)}40%{transform:translateY(-2.6px)}}@keyframes mbBloom{0%,100%{transform:scale(1)}45%{transform:scale(1.24)}}@@@ -259,6 +300,9 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}++@keyframes mbApart{0%,100%{transform:translateY(0)}45%{transform:translateY(-1.6px)}}++@keyframes mbOrbit{50%{transform:rotate(200deg)}}++@keyframes mbSpin{to{transform:rotate(90deg)}}.mb-card .mx-stage{color:#e8edf3}.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}.mb-big::before{inset:8px}@@@ -410,6 +454,16 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans.openvibe-navbar .ovnav-orb .mx-navmenu{all:unset;position:relative;display:grid;place-items:center;width:24px;height:24px;color:inherit}.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}++/* The two close buttons the picked menu button becomes (the menu's own and the Dock's) wear it too, open. Their own++ drawing stays in place, unseen: the Dock measures it to start on the bar's button. */++.mx-skinned > *{grid-area:1/1}++.mx-skinned > svg{opacity:0}++.mx-skinned > .mx-navmenu{all:unset;grid-area:1/1;display:grid;place-items:center;width:24px;height:24px;color:inherit;pointer-events:none}++.mx-skinned > .mx-navmenu svg{width:22px;height:22px}++/* On this page the bar is the preview, so it stays in view (the owner, 11-10: "the nav bar will just go away… while you++ scroll… disable that on that page"). Everywhere else a phone's bar still hides as you scroll down. */++html:has(.mx-wrap) .openvibe-navbar.ovnav-hide{transform:none!important}++html:has(.mx-wrap) #navbar-mount:has(> .ovnav-hide){pointer-events:auto}@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */@@@ -477,7 +531,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans/* Each wildcard moves by its kind of motion (data-wild-motion): only while its card is on screen, with long rests,and never under reduced motion. The parts that move are w-a, w-b and w-c. */.mx-wild svg :is(.w-a,.w-b,.w-c){transform-box:fill-box;transform-origin:center}--@media (prefers-reduced-motion:no-preference){.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion] :is(.w-a,.w-b,.w-c){animation-duration:3.6s;animation-iteration-count:infinite;animation-timing-function:var(--ov-ease-in-out, ease-in-out);animation-delay:calc(var(--d,0s) + var(--i,0) * .09s)}.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=draw] .w-a{stroke-dasharray:100;animation-name:wDraw;animation-duration:3.4s}.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=join] .w-a{animation-name:wJoinL}@@@ -508,7 +561,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-a{animation-name:wGlitchL;animation-duration:2.8s;animation-timing-function:steps(1)}.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-b{animation-name:wGlitchR;animation-duration:2.8s;animation-timing-function:steps(1)}.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=caret] .w-a{animation-name:wCaret;animation-duration:1.1s;animation-timing-function:steps(1)}--}@keyframes wBar{0%,100%{transform:scaleY(1)}30%{transform:scaleY(.55)}60%{transform:scaleY(1.08)}}@keyframes wRipple{0%,60%,100%{opacity:1;transform:scale(1)}25%{opacity:.2;transform:scale(1.08)}}@keyframes wHum{0%,40%,100%{transform:rotate(0)}5%{transform:rotate(1.6deg)}10%{transform:rotate(-1.6deg)}15%{transform:rotate(1.1deg)}20%{transform:rotate(-1.1deg)}25%{transform:rotate(.5deg)}}@@@ -116,7 +116,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d.line-act{margin-left:auto;font-size:13px;color:var(--text-secondary)}.line-work{position:relative;display:grid;place-items:center;height:110px;margin:0 10px 10px;border-radius:12px;background:var(--bg-secondary);color:var(--text-secondary)}.line-work.covered::after{content:"Building\u2026";position:absolute;inset:0;display:grid;place-items:center;border-radius:12px;background:color-mix(in srgb,var(--bg-primary) 88%,transparent);backdrop-filter:blur(4px);color:var(--text-primary);font-weight:700}--@media (prefers-reduced-motion:no-preference){.line-demo[data-state=running] i{animation:line-live 1.8s ease-out infinite}}++.line-demo[data-state=running] i{animation:line-live 1.8s ease-out infinite}@keyframes line-live{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 60%,transparent)}70%,100%{box-shadow:0 0 0 6px transparent}}.even-demo{display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:8px;padding:10px}.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}@@@ -110,6 +110,22 @@own.forEach(function (c) { if (c.hasAttribute('data-mx-hid')) { c.style.display = c.getAttribute('data-mx-hid'); c.removeAttribute('data-mx-hid'); } });return null;}++ // A class is set only when it changes (this page watches these elements; an unchanged write would call it back).++ function flag(el, cls, on) { if (el.classList.contains(cls) !== !!on) el.classList.toggle(cls, !!on); }++ // The close buttons the menu button becomes (the owner, 11-10: "it should change the close button and on the++ // other… then I can choose which one I like most"): the menu's own and the Dock's wear the pick, open, over++ // their own drawing, which stays in place unseen (the Dock measures it to start on the bar's button).++ function wearClose(src, code) {++ all('.ovs-orb, .ovdk-orbx').forEach(function (host) {++ var mine = host.querySelector(':scope > .mx-navmenu');++ if (!src) { if (mine) mine.parentNode.removeChild(mine); flag(host, 'mx-skinned', false); return; }++ if (!mine) { mine = document.createElement('span'); mine.className = 'mx-navmenu mb-btn'; host.appendChild(mine); }++ if (mine.getAttribute('data-code') !== code) { mine.innerHTML = src.outerHTML; mine.setAttribute('data-code', code); }++ flag(host, 'mx-skinned', true);++ var dock = host.closest('.ovdk');++ flag(mine, 'is-open', dock ? !dock.hidden && !host.classList.contains('is-from') : host.classList.contains('is-x'));++ });++ }function navPreview() {var bar = document.querySelector('.openvibe-navbar');var brand = bar && bar.querySelector('.openvibe-navbar-brand');@@@ -125,18 +141,24 @@} : null);if (mark) mark.firstChild.setAttribute('href', '#' + navRef);}-- var orb = bar.querySelector('.ovnav-orb');++ // Every menu button the bar has wears the pick (a phone shows the one at the bar's right end, a computer the++ // one by the name; the owner, 11-10: "when you select an icon, it sets that as the icon in your nav bar++ // actively"), open when its menu is. So do the two close buttons that button becomes (wearClose).++ var orbs = all('.ovnav-orb', bar), orb = orbs[0] || null;var src = menu && document.getElementById(menu) && document.getElementById(menu).querySelector('.mb-nav .mb-btn svg');-- if (orb) {-- var box = swapIn(orb, 'mx-navmenu', src ? function () { var b = document.createElement('span'); b.className = 'mx-navmenu mb-btn'; return b; } : null);-- if (box && box.getAttribute('data-code') !== menu) { box.innerHTML = src.outerHTML; box.setAttribute('data-code', menu); }-- }++ orbs.forEach(function (o) {++ var box = swapIn(o, 'mx-navmenu', src ? function () { var b = document.createElement('span'); b.className = 'mx-navmenu mb-btn'; return b; } : null);++ if (!box) return;++ if (box.getAttribute('data-code') !== menu) { box.innerHTML = src.outerHTML; box.setAttribute('data-code', menu); }++ flag(box, 'is-open', o.getAttribute('aria-expanded') === 'true' || o.classList.contains('is-dock'));++ });++ wearClose(src, menu);var name = brand.querySelector('.name');var word = font && document.getElementById(font) && document.getElementById(font).querySelector('.fx-word');if (name) name.style.fontFamily = word && word.style.fontFamily ? word.style.fontFamily : '';// A short glow on what changed, so the eye goes to the bar.var now = [navRef, menu, font].join('|');-- if (navSeen && now !== navSeen) { brand.classList.remove('mx-nav-flash'); if (orb) orb.classList.remove('mx-nav-flash'); void brand.offsetWidth; brand.classList.add('mx-nav-flash'); if (orb && menu) orb.classList.add('mx-nav-flash'); }++ if (navSeen && now !== navSeen) { brand.classList.remove('mx-nav-flash'); orbs.forEach(function (o) { o.classList.remove('mx-nav-flash'); }); void brand.offsetWidth; brand.classList.add('mx-nav-flash'); if (menu) orbs.forEach(function (o) { o.classList.add('mx-nav-flash'); }); }navSeen = now;}(function watchNav() {@@@ -146,6 +168,16 @@if (!bar) { if (++tries < 40) setTimeout(hook, 250); return; }navPreview();if (obs || !window.MutationObserver) return;++ // The menu and the Dock are built when first opened, and their buttons change as they open and close: the++ // pick follows them, once a frame at most.++ var due = 0, WATCH = '.ovnav-orb, .ovs-orb, .ovdk-orbx, .ovdk';++ new MutationObserver(function (ms) {++ if (due || !lastPick('M')) return;++ for (var i = 0; i < ms.length; i++) {++ var m = ms[i], hit = m.type === 'attributes' ? (m.target.matches && m.target.matches(WATCH)) : Array.prototype.some.call(m.addedNodes, function (n) { return n.nodeType === 1 && (n.matches(WATCH) || n.querySelector(WATCH)); });++ if (hit) { due = requestAnimationFrame(function () { due = 0; navPreview(); }); return; }++ }++ }).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['class', 'hidden', 'aria-expanded'] });// The navbar redraws its brand on sign-in and account changes: put the picks back when it does.obs = new MutationObserver(function () {if ((navRef && !bar.querySelector('.mx-navmark')) || (lastPick('M') && !bar.querySelector('.mx-navmenu'))) navPreview();@@@ -467,9 +499,13 @@var mb = e.target.closest('[data-mb]');if (mb) {var open = !mb.classList.contains('is-open');-- mb.classList.toggle('is-open', open);-- mb.setAttribute('aria-pressed', open ? 'true' : 'false');-- if (!open) play(mb);++ // One press opens it everywhere on its card: the big one (with the menu it opens behind it) and both bars.++ var card = mb.closest('.mb-card');++ (card ? all('[data-mb]', card) : [mb]).forEach(function (b) {++ b.classList.toggle('is-open', open);++ b.setAttribute('aria-pressed', open ? 'true' : 'false');++ if (!open) play(b);++ });return;}var p = e.target.closest('[data-pick]');@@@ -35,6 +35,10 @@ const RULES = Object.freeze(['There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.','2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',[{ label: 'Icons', href: '/icons' }]),++ R('opens-it-closes-it', 'navigation', 'The button that opens it closes it.',++ 'A panel that covers the screen grows out of the button that opened it, and that button, in the same place, is its close button: the menu button turns into the close mark and back, and the panel folds back into it. The panel carries no second close button of its own; a different one somewhere else is a second answer to one question.',++ '2026-11-10', 'A phone\u2019s Dock slid up from the bottom carrying its own close button, while the menu button it belongs to sat untouched in the bar.',++ [{ label: 'The menu button, opening', href: '/brand/explore#menu' }, { label: 'Motion', href: '/motion' }], ['openvibe-shared test: menu-home-screen-chrome']),R('calm-reduces-off-removes', 'honest', 'Calm makes motion smaller and slower. Only Off removes it.','Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.','2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',@@@ -10,7 +10,8 @@** L01–L72 the Loop: a ring opened at the top with the V folded into it* P01–P60 the Pair: a ring O and a V side by side, the ground showing where they cross-- * M01–M50 the menu button: search and every OpenVibe site, beside the name on every site++ * M01–M61 the menu button: search and every OpenVibe site, beside the name on every site (M51–M61, 2026-11-10:++ * drawn from the logo, and the menu in miniature; shown first)** Each tab is sorted into rows by idea (proportion, shape, construction, colour, app icon…), so a row shows what* changes. Every mark is drawn large, beside a site's name (OpenVibe.Live, .Video, .Pics…, in a font the reader picks),@@@ -584,6 +585,26 @@ const MENU_ROWS = [['Tiles in a circle', 'four tiles in a ring', 'bloom', '', ring(12, 12, 10, ' mb-faint', 0, 1.6) + tiles(2, 4.4, 1.6, 1.2, 1)],['Chevron in a circle', 'a V pointing down; open, it flips', 'nod', 'flip', ring(12, 12, 8.8, ' mb-faint', 0, 1.8) + `<g class="mb-g">${line('M8.2 10.4 12 14.2 15.8 10.4', ' mb-sa', 1, 2.4)}</g>`],] },++ // ── Eleven more (the owner, 2026-11-10: "generate more icons for the menu, considering what the menu opens… and++ // considering it'll turn into a close button… and OpenVibe in general, try and theme it better"). Two families made++ // for this button and no other: drawn from the logo, so the × is already in them (a V is the top half of an ×);++ // and the menu itself in miniature (search on top, your apps, you in the middle). They are appended, so M01–M50++ // keep their numbers and the votes cast for them; `lead` shows these two rows first.++ { title: 'From the logo', line: 'the O and the V as a button; most open into an × of their own', lead: true, items: [++ ['V in the O', 'the mark in a ring; open, the V grows through into an ×', 'pop', 'grow', ring(12, 12, 9.4, ' mb-faint', 0, 1.9) + line('M7.5 7.5L16.5 16.5', ' mb-h', 1, 2.5) + line('M16.5 7.5L7.5 16.5', ' mb-h', 2, 2.5)],++ ['The V and its dot', 'a V with the O as a lit dot; open, the dot rises to the middle of an ×', 'nod', 'growdot', line('M6.4 6.4L17.6 17.6', ' mb-h', 0, 2.8) + line('M17.6 6.4L6.4 17.6', ' mb-h', 1, 2.8) + dot(12, 18.4, 2.3, ' mb-a mb-o', 2)],++ ['Two Vs', 'a V over its reflection; open, they meet point to point', 'apart', 'meet', line('M6.8 4.4L12 9.6L17.2 4.4', ' mb-sa mb-u', 0, 2.6) + line('M6.8 19.6L12 14.4L17.2 19.6', ' mb-d', 1, 2.6)],++ ['Spark', 'the spark you ask Vibe with; open, an ×', 'twinkle', '', fill(spark(12, 12.4, 8.8), '', 0) + fill(spark(19, 5, 2.9), ' mb-a', 1)],++ ['Orbit', 'you, and something circling you', 'orbit', '', ring(12, 12, 8.4, ' mb-faint', 0, 1.7) + dot(12, 12, 2.4, '', 0) + `<g class="mb-orb">${dot(...polar(8.4, -45), 2.3, ' mb-a', 1)}</g>`],++ ['Today, in the logo\'s colours', 'the nine dots, cyan into blue into violet', 'ripple', 'diamond', `<g class="mb-g">${[[12, 12, 0], [12, 5, 1], [19, 12, 1], [12, 19, 1], [5, 12, 1], [5, 5, 2], [19, 5, 2], [19, 19, 2], [5, 19, 2]].map(([x, y, n]) => dot(x, y, n ? 2.15 : 2.5, ` mb-r${n} ${x < 12 ? 'mb-c1' : x > 12 ? 'mb-c3' : 'mb-a'}`, n)).join('')}</g>`],++ ] },++ { title: 'What it opens', line: 'the menu itself, small: search, your apps, you', lead: true, items: [++ ['Search and apps', 'the search bar over three apps', 'wave', '', frame(4, 5.2, 16, 5.6, 2.8, '', 0, 1.9) + dot(7.4, 8, 1.15, ' mb-a', 0) + box(4.2, 14.6, 4, 4, 1.3, '', 1) + box(10, 14.6, 4, 4, 1.3, ' mb-a', 2) + box(15.8, 14.6, 4, 4, 1.3, '', 3)],++ ['You and your apps', 'you in the middle, eight apps round you; open, an × of dots', 'spin', 'gather', `<g class="mb-g">${dot(12, 12, 2.7, ' mb-a mb-r0', 0)}${[0, 1, 2, 3, 4, 5, 6, 7].map((k) => dot(...polar(8, k * 45), 1.45, k % 2 ? ' mb-r2' : ' mb-r1', 1 + (k % 2))).join('')}</g>`],++ ['The V in the grid', 'the launcher\'s nine dots with a V lit in them; open, an × of dots', 'ripple', 'vgrid', `<g class="mb-g">${[[12, 12, 0, 1], [12, 5, 1, 0], [19, 12, 1, 0], [12, 19, 1, 0], [5, 12, 1, 0], [5, 5, 2, 1], [19, 5, 2, 1], [19, 19, 2, 0], [5, 19, 2, 0]].map(([x, y, n, lit]) => dot(x, y, n ? 2.1 : 2.4, ` mb-r${n}${lit ? ' mb-a' : ' mb-soft'}`, n)).join('')}</g>`],++ ['Petals', 'four petals round a lit centre; open, they turn into an ×', 'bloom', 'plus', `<g class="mb-g">${box(10.4, 3, 3.2, 7.2, 1.6, '', 1)}${box(13.8, 10.4, 7.2, 3.2, 1.6, '', 2)}${box(10.4, 13.8, 3.2, 7.2, 1.6, '', 3)}${box(3, 10.4, 7.2, 3.2, 1.6, '', 4)}${dot(12, 12, 1.5, ' mb-a', 0)}</g>`],++ ['Home screen', 'six apps and a dock, as on a phone', 'bloom', '', [0, 1].map((r) => [0, 1, 2].map((c) => box(4.2 + c * 5.8, 4 + r * 5.8, 4, 4, 1.3, r === 0 && c === 2 ? ' mb-a' : '', r + c)).join('')).join('') + box(5.2, 16.8, 13.6, 2.8, 1.4, ' mb-soft', 3)],++ ] },];// ── The page ─────────────────────────────────────────────────────────────────────────────────────────────────────@@@ -645,11 +666,14 @@ function markCard(code, kind, [name, line_, spec, lock, motion], i) {}const menuSvg = (anim, own, parts, size) => `<svg viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true" data-anim="${anim}"${own ? ` data-own="${own}"` : ''}><g class="mb-i">${parts}</g>${own ? '' : '<g class="mb-x"><path d="M7 7L17 17M17 7L7 17"/></g>'}</svg>`;++// What the button opens, behind it on the stage: the menu in small (its search bar, its three tabs, your apps). Pressed,++// the button goes to the menu's top right and is its close button (marks.css .mb-peek); it is a picture, not a control.++const PEEK = `<span class="mb-peek" aria-hidden="true"><i class="mb-peek-s"></i><span class="mb-peek-t">${[0, 1, 2].map((k) => `<i style="--i:${k + 1}"></i>`).join('')}</span><span class="mb-peek-g">${[212, 268, 28, 205, 152, 318, 48, 172].map((hue, k) => `<i style="--i:${k + 3};--h:${hue}"></i>`).join('')}</span></span>`;function menuCard(code, [name, line_, anim, own, parts], i) {const bar = (tone) => `<div class="mb-nav mb-${tone}">${useOf('art-pair-today', 22)}${word()}<button type="button" class="mb-btn" data-mb aria-pressed="false" aria-label="${code} at its real size: press to open">${menuSvg(anim, own, parts, 22)}</button></div>`;return `<li class="mx-card mb-card" id="${code}" data-code="${code}" style="${delay(i)}"><button type="button" class="mx-pick" data-pick="${code}" aria-pressed="false" aria-label="Pick ${code}"></button>--<div class="mx-stage"><button type="button" class="mb-btn mb-big" data-mb aria-pressed="false" aria-label="${code} ${esc(name)}: press to open it">${menuSvg(anim, own, parts, 84)}</button></div>++<div class="mx-stage">${PEEK}<button type="button" class="mb-btn mb-big" data-mb aria-pressed="false" aria-label="${code} ${esc(name)}: press to open it">${menuSvg(anim, own, parts, 84)}</button></div><div class="mb-bars">${bar('dark')}${bar('light')}</div><p class="mx-name"><b>${code}</b> ${esc(name)}</p><p class="mx-line">${esc(line_)}</p>@@@ -696,11 +720,13 @@ function fontCard(code, [name, line_, stack, weight], i) {const pad = (n) => String(n).padStart(2, '0');function panel(id, kind, rows, label, hidden) {let n = 0;-- const body = rows.map((row) => {++ // A row marked `lead` is shown first; its numbers still come from its place in the list, so no number ever moves.++ const sections = rows.map((row) => {const first = n + 1;const cards = row.items.map((item) => { n += 1; const code = `${kind}${pad(n)}`; return kind === 'M' ? menuCard(code, item, n - 1) : kind === 'F' ? fontCard(code, item, n - 1) : markCard(code, kind, item, n - 1); }).join('');return `<section class="mx-row"><h3>${esc(row.title)} <small>${kind}${pad(first)}–${kind}${pad(n)} · ${row.items.length}</small><span class="mx-swipe" aria-hidden="true">swipe →</span></h3><p class="mx-row-line">${esc(row.line)}</p><ul class="mx-grid">${cards}</ul></section>`;-- }).join('');++ });++ const body = rows.map((row, k) => [row.lead ? 0 : 1, k]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).map(([, k]) => sections[k]).join('');const NEXT = {loop: [['pair', 'See the Pair', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],pair: [['loop', 'See the Loop', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],@@@ -715,7 +741,7 @@ function panel(id, kind, rows, label, hidden) {loop: [1, 'Pick a logo: the Loop', 'Tap the ones you like. Look at the Pair too.'],pair: [1, 'Pick a logo: the Pair', 'Tap the ones you like. Look at the Loop too.'],wild: [1, 'Pick a logo: the Wildcards', 'The O and the V, forty other ways. Each moves, shows a status, and wears the name its own way.'],-- menu: [2, 'Pick a menu button', 'Tap one to see it open.'],++ menu: [2, 'Pick a menu button', 'Tap one: it opens the menu and becomes its close button.'],font: [3, 'Pick a font', 'Tap the one that looks right.'],}[id];const head = `<div class="mx-stephead"><span class="mx-stepn mx-stepn-lg" aria-hidden="true">${STEP[0]}</span><div><b>Step ${STEP[0]} of 3 · ${esc(STEP[1])}</b><small>${esc(STEP[2])}</small></div></div>`;@@@ -798,7 +824,7 @@ ${wild ? `<span class="mx-or" aria-hidden="true">+</span>${tab('wild', 'The Wildcards', 'OV, forty other ways', `<svg viewBox="0 0 100 100" width="58" height="58" aria-hidden="true"><defs>${wildEx.defs}</defs>${wildEx.body}</svg>`, `W01–W${pad(WN)} · ${WN} ideas`)}` : ''}</div><p class="mx-q" role="presentation"><span class="mx-qn">2</span> <b>Pick a menu button</b> <small>it sits next to the logo</small></p>--${tab('menu', 'The menu button', 'The button next to the logo', `<span class="mb-btn mb-tabbtn">${menuSvg(m1[2], m1[3], m1[4], 30)}</span>`, 'M01–M50', false, true)}++${tab('menu', 'The menu button', 'The button next to the logo', `<span class="mb-btn mb-tabbtn">${menuSvg(m1[2], m1[3], m1[4], 30)}</span>`, `M01–M${pad(MENU_ROWS.reduce((t, r) => t + r.items.length, 0))}`, false, true)}<p class="mx-q" role="presentation"><span class="mx-qn">3</span> <b>Pick a font</b> <small>for the name</small></p>${tab('font', 'The font', 'The letters of the name', '<span class="fx-tabart" aria-hidden="true">Aa</span>', `F01–F${pad(FONTS.length)}`, false, true)}</nav>@@@ -817,7 +843,7 @@ ${wild ? `<a href="#wild" data-mx-go="wild" class="mx-step">${miniArt('W01', 20,${panel('loop', 'L', LOOP_ROWS, `The Loop: ${LOOPS.length} takes`, false)}${panel('pair', 'P', PAIR_ROWS, `The Pair: ${PAIRS.length} takes`, true)}${wild ? panel('wild', 'W', WILD_ROWS, `The Wildcards: ${WN} ideas`, true) : ''}--${panel('menu', 'M', MENU_ROWS, 'The menu button: fifty ideas', true)}++${panel('menu', 'M', MENU_ROWS, `The menu button: ${MENU_ROWS.reduce((t, r) => t + r.items.length, 0)} ideas`, true)}${panel('font', 'F', FONT_ROWS, 'The font: twelve faces', true)}<div class="mx-nudge" data-mx-nudge role="status" hidden><span data-mx-nudge-text></span><a href="#menu" data-mx-go="menu" class="mx-btn" data-mx-nudge-go>Go to step 2 →</a><button type="button" class="mx-btn mx-btn-quiet" data-mx-nudge-close aria-label="Close">✕</button></div><p class="mx-fine">Nothing changes on the sites until a logo wins. <a href="/brand">Today’s logo</a>.</p>@@@ -12,7 +12,7 @@ const explore = require('../server/views/marks-explore');(async () => {await check('fifty distinct takes in each family: no two share their whole recipe', () => {for (const [name, list] of [['Loop', explore.LOOPS], ['Pair', explore.PAIRS], ['menu', explore.MENUS]]) {-- const want = { Loop: 72, Pair: 60, menu: 50 }[name];++ const want = { Loop: 72, Pair: 60, menu: 61 }[name];assert.strictEqual(list.length, want, `${name}: ${want}`);const recipes = list.map((x) => JSON.stringify(x.slice(2)));assert.strictEqual(new Set(recipes).size, want, `${name}: every recipe differs`);@@@ -20,15 +20,37 @@ const explore = require('../server/views/marks-explore');}});++ await check('the bar is the preview: every menu button it has wears the pick, so do the two close buttons, and it stays in view', () => {++ const fs = require('fs'), path = require('path');++ const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'marks.js'), 'utf8');++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'marks.css'), 'utf8');++ assert.ok(js.includes("var orbs = all('.ovnav-orb', bar)"), 'every menu button in the bar (a phone shows the one at its right end)');++ assert.ok(js.includes("all('.ovs-orb, .ovdk-orbx').forEach"), 'the menu\'s own close button and the Dock\'s wear it too');++ assert.ok(js.includes("flag(mine, 'is-open', dock ? !dock.hidden && !host.classList.contains('is-from') : host.classList.contains('is-x'))"), 'open when the sheet is, at rest while it is still on the bar\'s button');++ assert.ok(/\.mx-skinned > svg\{opacity:0\}/.test(css) && /\.mx-skinned > \.mx-navmenu\{all:unset;grid-area:1\/1;/.test(css), 'over their own drawing, which keeps its place (the Dock measures it)');++ assert.ok(css.includes('html:has(.mx-wrap) .openvibe-navbar.ovnav-hide{transform:none!important}'), 'on this page a phone\'s bar does not hide as you scroll');++ assert.ok(!/prefers-reduced-motion:\s*no-preference/.test(css), 'no move is kept only for people who have not asked for less motion');++ });++const t = await boot();try {await check('three tabs, a hundred and fifty numbered cards in rows, every one pickable', async () => {const r = await t.get('/brand/explore');assert.strictEqual(r.status, 200);const codes = [...r.text.matchAll(/<li class="mx-card(?: mb-card| fx-card| mx-wild)?" id="([LPMFW]\d{2})" data-code="\1"/g)].map((m) => m[1]);-- assert.strictEqual(codes.length, 194, 'the Loop, the Pair, the menu buttons and the fonts: the Wildcards are off the vote');-- assert.strictEqual(new Set(codes).size, 194, 'each number once');-- assert.deepStrictEqual(['L', 'P', 'W', 'M', 'F'].map((k) => codes.filter((c) => c[0] === k).length), [72, 60, 0, 50, 12]);++ assert.strictEqual(codes.length, 205, 'the Loop, the Pair, the menu buttons and the fonts: the Wildcards are off the vote');++ assert.strictEqual(new Set(codes).size, 205, 'each number once');++ // Eleven more menu buttons (2026-11-10), made for this button: drawn from the logo, and the menu in small. They++ // are shown first and numbered last, so M01–M50 and the votes cast for them stay where they were.++ const menus = codes.filter((c) => c[0] === 'M');++ assert.deepStrictEqual(menus.slice(0, 12), ['M51', 'M52', 'M53', 'M54', 'M55', 'M56', 'M57', 'M58', 'M59', 'M60', 'M61', 'M01'], 'the two new families lead, then the diamond');++ assert.strictEqual(explore.MENUS[7][0], 'Lit corners', 'M08 is still the idea it was');++ assert.match(r.text, /<h3>From the logo <small>M51–M56 · 6<\/small>/);++ assert.match(r.text, /<h3>What it opens <small>M57–M61 · 5<\/small>/);++ assert.match(r.text, /Tap one: it opens the menu and becomes its close button\./);++ assert.match(r.text, /<div class="mx-stage"><span class="mb-peek" aria-hidden="true">[\s\S]*?<\/span><button type="button" class="mb-btn mb-big" data-mb/, 'each stage holds the menu it opens, as a picture behind the button');++ for (const own of ['grow', 'growdot', 'meet', 'gather', 'vgrid']) assert.ok(r.text.includes(`data-own="${own}"`), `a button that opens its own way: ${own}`);++ assert.deepStrictEqual(['L', 'P', 'W', 'M', 'F'].map((k) => codes.filter((c) => c[0] === k).length), [72, 60, 0, 61, 12]);// The owner, 11-01: "I've changed my mind on them… keep them, they're cool to look at": ?wild=1 still shows them.const withWild = await t.get('/brand/explore?wild=1');assert.strictEqual((withWild.text.match(/<li class="mx-card mx-wild" id="W\d\d"/g) || []).length, 40, 'the Wildcards, to look at');
@@ -179,7 +179,6 @@
@@ -179,7 +179,6 @@
.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-s:not([stroke-dasharray]){stroke-dashoffset:100}
.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-s:not([stroke-dasharray]){stroke-dashoffset:100}
.mx-wrap.mx-live .mx-stage .mx-f{transition:opacity var(--ov-dur-4, .8s) var(--ov-ease-out, ease) calc(var(--d,0s) + .5s)}
.mx-wrap.mx-live .mx-stage .mx-f{transition:opacity var(--ov-dur-4, .8s) var(--ov-ease-out, ease) calc(var(--d,0s) + .5s)}
.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-f{opacity:0}
.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage .mx-f{opacity:0}
-@media (prefers-reduced-motion:no-preference){
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=breathe]{animation:mxBreathe 5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=breathe]{animation:mxBreathe 5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=drift]{animation:mxDrift 9s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=drift]{animation:mxDrift 9s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=rock]{animation:mxRock 6.5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=rock]{animation:mxRock 6.5s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
@@ -187,7 +186,6 @@
@@ -187,7 +186,6 @@
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=nod] .mx-b{animation:mxNod 4.2s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=nod] .mx-b{animation:mxNod 4.2s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=dot] .mx-dot{animation:mxDot 3.4s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=dot] .mx-dot{animation:mxDot 3.4s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=pulse] .mx-glow{animation:mxPulse 3.6s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
.mx-wrap.mx-live:not(.mx-still) .mx-card.is-on .mx-stage>svg[data-idle=pulse] .mx-glow{animation:mxPulse 3.6s var(--ov-ease-in-out, ease-in-out) calc(1.3s + var(--d,0s)) infinite}
-}
@keyframes mxBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}
@keyframes mxBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}
@keyframes mxDrift{0%,100%{filter:hue-rotate(0deg)}50%{filter:hue-rotate(-32deg) saturate(1.15)}}
@keyframes mxDrift{0%,100%{filter:hue-rotate(0deg)}50%{filter:hue-rotate(-32deg) saturate(1.15)}}
@keyframes mxRock{0%,100%{transform:rotate(0deg)}30%{transform:rotate(-3.5deg)}70%{transform:rotate(3.5deg)}}
@keyframes mxRock{0%,100%{transform:rotate(0deg)}30%{transform:rotate(-3.5deg)}70%{transform:rotate(3.5deg)}}
@@ -233,7 +231,47 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -233,7 +231,47 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
.mb-btn.is-open svg[data-own=burger] .mb-l2{transform:translateY(-5.5px) rotate(-45deg)}
.mb-btn.is-open svg[data-own=burger] .mb-l2{transform:translateY(-5.5px) rotate(-45deg)}
.mb-btn.is-open svg[data-own=burger2] .mb-l0{transform:translateY(3px) rotate(45deg)}
.mb-btn.is-open svg[data-own=burger2] .mb-l0{transform:translateY(3px) rotate(45deg)}
.mb-btn.is-open svg[data-own=burger2] .mb-l2{transform:translateY(-3px) rotate(-45deg)}
.mb-btn.is-open svg[data-own=burger2] .mb-l2{transform:translateY(-3px) rotate(-45deg)}
-@media (prefers-reduced-motion:no-preference){
+/* 2026-11-10, the buttons drawn from the logo and from the menu. A V is the top half of an ×: its two strokes are
+ drawn half way (mb-h) and grow through to the other side as it opens. Two Vs slide until their points meet. Four
+ petals turn an eighth. A ring of apps, and the launcher's grid with a V lit in it, drop their edge dots and
+ leave an × of dots. */
+.mb-btn .mb-h{stroke-dasharray:50 100;transition:stroke-dasharray var(--ov-dur-4, .45s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)),transform var(--ov-dur-4, .45s) var(--ov-ease-spring, cubic-bezier(.2,1.3,.3,1)),opacity var(--ov-dur-2, .25s)}
+.mb-btn.is-open svg:is([data-own=grow],[data-own=growdot]) .mb-h{stroke-dasharray:100 100}
+.mb-btn.is-open svg[data-own=growdot] .mb-o{transform:translateY(-6.4px) scale(.84)}
+.mb-btn.is-open svg[data-own=meet] .mb-u{transform:translateY(2.4px)}
+.mb-btn.is-open svg[data-own=meet] .mb-d{transform:translateY(-2.4px)}
+.mb-btn.is-open svg:is([data-own=gather],[data-own=vgrid]) .mb-r1{opacity:0;transform:scale(0)}
+.mb-btn.is-open svg[data-own=gather] .mb-r2{transform:scale(1.75)}
+.mb-btn.is-open svg[data-own=vgrid] .mb-r2{fill:var(--mb-a);opacity:1;transform:scale(1.14)}
+.mb-btn .mb-c1{fill:var(--mb-c1,#22d3ee)}
+.mb-btn .mb-c3{fill:var(--mb-c3,#a78bfa)}
+.mb-light .mb-btn{--mb-c1:#0891b2;--mb-c3:#7c3aed}
+.mb-btn .mb-orb{transform-box:view-box;transform-origin:12px 12px}
+/* Pressed, a ring leaves the button (once). */
+.mb-btn::after{content:"";position:absolute;inset:0;border-radius:50%;border:2px solid var(--mb-a);opacity:0;pointer-events:none}
+.mb-btn.is-open::after{animation:mbBurst var(--ov-dur-4, .55s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)) both}
+@keyframes mbBurst{from{opacity:.75;transform:scale(.35)}to{opacity:0;transform:scale(1.85)}}
+/* What it opens, on the stage behind the button: the menu in small (its search bar, its three tabs, your apps), grown
+ out of the button. The button itself goes to the menu's top right and is its close button: the one button the whole
+ way (the rule "the button that opens it closes it"). A picture, not a control. */
+.mb-card .mx-stage{position:relative;overflow:hidden;container-type:size}
+.mb-card .mb-big{position:relative;z-index:2;transition:transform var(--ov-dur-4, .5s) var(--ov-ease-spring, cubic-bezier(.2,1.15,.3,1))}
+/* Beside the card's own + (which keeps the corner), on the search bar's line. */
+.mb-card .mx-stage:has(.mb-big.is-open) .mb-big{transform:translate(calc(50cqw - 72px),calc(-50cqh + 24px)) scale(.4)}
+.mb-peek{position:absolute;inset:0;z-index:1;display:grid;grid-template-rows:auto auto 1fr;gap:8px;padding:12px;box-sizing:border-box;pointer-events:none;opacity:0;
+ clip-path:circle(0 at 50% 50%);transition:clip-path var(--ov-dur-4, .5s) var(--ov-ease-out, cubic-bezier(.2,.8,.2,1)),opacity var(--ov-dur-2, .25s);
+ background:linear-gradient(180deg,color-mix(in srgb,#3b82f6 10%,#0d1322),#0b1020)}
+.mx-stage:has(.mb-big.is-open) .mb-peek{opacity:1;clip-path:circle(150% at 50% 50%)}
+.mb-peek i{display:block;opacity:0;transform:translateY(calc(8px * var(--ov-move, 1))) scale(.94);transition:opacity var(--ov-dur-3, .3s),transform var(--ov-dur-4, .45s) var(--ov-ease-spring, cubic-bezier(.2,1.2,.3,1));transition-delay:0s}
+.mx-stage:has(.mb-big.is-open) .mb-peek i{opacity:1;transform:none;transition-delay:calc(90ms + var(--i, 0) * 32ms)}
+.mb-peek-s{height:24px;margin-right:88px;border-radius:999px;background:color-mix(in srgb,#e8edf3 7%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,#e8edf3 14%,transparent);position:relative}
+.mb-peek-s::before{content:"";position:absolute;left:9px;top:50%;width:8px;height:8px;margin-top:-5px;border-radius:50%;border:1.6px solid #7aa2ff}
+.mb-peek-t{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
+.mb-peek-t i{height:16px;border-radius:8px;background:color-mix(in srgb,#e8edf3 8%,transparent)}
+.mb-peek-t i:last-child{background:color-mix(in srgb,#3b82f6 34%,#131c2e)}
+.mb-peek-g{display:grid;grid-template-columns:repeat(4,1fr);gap:8px 10px;align-content:start;justify-items:center}
+.mb-peek-g i{width:min(100%,34px);aspect-ratio:1;border-radius:28%;background:linear-gradient(150deg,hsl(var(--h) 72% 58% / .5),hsl(var(--h) 70% 38% / .28));box-shadow:inset 0 0 0 1px hsl(var(--h) 70% 64% / .32)}
+html[data-ov-motion=off] .mb-peek,html[data-ov-motion=off] .mb-peek i{transition:none}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=ripple] .mb-p{animation:mbRipple var(--ov-dur-4, .75s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .08s)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=ripple] .mb-p{animation:mbRipple var(--ov-dur-4, .75s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .08s)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=wave] .mb-p{animation:mbWave var(--ov-dur-4, .7s) var(--ov-ease-in-out, ease-in-out) calc(var(--i,0) * .07s)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=wave] .mb-p{animation:mbWave var(--ov-dur-4, .7s) var(--ov-ease-in-out, ease-in-out) calc(var(--i,0) * .07s)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=bloom] .mb-p{animation:mbBloom var(--ov-dur-4, .7s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .06s)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=bloom] .mb-p{animation:mbBloom var(--ov-dur-4, .7s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1)) calc(var(--i,0) * .06s)}
@@ -246,7 +284,10 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -246,7 +284,10 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg:is([data-anim=pop],[data-anim=pulse]) .mb-i{animation:mbPop var(--ov-dur-4, .6s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1))}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg:is([data-anim=pop],[data-anim=pulse]) .mb-i{animation:mbPop var(--ov-dur-4, .6s) var(--ov-ease-out, cubic-bezier(.3,.7,.3,1))}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=tilt] .mb-i{animation:mbTilt var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=tilt] .mb-i{animation:mbTilt var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=nod] .mb-i{animation:mbNod var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}
.mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=nod] .mb-i{animation:mbNod var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}
-}
+ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=apart] .mb-u{animation:mbApart var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out)}
+ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=apart] .mb-d{animation:mbApart var(--ov-dur-4, .8s) var(--ov-ease-in-out, ease-in-out) reverse}
+ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=orbit] .mb-orb{animation:mbOrbit 1.2s var(--ov-ease-in-out, cubic-bezier(.65,0,.35,1))}
+ .mb-btn:not(.is-open):is(:hover,:focus-visible,.is-play) svg[data-anim=spin] .mb-g{animation:mbSpin .9s var(--ov-ease-in-out, cubic-bezier(.65,0,.35,1))}
@keyframes mbRipple{0%,100%{transform:scale(1)}35%{transform:scale(.5)}70%{transform:scale(1.2)}}
@keyframes mbRipple{0%,100%{transform:scale(1)}35%{transform:scale(.5)}70%{transform:scale(1.2)}}
@keyframes mbWave{0%,100%{transform:translateY(0)}40%{transform:translateY(-2.6px)}}
@keyframes mbWave{0%,100%{transform:translateY(0)}40%{transform:translateY(-2.6px)}}
@keyframes mbBloom{0%,100%{transform:scale(1)}45%{transform:scale(1.24)}}
@keyframes mbBloom{0%,100%{transform:scale(1)}45%{transform:scale(1.24)}}
@@ -259,6 +300,9 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -259,6 +300,9 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}
@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}
@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}
@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}
@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}
@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}
+@keyframes mbApart{0%,100%{transform:translateY(0)}45%{transform:translateY(-1.6px)}}
+@keyframes mbOrbit{50%{transform:rotate(200deg)}}
+@keyframes mbSpin{to{transform:rotate(90deg)}}
.mb-card .mx-stage{color:#e8edf3}
.mb-card .mx-stage{color:#e8edf3}
.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}
.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}
.mb-big::before{inset:8px}
.mb-big::before{inset:8px}
@@ -410,6 +454,16 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -410,6 +454,16 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
.openvibe-navbar .ovnav-orb .mx-navmenu{all:unset;position:relative;display:grid;place-items:center;width:24px;height:24px;color:inherit}
.openvibe-navbar .ovnav-orb .mx-navmenu{all:unset;position:relative;display:grid;place-items:center;width:24px;height:24px;color:inherit}
.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}
.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}
.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}
.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}
+/* The two close buttons the picked menu button becomes (the menu's own and the Dock's) wear it too, open. Their own
+ drawing stays in place, unseen: the Dock measures it to start on the bar's button. */
+.mx-skinned > *{grid-area:1/1}
+.mx-skinned > svg{opacity:0}
+.mx-skinned > .mx-navmenu{all:unset;grid-area:1/1;display:grid;place-items:center;width:24px;height:24px;color:inherit;pointer-events:none}
+.mx-skinned > .mx-navmenu svg{width:22px;height:22px}
+/* On this page the bar is the preview, so it stays in view (the owner, 11-10: "the nav bar will just go away… while you
+ scroll… disable that on that page"). Everywhere else a phone's bar still hides as you scroll down. */
+html:has(.mx-wrap) .openvibe-navbar.ovnav-hide{transform:none!important}
+html:has(.mx-wrap) #navbar-mount:has(> .ovnav-hide){pointer-events:auto}
@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}
@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}
/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */
/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */
@@ -477,7 +531,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -477,7 +531,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
/* Each wildcard moves by its kind of motion (data-wild-motion): only while its card is on screen, with long rests,
/* Each wildcard moves by its kind of motion (data-wild-motion): only while its card is on screen, with long rests,
and never under reduced motion. The parts that move are w-a, w-b and w-c. */
and never under reduced motion. The parts that move are w-a, w-b and w-c. */
.mx-wild svg :is(.w-a,.w-b,.w-c){transform-box:fill-box;transform-origin:center}
.mx-wild svg :is(.w-a,.w-b,.w-c){transform-box:fill-box;transform-origin:center}
-@media (prefers-reduced-motion:no-preference){
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion] :is(.w-a,.w-b,.w-c){animation-duration:3.6s;animation-iteration-count:infinite;animation-timing-function:var(--ov-ease-in-out, ease-in-out);animation-delay:calc(var(--d,0s) + var(--i,0) * .09s)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion] :is(.w-a,.w-b,.w-c){animation-duration:3.6s;animation-iteration-count:infinite;animation-timing-function:var(--ov-ease-in-out, ease-in-out);animation-delay:calc(var(--d,0s) + var(--i,0) * .09s)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=draw] .w-a{stroke-dasharray:100;animation-name:wDraw;animation-duration:3.4s}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=draw] .w-a{stroke-dasharray:100;animation-name:wDraw;animation-duration:3.4s}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=join] .w-a{animation-name:wJoinL}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=join] .w-a{animation-name:wJoinL}
@@ -508,7 +561,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
@@ -508,7 +561,6 @@ html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;trans
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-a{animation-name:wGlitchL;animation-duration:2.8s;animation-timing-function:steps(1)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-a{animation-name:wGlitchL;animation-duration:2.8s;animation-timing-function:steps(1)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-b{animation-name:wGlitchR;animation-duration:2.8s;animation-timing-function:steps(1)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=glitch] .w-b{animation-name:wGlitchR;animation-duration:2.8s;animation-timing-function:steps(1)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=caret] .w-a{animation-name:wCaret;animation-duration:1.1s;animation-timing-function:steps(1)}
.mx-wrap.mx-live:not(.mx-still) :is(.mx-card.is-on,.st) svg[data-wild-motion=caret] .w-a{animation-name:wCaret;animation-duration:1.1s;animation-timing-function:steps(1)}
-}
@keyframes wBar{0%,100%{transform:scaleY(1)}30%{transform:scaleY(.55)}60%{transform:scaleY(1.08)}}
@keyframes wBar{0%,100%{transform:scaleY(1)}30%{transform:scaleY(.55)}60%{transform:scaleY(1.08)}}
@keyframes wRipple{0%,60%,100%{opacity:1;transform:scale(1)}25%{opacity:.2;transform:scale(1.08)}}
@keyframes wRipple{0%,60%,100%{opacity:1;transform:scale(1)}25%{opacity:.2;transform:scale(1.08)}}
@keyframes wHum{0%,40%,100%{transform:rotate(0)}5%{transform:rotate(1.6deg)}10%{transform:rotate(-1.6deg)}15%{transform:rotate(1.1deg)}20%{transform:rotate(-1.1deg)}25%{transform:rotate(.5deg)}}
@keyframes wHum{0%,40%,100%{transform:rotate(0)}5%{transform:rotate(1.6deg)}10%{transform:rotate(-1.6deg)}15%{transform:rotate(1.1deg)}20%{transform:rotate(-1.1deg)}25%{transform:rotate(.5deg)}}
@@ -116,7 +116,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d
@@ -116,7 +116,7 @@ html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .d
.line-act{margin-left:auto;font-size:13px;color:var(--text-secondary)}
.line-act{margin-left:auto;font-size:13px;color:var(--text-secondary)}
.line-work{position:relative;display:grid;place-items:center;height:110px;margin:0 10px 10px;border-radius:12px;background:var(--bg-secondary);color:var(--text-secondary)}
.line-work{position:relative;display:grid;place-items:center;height:110px;margin:0 10px 10px;border-radius:12px;background:var(--bg-secondary);color:var(--text-secondary)}
.line-work.covered::after{content:"Building\u2026";position:absolute;inset:0;display:grid;place-items:center;border-radius:12px;background:color-mix(in srgb,var(--bg-primary) 88%,transparent);backdrop-filter:blur(4px);color:var(--text-primary);font-weight:700}
.line-work.covered::after{content:"Building\u2026";position:absolute;inset:0;display:grid;place-items:center;border-radius:12px;background:color-mix(in srgb,var(--bg-primary) 88%,transparent);backdrop-filter:blur(4px);color:var(--text-primary);font-weight:700}
-@media (prefers-reduced-motion:no-preference){.line-demo[data-state=running] i{animation:line-live 1.8s ease-out infinite}}
+.line-demo[data-state=running] i{animation:line-live 1.8s ease-out infinite}
@keyframes line-live{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 60%,transparent)}70%,100%{box-shadow:0 0 0 6px transparent}}
@keyframes line-live{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 60%,transparent)}70%,100%{box-shadow:0 0 0 6px transparent}}
.even-demo{display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:8px;padding:10px}
.even-demo{display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:8px;padding:10px}
.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}
.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}
@@ -110,6 +110,22 @@
@@ -110,6 +110,22 @@
own.forEach(function (c) { if (c.hasAttribute('data-mx-hid')) { c.style.display = c.getAttribute('data-mx-hid'); c.removeAttribute('data-mx-hid'); } });
own.forEach(function (c) { if (c.hasAttribute('data-mx-hid')) { c.style.display = c.getAttribute('data-mx-hid'); c.removeAttribute('data-mx-hid'); } });
return null;
return null;
}
}
+ // A class is set only when it changes (this page watches these elements; an unchanged write would call it back).
+ function flag(el, cls, on) { if (el.classList.contains(cls) !== !!on) el.classList.toggle(cls, !!on); }
+ // The close buttons the menu button becomes (the owner, 11-10: "it should change the close button and on the
+ // other… then I can choose which one I like most"): the menu's own and the Dock's wear the pick, open, over
+ // their own drawing, which stays in place unseen (the Dock measures it to start on the bar's button).
+ function wearClose(src, code) {
+ all('.ovs-orb, .ovdk-orbx').forEach(function (host) {
+ var mine = host.querySelector(':scope > .mx-navmenu');
+ if (!src) { if (mine) mine.parentNode.removeChild(mine); flag(host, 'mx-skinned', false); return; }
+ if (!mine) { mine = document.createElement('span'); mine.className = 'mx-navmenu mb-btn'; host.appendChild(mine); }
+ if (mine.getAttribute('data-code') !== code) { mine.innerHTML = src.outerHTML; mine.setAttribute('data-code', code); }
+ flag(host, 'mx-skinned', true);
+ var dock = host.closest('.ovdk');
+ flag(mine, 'is-open', dock ? !dock.hidden && !host.classList.contains('is-from') : host.classList.contains('is-x'));
+ });
+ }
function navPreview() {
function navPreview() {
var bar = document.querySelector('.openvibe-navbar');
var bar = document.querySelector('.openvibe-navbar');
var brand = bar && bar.querySelector('.openvibe-navbar-brand');
var brand = bar && bar.querySelector('.openvibe-navbar-brand');
@@ -125,18 +141,24 @@
@@ -125,18 +141,24 @@
} : null);
} : null);
if (mark) mark.firstChild.setAttribute('href', '#' + navRef);
if (mark) mark.firstChild.setAttribute('href', '#' + navRef);
}
}
- var orb = bar.querySelector('.ovnav-orb');
+ // Every menu button the bar has wears the pick (a phone shows the one at the bar's right end, a computer the
+ // one by the name; the owner, 11-10: "when you select an icon, it sets that as the icon in your nav bar
+ // actively"), open when its menu is. So do the two close buttons that button becomes (wearClose).
+ var orbs = all('.ovnav-orb', bar), orb = orbs[0] || null;
var src = menu && document.getElementById(menu) && document.getElementById(menu).querySelector('.mb-nav .mb-btn svg');
var src = menu && document.getElementById(menu) && document.getElementById(menu).querySelector('.mb-nav .mb-btn svg');
- if (orb) {
- var box = swapIn(orb, 'mx-navmenu', src ? function () { var b = document.createElement('span'); b.className = 'mx-navmenu mb-btn'; return b; } : null);
- if (box && box.getAttribute('data-code') !== menu) { box.innerHTML = src.outerHTML; box.setAttribute('data-code', menu); }
- }
+ orbs.forEach(function (o) {
+ var box = swapIn(o, 'mx-navmenu', src ? function () { var b = document.createElement('span'); b.className = 'mx-navmenu mb-btn'; return b; } : null);
+ if (!box) return;
+ if (box.getAttribute('data-code') !== menu) { box.innerHTML = src.outerHTML; box.setAttribute('data-code', menu); }
+ flag(box, 'is-open', o.getAttribute('aria-expanded') === 'true' || o.classList.contains('is-dock'));
+ });
+ wearClose(src, menu);
var name = brand.querySelector('.name');
var name = brand.querySelector('.name');
var word = font && document.getElementById(font) && document.getElementById(font).querySelector('.fx-word');
var word = font && document.getElementById(font) && document.getElementById(font).querySelector('.fx-word');
if (name) name.style.fontFamily = word && word.style.fontFamily ? word.style.fontFamily : '';
if (name) name.style.fontFamily = word && word.style.fontFamily ? word.style.fontFamily : '';
// A short glow on what changed, so the eye goes to the bar.
// A short glow on what changed, so the eye goes to the bar.
var now = [navRef, menu, font].join('|');
var now = [navRef, menu, font].join('|');
- if (navSeen && now !== navSeen) { brand.classList.remove('mx-nav-flash'); if (orb) orb.classList.remove('mx-nav-flash'); void brand.offsetWidth; brand.classList.add('mx-nav-flash'); if (orb && menu) orb.classList.add('mx-nav-flash'); }
+ if (navSeen && now !== navSeen) { brand.classList.remove('mx-nav-flash'); orbs.forEach(function (o) { o.classList.remove('mx-nav-flash'); }); void brand.offsetWidth; brand.classList.add('mx-nav-flash'); if (menu) orbs.forEach(function (o) { o.classList.add('mx-nav-flash'); }); }
navSeen = now;
navSeen = now;
}
}
(function watchNav() {
(function watchNav() {
@@ -146,6 +168,16 @@
@@ -146,6 +168,16 @@
if (!bar) { if (++tries < 40) setTimeout(hook, 250); return; }
if (!bar) { if (++tries < 40) setTimeout(hook, 250); return; }
navPreview();
navPreview();
if (obs || !window.MutationObserver) return;
if (obs || !window.MutationObserver) return;
+ // The menu and the Dock are built when first opened, and their buttons change as they open and close: the
+ // pick follows them, once a frame at most.
+ var due = 0, WATCH = '.ovnav-orb, .ovs-orb, .ovdk-orbx, .ovdk';
+ new MutationObserver(function (ms) {
+ if (due || !lastPick('M')) return;
+ for (var i = 0; i < ms.length; i++) {
+ var m = ms[i], hit = m.type === 'attributes' ? (m.target.matches && m.target.matches(WATCH)) : Array.prototype.some.call(m.addedNodes, function (n) { return n.nodeType === 1 && (n.matches(WATCH) || n.querySelector(WATCH)); });
+ if (hit) { due = requestAnimationFrame(function () { due = 0; navPreview(); }); return; }
+ }
+ }).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['class', 'hidden', 'aria-expanded'] });
// The navbar redraws its brand on sign-in and account changes: put the picks back when it does.
// The navbar redraws its brand on sign-in and account changes: put the picks back when it does.
obs = new MutationObserver(function () {
obs = new MutationObserver(function () {
if ((navRef && !bar.querySelector('.mx-navmark')) || (lastPick('M') && !bar.querySelector('.mx-navmenu'))) navPreview();
if ((navRef && !bar.querySelector('.mx-navmark')) || (lastPick('M') && !bar.querySelector('.mx-navmenu'))) navPreview();
@@ -467,9 +499,13 @@
@@ -467,9 +499,13 @@
var mb = e.target.closest('[data-mb]');
var mb = e.target.closest('[data-mb]');
if (mb) {
if (mb) {
var open = !mb.classList.contains('is-open');
var open = !mb.classList.contains('is-open');
- mb.classList.toggle('is-open', open);
- mb.setAttribute('aria-pressed', open ? 'true' : 'false');
- if (!open) play(mb);
+ // One press opens it everywhere on its card: the big one (with the menu it opens behind it) and both bars.
+ var card = mb.closest('.mb-card');
+ (card ? all('[data-mb]', card) : [mb]).forEach(function (b) {
+ b.classList.toggle('is-open', open);
+ b.setAttribute('aria-pressed', open ? 'true' : 'false');
+ if (!open) play(b);
+ });
return;
return;
}
}
var p = e.target.closest('[data-pick]');
var p = e.target.closest('[data-pick]');
@@ -35,6 +35,10 @@ const RULES = Object.freeze([
@@ -35,6 +35,10 @@ const RULES = Object.freeze([
'There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.',
'There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.',
'2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',
'2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',
[{ label: 'Icons', href: '/icons' }]),
[{ label: 'Icons', href: '/icons' }]),
+ R('opens-it-closes-it', 'navigation', 'The button that opens it closes it.',
+ 'A panel that covers the screen grows out of the button that opened it, and that button, in the same place, is its close button: the menu button turns into the close mark and back, and the panel folds back into it. The panel carries no second close button of its own; a different one somewhere else is a second answer to one question.',
+ '2026-11-10', 'A phone\u2019s Dock slid up from the bottom carrying its own close button, while the menu button it belongs to sat untouched in the bar.',
+ [{ label: 'The menu button, opening', href: '/brand/explore#menu' }, { label: 'Motion', href: '/motion' }], ['openvibe-shared test: menu-home-screen-chrome']),
R('calm-reduces-off-removes', 'honest', 'Calm makes motion smaller and slower. Only Off removes it.',
R('calm-reduces-off-removes', 'honest', 'Calm makes motion smaller and slower. Only Off removes it.',
'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',
'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',
'2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',
'2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',
@@ -10,7 +10,8 @@
@@ -10,7 +10,8 @@
*
*
* L01–L72 the Loop: a ring opened at the top with the V folded into it
* L01–L72 the Loop: a ring opened at the top with the V folded into it
* P01–P60 the Pair: a ring O and a V side by side, the ground showing where they cross
* P01–P60 the Pair: a ring O and a V side by side, the ground showing where they cross
- * M01–M50 the menu button: search and every OpenVibe site, beside the name on every site
+ * M01–M61 the menu button: search and every OpenVibe site, beside the name on every site (M51–M61, 2026-11-10:
+ * drawn from the logo, and the menu in miniature; shown first)
*
*
* Each tab is sorted into rows by idea (proportion, shape, construction, colour, app icon…), so a row shows what
* Each tab is sorted into rows by idea (proportion, shape, construction, colour, app icon…), so a row shows what
* changes. Every mark is drawn large, beside a site's name (OpenVibe.Live, .Video, .Pics…, in a font the reader picks),
* changes. Every mark is drawn large, beside a site's name (OpenVibe.Live, .Video, .Pics…, in a font the reader picks),
@@ -584,6 +585,26 @@ const MENU_ROWS = [
@@ -584,6 +585,26 @@ const MENU_ROWS = [
['Tiles in a circle', 'four tiles in a ring', 'bloom', '', ring(12, 12, 10, ' mb-faint', 0, 1.6) + tiles(2, 4.4, 1.6, 1.2, 1)],
['Tiles in a circle', 'four tiles in a ring', 'bloom', '', ring(12, 12, 10, ' mb-faint', 0, 1.6) + tiles(2, 4.4, 1.6, 1.2, 1)],
['Chevron in a circle', 'a V pointing down; open, it flips', 'nod', 'flip', ring(12, 12, 8.8, ' mb-faint', 0, 1.8) + `<g class="mb-g">${line('M8.2 10.4 12 14.2 15.8 10.4', ' mb-sa', 1, 2.4)}</g>`],
['Chevron in a circle', 'a V pointing down; open, it flips', 'nod', 'flip', ring(12, 12, 8.8, ' mb-faint', 0, 1.8) + `<g class="mb-g">${line('M8.2 10.4 12 14.2 15.8 10.4', ' mb-sa', 1, 2.4)}</g>`],
] },
] },
+ // ── Eleven more (the owner, 2026-11-10: "generate more icons for the menu, considering what the menu opens… and
+ // considering it'll turn into a close button… and OpenVibe in general, try and theme it better"). Two families made
+ // for this button and no other: drawn from the logo, so the × is already in them (a V is the top half of an ×);
+ // and the menu itself in miniature (search on top, your apps, you in the middle). They are appended, so M01–M50
+ // keep their numbers and the votes cast for them; `lead` shows these two rows first.
+ { title: 'From the logo', line: 'the O and the V as a button; most open into an × of their own', lead: true, items: [
+ ['V in the O', 'the mark in a ring; open, the V grows through into an ×', 'pop', 'grow', ring(12, 12, 9.4, ' mb-faint', 0, 1.9) + line('M7.5 7.5L16.5 16.5', ' mb-h', 1, 2.5) + line('M16.5 7.5L7.5 16.5', ' mb-h', 2, 2.5)],
+ ['The V and its dot', 'a V with the O as a lit dot; open, the dot rises to the middle of an ×', 'nod', 'growdot', line('M6.4 6.4L17.6 17.6', ' mb-h', 0, 2.8) + line('M17.6 6.4L6.4 17.6', ' mb-h', 1, 2.8) + dot(12, 18.4, 2.3, ' mb-a mb-o', 2)],
+ ['Two Vs', 'a V over its reflection; open, they meet point to point', 'apart', 'meet', line('M6.8 4.4L12 9.6L17.2 4.4', ' mb-sa mb-u', 0, 2.6) + line('M6.8 19.6L12 14.4L17.2 19.6', ' mb-d', 1, 2.6)],
+ ['Spark', 'the spark you ask Vibe with; open, an ×', 'twinkle', '', fill(spark(12, 12.4, 8.8), '', 0) + fill(spark(19, 5, 2.9), ' mb-a', 1)],
+ ['Orbit', 'you, and something circling you', 'orbit', '', ring(12, 12, 8.4, ' mb-faint', 0, 1.7) + dot(12, 12, 2.4, '', 0) + `<g class="mb-orb">${dot(...polar(8.4, -45), 2.3, ' mb-a', 1)}</g>`],
+ ['Today, in the logo\'s colours', 'the nine dots, cyan into blue into violet', 'ripple', 'diamond', `<g class="mb-g">${[[12, 12, 0], [12, 5, 1], [19, 12, 1], [12, 19, 1], [5, 12, 1], [5, 5, 2], [19, 5, 2], [19, 19, 2], [5, 19, 2]].map(([x, y, n]) => dot(x, y, n ? 2.15 : 2.5, ` mb-r${n} ${x < 12 ? 'mb-c1' : x > 12 ? 'mb-c3' : 'mb-a'}`, n)).join('')}</g>`],
+ ] },
+ { title: 'What it opens', line: 'the menu itself, small: search, your apps, you', lead: true, items: [
+ ['Search and apps', 'the search bar over three apps', 'wave', '', frame(4, 5.2, 16, 5.6, 2.8, '', 0, 1.9) + dot(7.4, 8, 1.15, ' mb-a', 0) + box(4.2, 14.6, 4, 4, 1.3, '', 1) + box(10, 14.6, 4, 4, 1.3, ' mb-a', 2) + box(15.8, 14.6, 4, 4, 1.3, '', 3)],
+ ['You and your apps', 'you in the middle, eight apps round you; open, an × of dots', 'spin', 'gather', `<g class="mb-g">${dot(12, 12, 2.7, ' mb-a mb-r0', 0)}${[0, 1, 2, 3, 4, 5, 6, 7].map((k) => dot(...polar(8, k * 45), 1.45, k % 2 ? ' mb-r2' : ' mb-r1', 1 + (k % 2))).join('')}</g>`],
+ ['The V in the grid', 'the launcher\'s nine dots with a V lit in them; open, an × of dots', 'ripple', 'vgrid', `<g class="mb-g">${[[12, 12, 0, 1], [12, 5, 1, 0], [19, 12, 1, 0], [12, 19, 1, 0], [5, 12, 1, 0], [5, 5, 2, 1], [19, 5, 2, 1], [19, 19, 2, 0], [5, 19, 2, 0]].map(([x, y, n, lit]) => dot(x, y, n ? 2.1 : 2.4, ` mb-r${n}${lit ? ' mb-a' : ' mb-soft'}`, n)).join('')}</g>`],
+ ['Petals', 'four petals round a lit centre; open, they turn into an ×', 'bloom', 'plus', `<g class="mb-g">${box(10.4, 3, 3.2, 7.2, 1.6, '', 1)}${box(13.8, 10.4, 7.2, 3.2, 1.6, '', 2)}${box(10.4, 13.8, 3.2, 7.2, 1.6, '', 3)}${box(3, 10.4, 7.2, 3.2, 1.6, '', 4)}${dot(12, 12, 1.5, ' mb-a', 0)}</g>`],
+ ['Home screen', 'six apps and a dock, as on a phone', 'bloom', '', [0, 1].map((r) => [0, 1, 2].map((c) => box(4.2 + c * 5.8, 4 + r * 5.8, 4, 4, 1.3, r === 0 && c === 2 ? ' mb-a' : '', r + c)).join('')).join('') + box(5.2, 16.8, 13.6, 2.8, 1.4, ' mb-soft', 3)],
+ ] },
];
];
// ── The page ─────────────────────────────────────────────────────────────────────────────────────────────────────
// ── The page ─────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -645,11 +666,14 @@ function markCard(code, kind, [name, line_, spec, lock, motion], i) {
@@ -645,11 +666,14 @@ function markCard(code, kind, [name, line_, spec, lock, motion], i) {
}
}
const menuSvg = (anim, own, parts, size) => `<svg viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true" data-anim="${anim}"${own ? ` data-own="${own}"` : ''}><g class="mb-i">${parts}</g>${own ? '' : '<g class="mb-x"><path d="M7 7L17 17M17 7L7 17"/></g>'}</svg>`;
const menuSvg = (anim, own, parts, size) => `<svg viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true" data-anim="${anim}"${own ? ` data-own="${own}"` : ''}><g class="mb-i">${parts}</g>${own ? '' : '<g class="mb-x"><path d="M7 7L17 17M17 7L7 17"/></g>'}</svg>`;
+// What the button opens, behind it on the stage: the menu in small (its search bar, its three tabs, your apps). Pressed,
+// the button goes to the menu's top right and is its close button (marks.css .mb-peek); it is a picture, not a control.
+const PEEK = `<span class="mb-peek" aria-hidden="true"><i class="mb-peek-s"></i><span class="mb-peek-t">${[0, 1, 2].map((k) => `<i style="--i:${k + 1}"></i>`).join('')}</span><span class="mb-peek-g">${[212, 268, 28, 205, 152, 318, 48, 172].map((hue, k) => `<i style="--i:${k + 3};--h:${hue}"></i>`).join('')}</span></span>`;
function menuCard(code, [name, line_, anim, own, parts], i) {
function menuCard(code, [name, line_, anim, own, parts], i) {
const bar = (tone) => `<div class="mb-nav mb-${tone}">${useOf('art-pair-today', 22)}${word()}<button type="button" class="mb-btn" data-mb aria-pressed="false" aria-label="${code} at its real size: press to open">${menuSvg(anim, own, parts, 22)}</button></div>`;
const bar = (tone) => `<div class="mb-nav mb-${tone}">${useOf('art-pair-today', 22)}${word()}<button type="button" class="mb-btn" data-mb aria-pressed="false" aria-label="${code} at its real size: press to open">${menuSvg(anim, own, parts, 22)}</button></div>`;
return `<li class="mx-card mb-card" id="${code}" data-code="${code}" style="${delay(i)}">
return `<li class="mx-card mb-card" id="${code}" data-code="${code}" style="${delay(i)}">
<button type="button" class="mx-pick" data-pick="${code}" aria-pressed="false" aria-label="Pick ${code}"></button>
<button type="button" class="mx-pick" data-pick="${code}" aria-pressed="false" aria-label="Pick ${code}"></button>
-<div class="mx-stage"><button type="button" class="mb-btn mb-big" data-mb aria-pressed="false" aria-label="${code} ${esc(name)}: press to open it">${menuSvg(anim, own, parts, 84)}</button></div>
+<div class="mx-stage">${PEEK}<button type="button" class="mb-btn mb-big" data-mb aria-pressed="false" aria-label="${code} ${esc(name)}: press to open it">${menuSvg(anim, own, parts, 84)}</button></div>
<div class="mb-bars">${bar('dark')}${bar('light')}</div>
<div class="mb-bars">${bar('dark')}${bar('light')}</div>
<p class="mx-name"><b>${code}</b> ${esc(name)}</p>
<p class="mx-name"><b>${code}</b> ${esc(name)}</p>
<p class="mx-line">${esc(line_)}</p>
<p class="mx-line">${esc(line_)}</p>
@@ -696,11 +720,13 @@ function fontCard(code, [name, line_, stack, weight], i) {
@@ -696,11 +720,13 @@ function fontCard(code, [name, line_, stack, weight], i) {
const pad = (n) => String(n).padStart(2, '0');
const pad = (n) => String(n).padStart(2, '0');
function panel(id, kind, rows, label, hidden) {
function panel(id, kind, rows, label, hidden) {
let n = 0;
let n = 0;
- const body = rows.map((row) => {
+ // A row marked `lead` is shown first; its numbers still come from its place in the list, so no number ever moves.
+ const sections = rows.map((row) => {
const first = n + 1;
const first = n + 1;
const cards = row.items.map((item) => { n += 1; const code = `${kind}${pad(n)}`; return kind === 'M' ? menuCard(code, item, n - 1) : kind === 'F' ? fontCard(code, item, n - 1) : markCard(code, kind, item, n - 1); }).join('');
const cards = row.items.map((item) => { n += 1; const code = `${kind}${pad(n)}`; return kind === 'M' ? menuCard(code, item, n - 1) : kind === 'F' ? fontCard(code, item, n - 1) : markCard(code, kind, item, n - 1); }).join('');
return `<section class="mx-row"><h3>${esc(row.title)} <small>${kind}${pad(first)}–${kind}${pad(n)} · ${row.items.length}</small><span class="mx-swipe" aria-hidden="true">swipe →</span></h3><p class="mx-row-line">${esc(row.line)}</p><ul class="mx-grid">${cards}</ul></section>`;
return `<section class="mx-row"><h3>${esc(row.title)} <small>${kind}${pad(first)}–${kind}${pad(n)} · ${row.items.length}</small><span class="mx-swipe" aria-hidden="true">swipe →</span></h3><p class="mx-row-line">${esc(row.line)}</p><ul class="mx-grid">${cards}</ul></section>`;
- }).join('');
+ });
+ const body = rows.map((row, k) => [row.lead ? 0 : 1, k]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).map(([, k]) => sections[k]).join('');
const NEXT = {
const NEXT = {
loop: [['pair', 'See the Pair', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],
loop: [['pair', 'See the Pair', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],
pair: [['loop', 'See the Loop', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],
pair: [['loop', 'See the Loop', 'the other kind'], ['menu', 'Next: menu button', 'step 2']],
@@ -715,7 +741,7 @@ function panel(id, kind, rows, label, hidden) {
@@ -715,7 +741,7 @@ function panel(id, kind, rows, label, hidden) {
loop: [1, 'Pick a logo: the Loop', 'Tap the ones you like. Look at the Pair too.'],
loop: [1, 'Pick a logo: the Loop', 'Tap the ones you like. Look at the Pair too.'],
pair: [1, 'Pick a logo: the Pair', 'Tap the ones you like. Look at the Loop too.'],
pair: [1, 'Pick a logo: the Pair', 'Tap the ones you like. Look at the Loop too.'],
wild: [1, 'Pick a logo: the Wildcards', 'The O and the V, forty other ways. Each moves, shows a status, and wears the name its own way.'],
wild: [1, 'Pick a logo: the Wildcards', 'The O and the V, forty other ways. Each moves, shows a status, and wears the name its own way.'],
- menu: [2, 'Pick a menu button', 'Tap one to see it open.'],
+ menu: [2, 'Pick a menu button', 'Tap one: it opens the menu and becomes its close button.'],
font: [3, 'Pick a font', 'Tap the one that looks right.'],
font: [3, 'Pick a font', 'Tap the one that looks right.'],
}[id];
}[id];
const head = `<div class="mx-stephead"><span class="mx-stepn mx-stepn-lg" aria-hidden="true">${STEP[0]}</span><div><b>Step ${STEP[0]} of 3 · ${esc(STEP[1])}</b><small>${esc(STEP[2])}</small></div></div>`;
const head = `<div class="mx-stephead"><span class="mx-stepn mx-stepn-lg" aria-hidden="true">${STEP[0]}</span><div><b>Step ${STEP[0]} of 3 · ${esc(STEP[1])}</b><small>${esc(STEP[2])}</small></div></div>`;
@@ -798,7 +824,7 @@ ${wild ? `<span class="mx-or" aria-hidden="true">+</span>
@@ -798,7 +824,7 @@ ${wild ? `<span class="mx-or" aria-hidden="true">+</span>
${tab('wild', 'The Wildcards', 'OV, forty other ways', `<svg viewBox="0 0 100 100" width="58" height="58" aria-hidden="true"><defs>${wildEx.defs}</defs>${wildEx.body}</svg>`, `W01–W${pad(WN)} · ${WN} ideas`)}` : ''}
${tab('wild', 'The Wildcards', 'OV, forty other ways', `<svg viewBox="0 0 100 100" width="58" height="58" aria-hidden="true"><defs>${wildEx.defs}</defs>${wildEx.body}</svg>`, `W01–W${pad(WN)} · ${WN} ideas`)}` : ''}
</div>
</div>
<p class="mx-q" role="presentation"><span class="mx-qn">2</span> <b>Pick a menu button</b> <small>it sits next to the logo</small></p>
<p class="mx-q" role="presentation"><span class="mx-qn">2</span> <b>Pick a menu button</b> <small>it sits next to the logo</small></p>
-${tab('menu', 'The menu button', 'The button next to the logo', `<span class="mb-btn mb-tabbtn">${menuSvg(m1[2], m1[3], m1[4], 30)}</span>`, 'M01–M50', false, true)}
+${tab('menu', 'The menu button', 'The button next to the logo', `<span class="mb-btn mb-tabbtn">${menuSvg(m1[2], m1[3], m1[4], 30)}</span>`, `M01–M${pad(MENU_ROWS.reduce((t, r) => t + r.items.length, 0))}`, false, true)}
<p class="mx-q" role="presentation"><span class="mx-qn">3</span> <b>Pick a font</b> <small>for the name</small></p>
<p class="mx-q" role="presentation"><span class="mx-qn">3</span> <b>Pick a font</b> <small>for the name</small></p>
${tab('font', 'The font', 'The letters of the name', '<span class="fx-tabart" aria-hidden="true">Aa</span>', `F01–F${pad(FONTS.length)}`, false, true)}
${tab('font', 'The font', 'The letters of the name', '<span class="fx-tabart" aria-hidden="true">Aa</span>', `F01–F${pad(FONTS.length)}`, false, true)}
</nav>
</nav>
@@ -817,7 +843,7 @@ ${wild ? `<a href="#wild" data-mx-go="wild" class="mx-step">${miniArt('W01', 20,
@@ -817,7 +843,7 @@ ${wild ? `<a href="#wild" data-mx-go="wild" class="mx-step">${miniArt('W01', 20,
${panel('loop', 'L', LOOP_ROWS, `The Loop: ${LOOPS.length} takes`, false)}
${panel('loop', 'L', LOOP_ROWS, `The Loop: ${LOOPS.length} takes`, false)}
${panel('pair', 'P', PAIR_ROWS, `The Pair: ${PAIRS.length} takes`, true)}
${panel('pair', 'P', PAIR_ROWS, `The Pair: ${PAIRS.length} takes`, true)}
${wild ? panel('wild', 'W', WILD_ROWS, `The Wildcards: ${WN} ideas`, true) : ''}
${wild ? panel('wild', 'W', WILD_ROWS, `The Wildcards: ${WN} ideas`, true) : ''}
-${panel('menu', 'M', MENU_ROWS, 'The menu button: fifty ideas', true)}
+${panel('menu', 'M', MENU_ROWS, `The menu button: ${MENU_ROWS.reduce((t, r) => t + r.items.length, 0)} ideas`, true)}
${panel('font', 'F', FONT_ROWS, 'The font: twelve faces', true)}
${panel('font', 'F', FONT_ROWS, 'The font: twelve faces', true)}
<div class="mx-nudge" data-mx-nudge role="status" hidden><span data-mx-nudge-text></span><a href="#menu" data-mx-go="menu" class="mx-btn" data-mx-nudge-go>Go to step 2 →</a><button type="button" class="mx-btn mx-btn-quiet" data-mx-nudge-close aria-label="Close">✕</button></div>
<div class="mx-nudge" data-mx-nudge role="status" hidden><span data-mx-nudge-text></span><a href="#menu" data-mx-go="menu" class="mx-btn" data-mx-nudge-go>Go to step 2 →</a><button type="button" class="mx-btn mx-btn-quiet" data-mx-nudge-close aria-label="Close">✕</button></div>
<p class="mx-fine">Nothing changes on the sites until a logo wins. <a href="/brand">Today’s logo</a>.</p>
<p class="mx-fine">Nothing changes on the sites until a logo wins. <a href="/brand">Today’s logo</a>.</p>
@@ -12,7 +12,7 @@ const explore = require('../server/views/marks-explore');
@@ -12,7 +12,7 @@ const explore = require('../server/views/marks-explore');
(async () => {
(async () => {
await check('fifty distinct takes in each family: no two share their whole recipe', () => {
await check('fifty distinct takes in each family: no two share their whole recipe', () => {
for (const [name, list] of [['Loop', explore.LOOPS], ['Pair', explore.PAIRS], ['menu', explore.MENUS]]) {
for (const [name, list] of [['Loop', explore.LOOPS], ['Pair', explore.PAIRS], ['menu', explore.MENUS]]) {
- const want = { Loop: 72, Pair: 60, menu: 50 }[name];
+ const want = { Loop: 72, Pair: 60, menu: 61 }[name];
assert.strictEqual(list.length, want, `${name}: ${want}`);
assert.strictEqual(list.length, want, `${name}: ${want}`);
const recipes = list.map((x) => JSON.stringify(x.slice(2)));
const recipes = list.map((x) => JSON.stringify(x.slice(2)));
assert.strictEqual(new Set(recipes).size, want, `${name}: every recipe differs`);
assert.strictEqual(new Set(recipes).size, want, `${name}: every recipe differs`);
@@ -20,15 +20,37 @@ const explore = require('../server/views/marks-explore');
@@ -20,15 +20,37 @@ const explore = require('../server/views/marks-explore');
}
}
});
});
+ await check('the bar is the preview: every menu button it has wears the pick, so do the two close buttons, and it stays in view', () => {
+ const fs = require('fs'), path = require('path');
+ const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'marks.js'), 'utf8');
+ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'marks.css'), 'utf8');
+ assert.ok(js.includes("var orbs = all('.ovnav-orb', bar)"), 'every menu button in the bar (a phone shows the one at its right end)');
+ assert.ok(js.includes("all('.ovs-orb, .ovdk-orbx').forEach"), 'the menu\'s own close button and the Dock\'s wear it too');
+ assert.ok(js.includes("flag(mine, 'is-open', dock ? !dock.hidden && !host.classList.contains('is-from') : host.classList.contains('is-x'))"), 'open when the sheet is, at rest while it is still on the bar\'s button');
+ assert.ok(/\.mx-skinned > svg\{opacity:0\}/.test(css) && /\.mx-skinned > \.mx-navmenu\{all:unset;grid-area:1\/1;/.test(css), 'over their own drawing, which keeps its place (the Dock measures it)');
+ assert.ok(css.includes('html:has(.mx-wrap) .openvibe-navbar.ovnav-hide{transform:none!important}'), 'on this page a phone\'s bar does not hide as you scroll');
+ assert.ok(!/prefers-reduced-motion:\s*no-preference/.test(css), 'no move is kept only for people who have not asked for less motion');
+ });
+
const t = await boot();
const t = await boot();
try {
try {
await check('three tabs, a hundred and fifty numbered cards in rows, every one pickable', async () => {
await check('three tabs, a hundred and fifty numbered cards in rows, every one pickable', async () => {
const r = await t.get('/brand/explore');
const r = await t.get('/brand/explore');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const codes = [...r.text.matchAll(/<li class="mx-card(?: mb-card| fx-card| mx-wild)?" id="([LPMFW]\d{2})" data-code="\1"/g)].map((m) => m[1]);
const codes = [...r.text.matchAll(/<li class="mx-card(?: mb-card| fx-card| mx-wild)?" id="([LPMFW]\d{2})" data-code="\1"/g)].map((m) => m[1]);
- assert.strictEqual(codes.length, 194, 'the Loop, the Pair, the menu buttons and the fonts: the Wildcards are off the vote');
- assert.strictEqual(new Set(codes).size, 194, 'each number once');
- assert.deepStrictEqual(['L', 'P', 'W', 'M', 'F'].map((k) => codes.filter((c) => c[0] === k).length), [72, 60, 0, 50, 12]);
+ assert.strictEqual(codes.length, 205, 'the Loop, the Pair, the menu buttons and the fonts: the Wildcards are off the vote');
+ assert.strictEqual(new Set(codes).size, 205, 'each number once');
+ // Eleven more menu buttons (2026-11-10), made for this button: drawn from the logo, and the menu in small. They
+ // are shown first and numbered last, so M01–M50 and the votes cast for them stay where they were.
+ const menus = codes.filter((c) => c[0] === 'M');
+ assert.deepStrictEqual(menus.slice(0, 12), ['M51', 'M52', 'M53', 'M54', 'M55', 'M56', 'M57', 'M58', 'M59', 'M60', 'M61', 'M01'], 'the two new families lead, then the diamond');
+ assert.strictEqual(explore.MENUS[7][0], 'Lit corners', 'M08 is still the idea it was');
+ assert.match(r.text, /<h3>From the logo <small>M51–M56 · 6<\/small>/);
+ assert.match(r.text, /<h3>What it opens <small>M57–M61 · 5<\/small>/);
+ assert.match(r.text, /Tap one: it opens the menu and becomes its close button\./);
+ assert.match(r.text, /<div class="mx-stage"><span class="mb-peek" aria-hidden="true">[\s\S]*?<\/span><button type="button" class="mb-btn mb-big" data-mb/, 'each stage holds the menu it opens, as a picture behind the button');
+ for (const own of ['grow', 'growdot', 'meet', 'gather', 'vgrid']) assert.ok(r.text.includes(`data-own="${own}"`), `a button that opens its own way: ${own}`);
+ assert.deepStrictEqual(['L', 'P', 'W', 'M', 'F'].map((k) => codes.filter((c) => c[0] === k).length), [72, 60, 0, 61, 12]);
// The owner, 11-01: "I've changed my mind on them… keep them, they're cool to look at": ?wild=1 still shows them.
// The owner, 11-01: "I've changed my mind on them… keep them, they're cool to look at": ?wild=1 still shows them.
const withWild = await t.get('/brand/explore?wild=1');
const withWild = await t.get('/brand/explore?wild=1');
assert.strictEqual((withWild.text.match(/<li class="mx-card mx-wild" id="W\d\d"/g) || []).length, 40, 'the Wildcards, to look at');
assert.strictEqual((withWild.text.match(/<li class="mx-card mx-wild" id="W\d\d"/g) || []).length, 40, 'the Wildcards, to look at');