// 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)
  })
}
