site: motion & delight pass — media fades in, cards lift, grids ripple

- Images/videos fade in on load instead of popping (cached media renders
  instantly; nothing is hidden without JS)
- Cross-document view transitions: soft cross-fade between pages where
  supported
- Masonry reveals ripple: initial grid and "Load more" batches stagger in
  per-tile instead of blinking on at once
- Card hover: gentle -2px lift + slow image zoom on hero, home/grid tiles,
  and find cards; find-card lift switched to `translate` so it composes
  with the masonry's inline transform (it was silently dead before), and
  its transition list unified with .masonry-tile's to end their cascade tie
- Tactile press/lift on "Load more" and the external CTA button
- Accent-tinted ::selection, visible :focus-visible rings, smooth anchor
  scrolling; global prefers-reduced-motion catch-all disables it all

Claude-Session: https://claude.ai/code/session_01ADR9r5rw5NuvvnCxfYbMuc
This commit is contained in:
2026-07-11 15:20:47 -04:00
parent e06e7311d6
commit a0ea9c9690
5 changed files with 119 additions and 13 deletions
+58 -2
View File
@@ -212,9 +212,35 @@ const year = new Date().getFullYear();
@media (prefers-reduced-motion: reduce) {
body { transition: none; }
}
a { color: var(--accent); text-decoration: none; }
a { color: var(--accent); text-decoration: none; transition: color 0.15s ease; }
a:hover { text-decoration: underline; }
/* Cross-document view transitions — a soft cross-fade between pages in
browsers that support it; everyone else just navigates. */
@view-transition { navigation: auto; }
::selection { background: color-mix(in srgb, var(--accent) 24%, var(--bg)); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
html { scroll-behavior: smooth; }
/* Media fade-in: the script at the foot of <body> tags not-yet-loaded
images/videos with .fx-fade and flips .fx-in when each finishes, so
media eases in instead of popping. Cached media never gets the class
(no flash on repeat views); without JS nothing is ever hidden. */
.fx-fade { opacity: 0; }
.fx-fade.fx-in { opacity: 1; transition: opacity 0.55s ease; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
@view-transition { navigation: none; }
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* Masthead */
.masthead {
margin-bottom: 2rem;
@@ -420,7 +446,16 @@ const year = new Date().getFullYear();
font-weight: 600;
font-size: 0.95rem;
}
.external a:hover { text-decoration: none; opacity: 0.9; }
.external a {
transition: opacity 0.18s ease, translate 0.18s ease, box-shadow 0.18s ease;
}
.external a:hover {
text-decoration: none;
opacity: 0.92;
translate: 0 -1px;
box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--accent) 70%, transparent);
}
.external a:active { translate: 0 0; }
.external .aff-badge {
display: inline-block;
margin-left: 0.6rem;
@@ -519,6 +554,27 @@ const year = new Date().getFullYear();
.colophon dt:first-child { margin-top: 0; }
}
</style>
<script>
// Fade media in on load instead of popping — see .fx-fade in the global
// styles. Only media still in flight is tagged, so cached images render
// instantly with no flash.
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
for (const img of document.querySelectorAll('img')) {
if (img.complete) continue;
const reveal = () => img.classList.add('fx-in');
img.classList.add('fx-fade');
img.addEventListener('load', reveal, { once: true });
img.addEventListener('error', reveal, { once: true });
}
for (const video of document.querySelectorAll('video')) {
if (video.readyState >= 2) continue;
const reveal = () => video.classList.add('fx-in');
video.classList.add('fx-fade');
video.addEventListener('loadeddata', reveal, { once: true });
video.addEventListener('error', reveal, { once: true });
}
}
</script>
{import.meta.env.DEV && <EditConfirm />}
</body>
</html>