diff --git a/backend/static/dashboard-transition.js b/backend/static/dashboard-transition.js
new file mode 100644
index 0000000..df82879
--- /dev/null
+++ b/backend/static/dashboard-transition.js
@@ -0,0 +1,87 @@
+(() => {
+ const OPENING_MS = 420;
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const dashboardGrid = document.querySelector(".dashboard-grid");
+ const dashboardLinks = document.querySelectorAll(".dashboard-link-card[href]");
+
+ if (dashboardGrid) {
+ dashboardLinks.forEach((link, index) => {
+ link.style.setProperty("--stagger-delay", `${index * 55}ms`);
+ });
+
+ dashboardGrid.classList.add("is-ready");
+ window.requestAnimationFrame(() => {
+ dashboardGrid.classList.add("is-animated");
+ });
+ }
+
+ dashboardLinks.forEach((link) => {
+ link.addEventListener("click", (event) => {
+ if (link.classList.contains("is-opening")) {
+ event.preventDefault();
+ return;
+ }
+
+ if (
+ event.defaultPrevented ||
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey
+ ) {
+ return;
+ }
+
+ const href = link.getAttribute("href");
+ if (!href || href.startsWith("http")) {
+ return;
+ }
+
+ event.preventDefault();
+ if (dashboardGrid) {
+ dashboardGrid.classList.add("is-focusing");
+ }
+ link.classList.add("is-opening");
+
+ // Force layout before running the exit animation for consistent playback.
+ void link.offsetWidth;
+
+ let hasNavigated = false;
+ const navigate = () => {
+ if (hasNavigated) {
+ return;
+ }
+ hasNavigated = true;
+ window.location.href = href;
+ };
+
+ if (typeof link.animate === "function") {
+ const animation = link.animate(
+ [
+ { opacity: 1, transform: "translateY(-6px) scale(1)", filter: "blur(0px)" },
+ { opacity: 0, transform: "translateY(-42px) scale(0.982)", filter: "blur(2px)" },
+ ],
+ {
+ duration: OPENING_MS,
+ easing: "cubic-bezier(0.22, 0.61, 0.36, 1)",
+ fill: "forwards",
+ }
+ );
+ animation.finished.then(navigate).catch(navigate);
+ } else {
+ // Fallback if WAAPI is unavailable.
+ link.style.transition = `transform ${OPENING_MS}ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity ${OPENING_MS}ms cubic-bezier(0.22, 0.61, 0.36, 1), filter ${OPENING_MS}ms cubic-bezier(0.22, 0.61, 0.36, 1)`;
+ requestAnimationFrame(() => {
+ link.style.transform = "translateY(-42px) scale(0.982)";
+ link.style.opacity = "0";
+ link.style.filter = "blur(2px)";
+ });
+ }
+
+ window.setTimeout(navigate, OPENING_MS + 240);
+ });
+ });
+ });
+})();
diff --git a/backend/static/styles.css b/backend/static/styles.css
index 62d8664..366cca6 100644
--- a/backend/static/styles.css
+++ b/backend/static/styles.css
@@ -130,11 +130,73 @@ h3 {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
+.dashboard-grid {
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1rem;
+}
+
+.dashboard-grid.is-ready .dashboard-link-card {
+ opacity: 0;
+ transform: translateY(14px) scale(0.985);
+}
+
+.dashboard-grid.is-ready.is-animated .dashboard-link-card {
+ animation: dashboard-card-in 460ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
+ animation-delay: var(--stagger-delay, 0ms);
+}
+
+.dashboard-grid.is-focusing .dashboard-link-card:not(.is-opening) {
+ opacity: 0.56;
+ transform: scale(0.975);
+ filter: saturate(0.82);
+}
+
+@keyframes dashboard-card-in {
+ from {
+ opacity: 0;
+ transform: translateY(14px) scale(0.985);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.dashboard-link-card {
+ min-height: 140px;
+ aspect-ratio: 1 / 1;
+ font-size: 1.14rem;
+ font-weight: 600;
+ border-radius: 20px;
+ padding: 1rem;
+ position: relative;
+ background: #fffdf9;
+ border: 1px solid rgba(39, 66, 53, 0.11);
+ letter-spacing: 0.01em;
+ box-shadow: 0 12px 30px rgba(39, 66, 53, 0.12);
+ will-change: transform, opacity, filter;
+}
+
+.dashboard-link-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 18px 40px rgba(39, 66, 53, 0.18);
+}
+
+.dashboard-link-card.is-opening {
+ box-shadow: 0 20px 44px rgba(39, 66, 53, 0.22);
+ pointer-events: none;
+}
+
.link-card:hover {
transform: translateY(-3px);
box-shadow: 0 14px 30px rgba(39, 66, 53, 0.16);
}
+.dashboard-grid .dashboard-link-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 18px 40px rgba(39, 66, 53, 0.18);
+}
+
.form-card {
max-width: 560px;
}
@@ -721,6 +783,26 @@ input[type="file"]:focus {
.location-actions {
justify-content: flex-start;
}
+
+ .dashboard-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .dashboard-link-card {
+ min-height: 0;
+ font-size: 1.08rem;
+ }
+}
+
+@media (max-width: 380px) {
+ .dashboard-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .dashboard-link-card {
+ min-height: 118px;
+ aspect-ratio: auto;
+ }
}
.site-footer {
diff --git a/backend/templates/base.html b/backend/templates/base.html
index bb2c5a9..0e425eb 100644
--- a/backend/templates/base.html
+++ b/backend/templates/base.html
@@ -51,5 +51,6 @@
{{ t('privacy') }}
{{ t('imprint') }}
+