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 {