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);