diff --git a/avian/frontend/apt.js b/avian/frontend/apt.js index 5acf486..35a4087 100644 --- a/avian/frontend/apt.js +++ b/avian/frontend/apt.js @@ -412,6 +412,12 @@ function renderCollage(items, animate) { collage.innerHTML = ''; + // Drop the previous render's hit-test tiles up front so a click or hover on + // the empty-nest state (or a collage that hasn't laid out yet) resolves to + // nothing, not to a stale bird from the last populated render. The populated + // path repopulates collagePlaced once the new tiles are placed. + collagePlaced = []; + collageHovered = null; if (!items.length) { // No birds heard yet: show an empty nest where the collage would be, with // the status line beneath it. The frame (shoot.py) overrides the .empty @@ -419,6 +425,16 @@ collage.innerHTML = '
' + 'an empty nest' + '

no birds heard in this window.

'; + // Bloom the nest in on the same cues as the collage (first load, window + // change, view switch); a silent poll/resize renders without animate. The + // class self-clears after the worst case so a throttled tab still ends + // with the nest visible, mirroring the tile entrance's safety net. + if (animate) { + var enest = collage.firstChild; + enest.classList.add('entering'); + clearTimeout(collageEntranceT); + collageEntranceT = setTimeout(function () { enest.classList.remove('entering'); }, 900); + } return; } var W = collage.clientWidth, H = collage.clientHeight; diff --git a/avian/frontend/styles.css b/avian/frontend/styles.css index ae494fa..8243a1d 100644 --- a/avian/frontend/styles.css +++ b/avian/frontend/styles.css @@ -602,6 +602,18 @@ } .empty-nest .nest-img { width: 78%; max-width: 470px; height: auto; } .empty-nest .empty { padding: 0; margin: 0; } + /* Bloom the empty nest in on the same cues as the collage, reusing the tile + keyframe (gtile-in): the nest fades + scales, the status line trails it. */ + .empty-nest.entering .nest-img { + animation: gtile-in 420ms cubic-bezier(.2, .7, .3, 1) backwards; + } + .empty-nest.entering .empty { + animation: gtile-in 420ms cubic-bezier(.2, .7, .3, 1) 130ms backwards; + } + @media (prefers-reduced-motion: reduce) { + .empty-nest.entering .nest-img, + .empty-nest.entering .empty { animation: none; } + } /* Page header is now shared at .stage > .static-head - see top of CSS. */ /* Stats grid: charts stack on the LEFT, text sections on the RIGHT.