diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 1521609..54682ec 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -26,7 +26,7 @@ - + @@ -82,7 +82,7 @@ - + diff --git a/layouts/partials/nav.html b/layouts/partials/nav.html index 6c61e6b..788e74a 100644 --- a/layouts/partials/nav.html +++ b/layouts/partials/nav.html @@ -12,6 +12,7 @@
+
diff --git a/static/js/coin-flip.js b/static/js/coin-flip.js index 57db9bd..e7a8701 100644 --- a/static/js/coin-flip.js +++ b/static/js/coin-flip.js @@ -5,9 +5,19 @@ const coin = document.getElementById("profileCoin"); const mobileCoin = document.getElementById("mobileCoin"); const frontFace = coin?.querySelector(".coin-front"); const backFace = coin?.querySelector(".coin-back"); +const ripple = coin?.querySelector(".coin-ripple"); + +// Water ripple behind the coin once it lands +function fireRipple() { + if (!ripple || (window.FIELD && window.FIELD.prefersReducedMotion())) return; + ripple.classList.remove("is-rippling"); + void ripple.offsetWidth; // restart CSS animation + ripple.classList.add("is-rippling"); +} let flipping = false; let showingReal = true; // front = real, back = generative +let rippleTimer = null; function randomRange(min, max) { return Math.random() * (max - min) + min; @@ -34,6 +44,13 @@ function flipCoin() { setTimeout(() => { flipping = false; }, lockDuration); + + // Ripple lands slightly after the 600ms flip settles; reschedule if a + // second flip sneaks in between unlock (600ms) and ripple (700ms) + if (lockDuration) { + clearTimeout(rippleTimer); + rippleTimer = setTimeout(fireRipple, 700); + } } // Click handler (count only real clicks; auto-flips don't increment) diff --git a/themes/resume/static/css/resume.css b/themes/resume/static/css/resume.css index 5e872e8..a66f4c7 100644 --- a/themes/resume/static/css/resume.css +++ b/themes/resume/static/css/resume.css @@ -285,6 +285,83 @@ a:hover, a:focus, a:active { } } +/* Water ripple — rings spread out from under the coin after it lands (triggered by + coin-flip.js). Rings paint above the faces (z-index 3) because the coin's ::before + drop shadow would otherwise bury them; starting at scale(1) hides the ring under + the coin's dark rim so it reads as emerging from behind. Transform/opacity only; + rings expand past the sidebar and fade to nothing, clipped at the blue edge. */ +@media (min-width: 992px) { + #sideNav { + overflow: hidden; /* clip ripple rings to the blue sidebar */ + } +} + +.coin-ripple { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + pointer-events: none; + z-index: 3; +} + +/* Soft shadow-toned bands (not hairlines): thick low-alpha border + baked-in + box-shadow blur, painted once then transform-scaled — the blur diffuses as + the ring grows, reading as the coin's shadow dissolving into the water. + Colors sit between the coin's black drop shadow and the --brand teal. */ +.coin-ripple span { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + border: 8px solid rgba(16, 45, 66, 0.3); + box-shadow: 0 0 10px rgba(16, 45, 66, 0.25), inset 0 0 10px rgba(16, 45, 66, 0.25); + opacity: 0; + transform: scale(1); +} + +.coin-ripple span:first-child { + /* impact band — blacker, the coin shadow "leaking" outward */ + border-color: rgba(6, 20, 30, 0.35); + box-shadow: 0 0 10px rgba(6, 20, 30, 0.3), inset 0 0 10px rgba(6, 20, 30, 0.3); +} + +.coin-ripple.is-rippling span { + animation: coin-ripple 1.5s cubic-bezier(0.15, 0.55, 0.45, 1) forwards; +} + +.coin-ripple.is-rippling span:nth-child(2) { + animation-delay: 0.12s; +} + +.coin-ripple.is-rippling span:nth-child(3) { + animation-delay: 0.24s; +} + +@keyframes coin-ripple { + from { + opacity: 0.55; + transform: scale(1); + } + 55% { + opacity: 0.18; + } + to { + opacity: 0; + transform: scale(4.75); + } +} + +@media (prefers-reduced-motion: reduce) { + .coin-ripple span { + animation: none !important; + } +} + /* Nav scroll-reveal — mobile only */ @media (max-width: 991px) { #sideNav {