Skip to main content

max / goingson

Remove motion: no transitions, no enter or leave choreography Max wants a snappy UI, so the wave 5 compromise is gone. All 18 remaining transitions go, along with the --transition-* scale that fed them, and the three inline snap-back transitions the swipe handlers injected. The drag handlers keep their transition:none, which is now the explicit guarantee that a dragged element tracks the finger rather than a leftover. Enter and leave choreography goes with it: modal fade and slide including the mobile bottom-sheet variants, toast slide, action-sheet slide, the weekly-review slot fill, and the attention pulses on tab badges, status dots and the nudge dot. The JS had to move with the CSS or this would have been slower, not faster. Exit timing was driven by setTimeout values hand-matched to the animation durations, not by animationend, so deleting the CSS alone would have left a modal frozen for 150ms before vanishing. Gone: the 150ms modal close, both 300ms toast removals, the 250ms wait before a completed task leaves state, the 200ms sheet dismiss and the 200ms swipe peek clear. What still moves is only what carries information no static frame can: spinners, skeleton placeholders, the running-timer pulse and the syncing indicator. Four keyframes, down from seventeen. Frontend tests pass at 72.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 00:02 UTC
Signed with PGP, not checked
Commit: fba10880ea70851b1d25db1b3f9c445d48c4baa7
Parent: 26cccf3
5 files changed, +16 insertions, -204 deletions
@@ -346,15 +346,6 @@
346 346 --line-height-normal: 1.5;
347 347 --line-height-relaxed: 1.75;
348 348
349 - /* Transition timing. The scale survives the motion triage on purpose:
350 - state changes are instant under Platinum, but navigational motion
351 - (drawers, sheets, disclosure, progress) still moves through space and
352 - still needs a duration. Only --transition-fast has a consumer today;
353 - normal and slow are kept so the scale stays whole. */
354 - --transition-fast: 0.1s;
355 - --transition-normal: 0.15s;
356 - --transition-slow: 0.3s;
357 -
358 349 /* Modal overlay, derived: mixes --text-primary into transparent. On a dark theme this
359 350 produces a *light* scrim, which is wrong; dark themes should override with their own
360 351 --overlay-color or this should be reworked to use a dedicated --scrim token. */
@@ -1066,15 +1057,9 @@
1066 1057 .task-row {
1067 1058 padding: 0.75rem 1.25rem;
1068 1059 border-bottom: var(--border-width) solid var(--border);
1069 - transition: opacity 0.25s ease, transform 0.25s ease;
1070 1060 cursor: pointer;
1071 1061 }
1072 1062
1073 - .task-row-removing {
1074 - opacity: 0;
1075 - transform: translateX(20px);
1076 - }
1077 -
1078 1063 .task-row:hover {
1079 1064 background-color: var(--surface-overlay);
1080 1065 }
@@ -1457,7 +1442,6 @@
1457 1442 .past-events-toggle::before {
1458 1443 content: '▶';
1459 1444 font-size: 0.7rem;
1460 - transition: transform 0.15s ease;
1461 1445 }
1462 1446
1463 1447 .past-events-section[open] .past-events-toggle::before {
@@ -1693,17 +1677,6 @@
1693 1677 }
1694 1678
1695 1679 /* 18. Toast Notifications */
1696 - @keyframes toastSlideIn {
1697 - from {
1698 - opacity: 0;
1699 - transform: translateY(20px);
1700 - }
1701 - to {
1702 - opacity: 1;
1703 - transform: translateY(0);
1704 - }
1705 - }
1706 -
1707 1680 .toast {
1708 1681 position: fixed;
1709 1682 bottom: var(--space-5);
@@ -1719,13 +1692,6 @@
1719 1692 display: flex;
1720 1693 align-items: center;
1721 1694 gap: var(--space-3);
1722 - animation: toastSlideIn 0.3s ease;
1723 - }
1724 -
1725 - .toast.toast-leaving {
1726 - opacity: 0;
1727 - transform: translateY(10px);
1728 - transition: opacity 0.3s ease, transform 0.3s ease;
1729 1695 }
1730 1696
1731 1697 .toast-info {
@@ -1772,46 +1738,6 @@
1772 1738 }
1773 1739
1774 1740 /* 19. Modals */
1775 - @keyframes modalFadeIn {
1776 - from {
1777 - opacity: 0;
1778 - }
1779 - to {
1780 - opacity: 1;
1781 - }
1782 - }
1783 -
1784 - @keyframes modalSlideIn {
1785 - from {
1786 - opacity: 0;
1787 - transform: translateY(-20px) scale(0.95);
1788 - }
1789 - to {
1790 - opacity: 1;
1791 - transform: translateY(0) scale(1);
1792 - }
1793 - }
1794 -
1795 - @keyframes modalFadeOut {
1796 - from {
1797 - opacity: 1;
1798 - }
1799 - to {
1800 - opacity: 0;
1801 - }
1802 - }
1803 -
1804 - @keyframes modalSlideOut {
1805 - from {
1806 - opacity: 1;
1807 - transform: translateY(0) scale(1);
1808 - }
1809 - to {
1810 - opacity: 0;
1811 - transform: translateY(-20px) scale(0.95);
1812 - }
1813 - }
1814 -
1815 1741 .modal-overlay {
1816 1742 position: fixed;
1817 1743 top: 0;
@@ -1823,17 +1749,12 @@
1823 1749 align-items: center;
1824 1750 justify-content: center;
1825 1751 z-index: 1000;
1826 - animation: modalFadeIn 0.15s ease-out;
1827 1752 }
1828 1753
1829 1754 .modal-overlay.hidden {
1830 1755 display: none;
1831 1756 }
1832 1757
1833 - .modal-overlay.closing {
1834 - animation: modalFadeOut 0.15s ease-in forwards;
1835 - }
1836 -
1837 1758 .modal-container {
1838 1759 background-color: var(--surface-raised);
1839 1760 border: var(--border-width) solid var(--border);
@@ -1849,7 +1770,6 @@
1849 1770 doesn't apply (e.g. iPad landscape > 768px). */
1850 1771 max-height: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 2rem);
1851 1772 overflow: auto;
1852 - animation: modalSlideIn 0.2s ease-out;
1853 1773 }
1854 1774
1855 1775 .modal-container.modal-large {
@@ -1868,10 +1788,6 @@
1868 1788 flex-direction: column;
1869 1789 }
1870 1790
1871 - .modal-overlay.closing .modal-container {
1872 - animation: modalSlideOut 0.15s ease-in forwards;
1873 - }
1874 -
1875 1791 .modal-header {
1876 1792 display: flex;
1877 1793 justify-content: space-between;
@@ -2336,7 +2252,6 @@
2336 2252 .settings-disclosure-toggle::before {
2337 2253 content: '▶';
2338 2254 font-size: 0.7rem;
2339 - transition: transform 0.15s ease;
2340 2255 }
2341 2256 .settings-disclosure[open] > .settings-disclosure-toggle::before {
2342 2257 transform: rotate(90deg);
@@ -2643,7 +2558,6 @@
2643 2558 z-index: 10000;
2644 2559 font-size: var(--font-size-sm);
2645 2560 white-space: nowrap;
2646 - animation: toastSlideIn 0.15s ease;
2647 2561 }
2648 2562 .pending-key-hint-label { opacity: 0.7; }
2649 2563 .pending-key-hint kbd {
@@ -3055,7 +2969,6 @@
3055 2969 color: var(--content-on-action);
3056 2970 pointer-events: none;
3057 2971 opacity: 0;
3058 - transition: opacity 0.1s linear;
3059 2972 z-index: 0;
3060 2973 }
3061 2974 /* Right-swipe reveals what was hidden to the LEFT of the row */
@@ -4347,7 +4260,6 @@
4347 4260 .progress-bar {
4348 4261 height: 100%;
4349 4262 background: var(--success);
4350 - transition: width 0.3s ease;
4351 4263 }
4352 4264
4353 4265 .no-subtasks {
@@ -5563,7 +5475,6 @@
5563 5475 border-left: var(--border-width) solid var(--border);
5564 5476 box-shadow: -4px 0 18px rgba(0, 0, 0, 0.18);
5565 5477 transform: translateX(100%);
5566 - transition: transform 180ms ease-out;
5567 5478 z-index: 60;
5568 5479 display: flex;
5569 5480 flex-direction: column;
@@ -5918,18 +5829,6 @@
5918 5829 border-radius: var(--radius-full);
5919 5830 margin-left: 0.5rem;
5920 5831 vertical-align: middle;
5921 - animation: pulse-badge 2s infinite;
5922 - }
5923 -
5924 - @keyframes pulse-badge {
5925 - 0%, 100% {
5926 - opacity: 1;
5927 - transform: scale(1);
5928 - }
5929 - 50% {
5930 - opacity: 0.6;
5931 - transform: scale(0.8);
5932 - }
5933 5832 }
5934 5833
5935 5834 .tab-status-dot {
@@ -5942,8 +5841,8 @@
5942 5841 }
5943 5842 .tab-status-dot.status-none { display: none; }
5944 5843 .tab-status-dot.status-green { background-color: var(--success); }
5945 - .tab-status-dot.status-yellow { background-color: var(--warning); animation: pulse-badge 2s ease-in-out infinite; }
5946 - .tab-status-dot.status-red { background-color: var(--danger); animation: pulse-badge 1.5s ease-in-out infinite; }
5844 + .tab-status-dot.status-yellow { background-color: var(--warning); }
5845 + .tab-status-dot.status-red { background-color: var(--danger); }
5947 5846
5948 5847 /* 50. Weekly Review V2 - Grid Layout */
5949 5848
@@ -6556,22 +6455,7 @@
6556 6455 grid-template-columns: 1fr;
6557 6456 }
6558 6457
6559 - /* 51. Weekly Review Transitions & Keyboard Navigation */
6560 -
6561 - .focus-slot.filled {
6562 - animation: focusSlotFill 0.3s ease-out;
6563 - }
6564 -
6565 - @keyframes focusSlotFill {
6566 - 0% {
6567 - transform: scale(0.95);
6568 - opacity: 0.7;
6569 - }
6570 - 100% {
6571 - transform: scale(1);
6572 - opacity: 1;
6573 - }
6574 - }
6458 + /* 51. Weekly Review Keyboard Navigation */
6575 6459
6576 6460 /* Keyboard focus states */
6577 6461 .focus-slot:focus-within,
@@ -7243,7 +7127,6 @@
7243 7127 bottom: 2px;
7244 7128 background-color: var(--content-muted);
7245 7129 border-radius: var(--radius-full);
7246 - transition: transform var(--transition-fast);
7247 7130 }
7248 7131
7249 7132 .toggle-switch input:checked + .toggle-slider {
@@ -7343,7 +7226,6 @@
7343 7226 height: 100%;
7344 7227 background: var(--success);
7345 7228 border-radius: var(--radius-full);
7346 - transition: width var(--transition-fast);
7347 7229 }
7348 7230
7349 7231 .milestone-actions {
@@ -7476,7 +7358,6 @@
7476 7358 opacity: 0;
7477 7359 transform: scale(0.92);
7478 7360 transform-origin: 50% 100%;
7479 - transition: opacity 120ms ease-out, transform 120ms ease-out;
7480 7361 }
7481 7362
7482 7363 .mobile-tab-slide-menu.is-open {
@@ -7496,7 +7377,6 @@
7496 7377 letter-spacing: 0.05em;
7497 7378 color: var(--content);
7498 7379 border-radius: var(--radius-sm);
7499 - transition: transform 80ms ease;
7500 7380 }
7501 7381
7502 7382 .mobile-tab-slide-item.is-highlighted {
@@ -7533,7 +7413,6 @@
7533 7413 padding: 0.5rem 1rem calc(0.5rem + env(safe-area-inset-bottom, 0px));
7534 7414 max-height: 60vh;
7535 7415 overflow-y: auto;
7536 - animation: sheetSlideUp 0.25s ease-out;
7537 7416 }
7538 7417
7539 7418 .action-sheet-handle {
@@ -7652,7 +7531,6 @@
7652 7531 .swipe-content {
7653 7532 position: relative;
7654 7533 background: var(--surface-raised);
7655 - transition: transform 0.15s ease;
7656 7534 }
7657 7535
7658 7536 /* --- Pull to Refresh --- */
@@ -7679,21 +7557,6 @@
7679 7557 display: none;
7680 7558 }
7681 7559
7682 - @keyframes sheetSlideUp {
7683 - from { transform: translateY(100%); }
7684 - to { transform: translateY(0); }
7685 - }
7686 -
7687 - @keyframes sheetSlideDown {
7688 - from { transform: translateY(0); }
7689 - to { transform: translateY(100%); }
7690 - }
7691 -
7692 - @keyframes dialFadeIn {
7693 - from { opacity: 0; }
7694 - to { opacity: 1; }
7695 - }
7696 -
7697 7560 /* 59. Mobile Responsive (768px overrides) */
7698 7561
7699 7562 /* --- Safe area padding + room for mobile tab bar --- */
@@ -8026,26 +7889,6 @@
8026 7889 padding: 1rem;
8027 7890 }
8028 7891
8029 - /* Mobile uses different modal animations (slide up/down from bottom rather
8030 - than fade-and-scale). @keyframes can't be scoped; defined globally under
8031 - distinct names and applied via animation-name override below. */
8032 - @keyframes modalSlideInMobile {
8033 - from { transform: translateY(100%); }
8034 - to { transform: translateY(0); }
8035 - }
8036 -
8037 - @keyframes modalSlideOutMobile {
8038 - from { transform: translateY(0); }
8039 - to { transform: translateY(100%); }
8040 - }
8041 -
8042 - .ui-mode-mobile .modal-container {
8043 - animation-name: modalSlideInMobile;
8044 - }
8045 - .ui-mode-mobile .modal-overlay.closing .modal-container {
8046 - animation-name: modalSlideOutMobile;
8047 - }
8048 -
8049 7892 /* --- Toast repositioning for mobile --- */
8050 7893 .ui-mode-mobile .toast,
8051 7894 .ui-mode-mobile .toast-undo {
@@ -8616,7 +8459,6 @@
8616 8459 border-top: var(--border-width) solid var(--border);
8617 8460 box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
8618 8461 padding: 0.5rem 1rem;
8619 - transition: transform 0.2s ease;
8620 8462 }
8621 8463
8622 8464 .timer-widget.hidden {
@@ -8664,7 +8506,6 @@
8664 8506 display: flex;
8665 8507 align-items: center;
8666 8508 justify-content: center;
8667 - transition: opacity 0.3s ease;
8668 8509 }
8669 8510
8670 8511 .focus-overlay.hidden {
@@ -8724,7 +8565,6 @@
8724 8565 height: 100%;
8725 8566 background: var(--action);
8726 8567 border-radius: var(--radius-xs);
8727 - transition: width 1s linear;
8728 8568 }
8729 8569
8730 8570 .focus-task-name {
@@ -8767,13 +8607,11 @@
8767 8607
8768 8608 .time-summary-toggle-icon {
8769 8609 font-size: 0.625rem;
8770 - transition: transform 0.15s ease;
8771 8610 }
8772 8611
8773 8612 .time-summary-body {
8774 8613 padding: 0.5rem;
8775 8614 overflow: hidden;
8776 - transition: max-height 0.2s ease;
8777 8615 max-height: 500px;
8778 8616 }
8779 8617
@@ -9268,7 +9106,6 @@
9268 9106 .progress-fill {
9269 9107 height: 100%;
9270 9108 background: var(--success);
9271 - transition: width 0.3s ease;
9272 9109 }
9273 9110
9274 9111 .progress-bar.over-estimate .progress-fill {
@@ -9291,7 +9128,6 @@
9291 9128 border-radius: var(--radius-full);
9292 9129 margin-left: 0.35rem;
9293 9130 vertical-align: middle;
9294 - animation: pulse-badge 2s infinite;
9295 9131 }
9296 9132
9297 9133 .view-toggle-btn {
@@ -48,21 +48,14 @@
48 48 }
49 49
50 50 /**
51 - * Close the modal dialog with animation
51 + * Close the modal dialog.
52 52 */
53 53 function closeModal() {
54 54 const overlay = document.getElementById('modal-overlay');
55 55
56 - // Add closing class to trigger exit animation
57 - overlay.classList.add('closing');
58 -
59 - // Wait for animation to complete before hiding
60 - setTimeout(() => {
61 - overlay.classList.add('hidden');
62 - overlay.classList.remove('closing');
63 - overlay.setAttribute('aria-hidden', 'true');
64 - releaseFocusTrap();
65 - }, 150); // Match the animation duration
56 + overlay.classList.add('hidden');
57 + overlay.setAttribute('aria-hidden', 'true');
58 + releaseFocusTrap();
66 59 }
67 60
68 61 // Focus trap for modal accessibility
@@ -148,10 +141,7 @@
148 141 document.body.appendChild(toast);
149 142
150 143 const duration = opts.duration || (type === 'error' ? 6000 : 4000);
151 - setTimeout(() => {
152 - toast.classList.add('toast-leaving');
153 - setTimeout(() => toast.remove(), 300);
154 - }, duration);
144 + setTimeout(() => toast.remove(), duration);
155 145 }
156 146
157 147 // Undo Toast
@@ -313,11 +303,10 @@
313 303 }
314 304
315 305 /**
316 - * Remove an undo toast with animation.
306 + * Remove an undo toast.
317 307 */
318 308 function removeUndoToast(toast) {
319 - toast.classList.add('toast-leaving');
320 - setTimeout(() => toast.remove(), 300);
309 + toast.remove();
321 310 }
322 311
323 312 // Confirmation Dialog
@@ -125,8 +125,6 @@
125 125 if (!isDragging || !activeRow) return;
126 126 isDragging = false;
127 127
128 - activeRow.style.transition = 'transform 0.2s ease';
129 -
130 128 if (Math.abs(currentX) >= threshold) {
131 129 if (currentX < 0 && actions?.left?.action) {
132 130 actions.left.action();
@@ -136,8 +134,7 @@
136 134 }
137 135
138 136 activeRow.style.transform = 'translateX(0)';
139 - const row = activeRow;
140 - setTimeout(() => clearPeek(row), 200);
137 + clearPeek(activeRow);
141 138 activeRow = null;
142 139 actions = null;
143 140 currentX = 0;
@@ -796,20 +796,14 @@
796 796 async function complete(id) {
797 797 GoingsOn.cache.invalidate('tasks');
798 798
799 - // Animate row out before removing from state
800 - const row = document.querySelector(`.task-row[data-id="${id}"]`);
801 - if (row) row.classList.add('task-row-removing');
802 -
803 799 const cachedTasks = GoingsOn.state.tasks;
804 800 const removedIndex = cachedTasks.findIndex(t => t.id === id);
805 801 const removedTask = removedIndex > -1 ? cachedTasks[removedIndex] : null;
806 802
807 - // Optimistic surgical removal once the row has animated out. The state
808 - // subscriber refreshes the scroller in place, no full-window refetch
809 - // (ultra-fuzz Run #28 S4). The completed task leaves the pending view.
810 - setTimeout(() => {
811 - GoingsOn.state.set('tasks', GoingsOn.state.tasks.filter(t => t.id !== id));
812 - }, 250);
803 + // Optimistic surgical removal. The state subscriber refreshes the
804 + // scroller in place, no full-window refetch (ultra-fuzz Run #28 S4).
805 + // The completed task leaves the pending view.
806 + GoingsOn.state.set('tasks', GoingsOn.state.tasks.filter(t => t.id !== id));
813 807
814 808 GoingsOn.ui.showUndoToast('Task completed', {
815 809 onConfirm: async () => {
@@ -150,8 +150,6 @@
150 150 if (!isDragging) return;
151 151 isDragging = false;
152 152
153 - element.style.transition = 'transform 0.2s ease';
154 -
155 153 if (Math.abs(currentX) >= threshold) {
156 154 // Fire action
157 155 if (currentX < 0 && config.left?.action) {
@@ -351,11 +349,9 @@
351 349 if (!isDragging) return;
352 350 isDragging = false;
353 351
354 - element.style.transition = 'transform 0.2s ease';
355 -
356 352 if (currentY >= threshold) {
357 353 element.style.transform = `translateY(100%)`;
358 - setTimeout(onDismiss, 200);
354 + onDismiss();
359 355 } else {
360 356 element.style.transform = 'translateY(0)';
361 357 }