diff --git a/web/frontend/index.html b/web/frontend/index.html
index 59720c0..cb962c7 100644
--- a/web/frontend/index.html
+++ b/web/frontend/index.html
@@ -255,6 +255,25 @@ img { display: block; }
#app.side .island button { text-align: left; border-radius: var(--r-control); }
#app.side .island .sep { width: auto; height: 1px; margin: var(--s-snug); }
#app.side #content { margin-left: calc(var(--sidebar-w) + 24px); }
+
+/* ---- Der Umzug der Leiste -----------------------------------------------
+ Nachgebaut nach der App: die Spalte klappt nach **oben** weg und kommt von
+ oben zurück, die Insel schiebt sich **seitwärts** hinaus und herein.
+ Zusammen sieht es aus, als zöge dieselbe Leiste um — erst zusammenklappen,
+ dann hinüberschieben.
+
+ Der Inhalt rückt gleichzeitig nach; ohne das bliebe links ein Loch stehen,
+ bis die Leiste angekommen ist. */
+.island { transition: transform .3s cubic-bezier(.32,.72,0,1), opacity .22s ease; }
+#content { transition: margin-left .32s cubic-bezier(.32,.72,0,1); }
+
+#chrome.moving .island { opacity: 0; transform: translateX(-115%); }
+#app.side #chrome.moving .island { opacity: 0; transform: translateY(-115%); }
+
+/* Wer Bewegung nicht mag, bekommt keine. */
+@media (prefers-reduced-motion: reduce) {
+ .island, #content, #chrome, .card .art, .shot { transition: none !important; }
+}
#app.side .pad-top { padding-top: var(--s-loose); }
.island .brand {
@@ -298,6 +317,20 @@ input[type=range] { width: 100%; accent-color: var(--accent); }
/* ---- Zustände ------------------------------------------------------------ */
#content { padding-bottom: var(--s-section); }
+
+/* Ein Seitenwechsel soll nicht schlagen. Kurz und flach — was länger dauert,
+ fühlt sich nach Warten an statt nach Bewegung. */
+@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
+.row, .grid, .settings, #hero { animation: rise .32s cubic-bezier(.32,.72,0,1) both; }
+/* Die Reihen kommen gestaffelt — die erste zuerst, dann nach unten weg. */
+.row:nth-of-type(2) { animation-delay: .04s; }
+.row:nth-of-type(3) { animation-delay: .08s; }
+.row:nth-of-type(4) { animation-delay: .12s; }
+.row:nth-of-type(n+5) { animation-delay: .16s; }
+
+/* Kacheln heben sich beim Überfahren leicht an. */
+.card .art { transition: transform .2s cubic-bezier(.32,.72,0,1), box-shadow .2s ease; }
+.card:hover .art { transform: scale(1.04); box-shadow: 0 10px 24px rgba(0,0,0,.5); }
.pad-top { padding-top: calc(84px + env(safe-area-inset-top)); }
.spinner {
width: 26px; height: 26px; border: 2px solid var(--separator);
@@ -533,10 +566,28 @@ function buildIsland() {
? ''
// Rechteck mit Spalte links — die Leiste zieht an die Seite.
: '';
- nav.onclick = () => {
+ // Erst wegklappen, dann umziehen, dann zurückkommen. Ohne die Pause
+ // dazwischen tauscht der Browser die Leiste in einem Bild aus, und vom
+ // Umzug ist nichts zu sehen.
+ nav.onclick = async () => {
+ const chrome = el('chrome');
+ const quick = matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+ if (!quick) {
+ chrome.classList.add('moving');
+ await new Promise((r) => setTimeout(r, 210));
+ }
+
setPref('nav', P.nav === 'side' ? 'top' : 'side');
buildIsland();
- go(current);
+
+ if (!quick) {
+ // Ein Bild abwarten, damit die neue Form ihre Ausgangsstellung
+ // übernimmt, bevor sie hereinfährt — sonst gibt es keinen Weg zu gehen.
+ requestAnimationFrame(() => requestAnimationFrame(() => {
+ el('chrome').classList.remove('moving');
+ }));
+ }
};
isl.appendChild(nav);