// Resolve once the image at `url` is fully loaded AND decoded (paint-ready), // or after `timeoutMs` as a safety net so a broken/slow image never stalls a // cascade. Never rejects — the caller only cares that it's safe to reveal. // // Used by the showcase cascade to gate each tile's entry animation on the // thumbnail actually being ready, so the flip-in plays on a real image rather // than on a gray placeholder (operator-flagged 2026-06-04). export function preloadImage (url, timeoutMs = 4000) { return new Promise((resolve) => { let done = false const finish = () => { if (!done) { done = true; resolve() } } const img = new Image() img.onload = finish img.onerror = finish img.src = url // decode() resolves at paint-ready (a beat after onload); prefer it when // available, but onload/onerror/timeout all still settle the promise. if (typeof img.decode === 'function') img.decode().then(finish).catch(() => {}) setTimeout(finish, timeoutMs) }) }