diff --git a/script.js b/script.js index 1d6c3ff..f88431c 100644 --- a/script.js +++ b/script.js @@ -1,5 +1,7 @@ "use strict"; +document.documentElement.classList.add("js"); + // Theme: persisted in sessionStorage, dark default. const root = document.documentElement; root.dataset.theme = sessionStorage.getItem("theme") || "dark"; @@ -12,6 +14,28 @@ document.querySelector("[data-theme-btn]")?.addEventListener("click", () => { // Footer year document.getElementById("year").textContent = new Date().getFullYear(); +// Card spotlight: track cursor into --mx/--my +for (const card of document.querySelectorAll(".card")) { + card.addEventListener("mousemove", (e) => { + const r = card.getBoundingClientRect(); + card.style.setProperty("--mx", `${e.clientX - r.left}px`); + card.style.setProperty("--my", `${e.clientY - r.top}px`); + }); +} + +// Staggered reveal on scroll +const reveal = new IntersectionObserver((entries) => { + for (const en of entries) { + if (en.isIntersecting) { + const cards = [...en.target.parentElement.children]; + en.target.style.transitionDelay = `${cards.indexOf(en.target) * 60}ms`; + en.target.classList.add("in"); + reveal.unobserve(en.target); + } + } +}, { threshold: 0.15 }); +document.querySelectorAll(".card").forEach((c) => reveal.observe(c)); + // Contact form -> sendmail.tdvorak.dev const form = document.getElementById("contact-form"); const status = document.getElementById("form-status"); diff --git a/style.css b/style.css index 110008a..26330c7 100644 --- a/style.css +++ b/style.css @@ -152,14 +152,54 @@ a { color: inherit; text-decoration: none; } margin-top: 40px; } .card { + --glow: rgba(255, 255, 255, 0.35); + position: relative; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-raised); overflow: hidden; - transition: border-color 0.15s, transform 0.15s; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; } -.card:hover { border-color: var(--border-strong); transform: translateY(-2px); } -.card figure { aspect-ratio: 16/9; overflow: hidden; border-bottom: 1px solid var(--border); } -.card figure img { width: 100%; height: 100%; object-fit: cover; object-position: top; } +.js .card { opacity: 0; transform: translateY(14px); } +[data-theme="light"] .card { --glow: rgba(0, 0, 0, 0.16); } +.card.in { opacity: 1; transform: none; transition: opacity 0.5s ease, transform 0.5s ease, border-color 0.2s, box-shadow 0.2s; } +.card:hover { border-color: var(--border-strong); transform: translateY(-3px); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35); } +[data-theme="light"] .card:hover { box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); } +/* mouse-tracked border glow */ +.card::before { + content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; + border-radius: var(--radius); padding: 1px; + background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%), var(--glow), transparent 70%); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; mask-composite: exclude; + opacity: 0; transition: opacity 0.3s; +} +/* soft inner spotlight */ +.card::after { + content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; + background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.05), transparent 70%); + opacity: 0; transition: opacity 0.3s; +} +[data-theme="light"] .card::after { background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%), rgba(0, 0, 0, 0.04), transparent 70%); } +.card:hover::before, .card:hover::after { opacity: 1; } +.card > * { position: relative; z-index: 1; } +.card figure { position: relative; aspect-ratio: 16/9; overflow: hidden; border-bottom: 1px solid var(--border); } +.card figure img { + width: 100%; height: 100%; object-fit: cover; object-position: top; + filter: saturate(0.9); transition: transform 0.5s ease, filter 0.5s ease; +} +.card:hover figure img { transform: scale(1.03); filter: saturate(1.05); } +/* ↗ badge over the image */ +.card figure::after { + content: "↗"; position: absolute; top: 10px; right: 10px; + display: grid; place-items: center; width: 26px; height: 26px; + font-family: var(--font-mono); font-size: 13px; color: var(--fg); + background: color-mix(in srgb, var(--bg) 75%, transparent); + border: 1px solid var(--border-strong); border-radius: 999px; + backdrop-filter: blur(8px); + opacity: 0; transform: translate(-6px, 6px); + transition: opacity 0.2s, transform 0.2s; +} +.card:hover figure::after { opacity: 1; transform: none; } .card-body { padding: 20px; } .card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .card-head h3 { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }