{
const detail = $event.detail;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const wait = (ms) => new Promise((r) => setTimeout(r, reduce ? 0 : ms));
// ONE synchronized motion. The gradient colour shift starts NOW and
// its CSS transition (700ms, stories-drawer.css) spans the entire
// out+in cycle below — so colours morph continuously while the items
// and text fade out (350ms), swap invisibly, and come back in
// (350ms). sphere-gradient-shift is window-scoped so it reaches the
// globe's nested x-data regardless of DOM position; `gradient` (this
// component's own prop) is kept in sync as the fallback background
// whenever the colour-transition path isn't available (see the
// globe).
window.dispatchEvent(new CustomEvent('sphere-gradient-shift', { detail }));
gradient = detail.gradient;
// Out (~350ms): floating items AND hero text fade together
// (.is-rerolling drives the items, titleOpacity the text).
$el.classList.add('is-rerolling');
hoveredIndex = -1;
titleIndex = -1;
titleFx = reduce ? 'none' : 'opacity 350ms ease';
titleOpacity = 0;
await wait(350);
// Swap while everything is at opacity 0: new title, items, layout.
title = detail.title;
items = detail.items;
positions = buildPositions(items.length);
applyEdgeBias();
$el.classList.remove('is-rerolling');
await new Promise((r) => $nextTick(r));
// In (~350-400ms): title fades back while the new items reveal
// staggered on the same clock (~70ms/item — the x-for below drives
// each item's delay off its index; sphere-items-enter fires once the
// new elements are mounted so their listeners are ready to catch it).
titleFx = reduce ? 'none' : 'opacity 350ms ease';
titleOpacity = 1;
window.dispatchEvent(new CustomEvent('sphere-items-enter'));
})()
"
>
{ entering = false }, i * 70);
"
:class="{ 'is-item-entering': entering }"
>