remove smooth scrolling

Hunter
Wednesday, June 17th, 2026 at 3:58 PM

resources/main.css+50-36
resources/main.js+6-1
resources/multi-select.js -8
resources/navigation.js -8
resources/render.js+87-15
resources/reorganize.js -8
resources/state.js -1
resources/theme.js+7-1

resources/main.css
115  	--icon: '🍬';
116  
117  	.checkbox-label {
    -		border-radius: 10px;
118 +		border-radius: 12px;
119  	}
120  	.active {
    -		border-radius: 30px 0px 0px 30px;
121 +		border-radius: 36px 0px 0px 36px;
122  	}
123  	.custom-checkbox:checked + .checkbox-label::before {
124  		display: none;

151  		text-shadow: none;
152  	}
153  	.active {
    -		border-radius: 30px 0px 0px 30px;
154 +		border-radius: 36px 0px 0px 36px;
155  	}
156  	.active .task-text {
157  		color: var(--bloom);

256  /* interface styling */
257  @keyframes shake {
258  	0% { transform: translateX(0); }
    -	20% { transform: translateX(3px); }
    -	40% { transform: translateX(-3px); }
    -	60% { transform: translateX(3px); }
    -	80% { transform: translateX(-3px); }
259 +	20% { transform: translateX(3.6px); }
260 +	40% { transform: translateX(-3.6px); }
261 +	60% { transform: translateX(3.6px); }
262 +	80% { transform: translateX(-3.6px); }
263  	100% { transform: translateX(0); }
264  }
265  .shake {

267  }
268  @keyframes shake-vertical {
269  	0% { transform: translateY(0); }
    -	20% { transform: translateY(3px); }
    -	40% { transform: translateY(-3px); }
    -	60% { transform: translateY(3px); }
    -	80% { transform: translateY(-3px); }
270 +	20% { transform: translateY(3.6px); }
271 +	40% { transform: translateY(-3.6px); }
272 +	60% { transform: translateY(3.6px); }
273 +	80% { transform: translateY(-3.6px); }
274  	100% { transform: translateY(0); }
275  }
276  .shake-vertical {
277  	animation: shake-vertical 0.25s ease-out;
278  }
279  html {
    -	scroll-behavior: smooth;
280  	overflow-y: scroll;
281  	overflow-x: hidden;
282  	scrollbar-width: auto;
283  	scrollbar-color: color-mix(in srgb, var(--text) 75%, transparent) color-mix(in srgb, var(--background) 75%, transparent);
284  }
285  body {
    -	zoom: 1.2;
286 +	font-size: 19.2px;
287  	font-family: Arial, sans-serif;
    -	max-width: 800px;
288 +	max-width: 960px;
289  	margin: 0 auto;
    -	padding: 0px 20px 20px 20px;
290 +	padding: 0px 24px 24px 24px;
291  	background-color: var(--background);
292  	color: var(--text);
293  }
294  ul {
295  	list-style-type: none;
    -	padding-left: 20px;
296 +	padding-left: 24px;
297  }
298  .task-text {
299  	font-size: 1.17em;
300  	font-weight: bold;
301  	flex: 1;
302  	min-width: 0;
    -	margin-left: 10px;
    -	padding: 5px 5px 5px 0;
303 +	margin-left: 12px;
304 +	padding: 6px 6px 6px 0;
305  	color: var(--text);
306  	overflow: hidden;
307  	text-overflow: ellipsis;

324  .task-container {
325  	display: flex;
326  	align-items: center;
    -	margin: 5px 0;
    -	padding-left: 11px;
    -	padding-right: 11px;
    -	padding-top: 4px;
    -	padding-bottom: 4px;
327 +	margin: 6px 0;
328 +	box-sizing: border-box;
329 +	padding-left: 13.2px;
330 +	padding-right: 13.2px;
331 +	padding-top: 4.8px;
332 +	padding-bottom: 4.8px;
333  	position: relative;
334  }
335 +#subtasks-container .task-container {
336 +	height: var(--row-h, auto);
337 +}
338  /* the native caret is hidden app-wide; simulated carets render identically
339     across OSes and between single- and multi-select editing */
340  .task-text {

367  	top: 0;
368  	background-color: var(--background);
369  	z-index: 1;
    -	padding-top: 20px;
370 +	padding-top: 24px;
371 +}
372 +/* opaque strip covering the fixed margin gap below the header so subtasks
373 +   scrolling under it stay hidden */
374 +.sticky-header::after {
375 +	content: '';
376 +	position: absolute;
377 +	left: 0;
378 +	right: 0;
379 +	top: 100%;
380 +	height: 16px;
381 +	background-color: var(--background);
382  }
383  .parent-task {
384  	font-size: 1.5em;
385  	font-weight: bold;
386  }
387  #breadcrumbs {
    -	font-size: 24px;
    -	margin-bottom: 10px;
388 +	font-size: 28.8px;
389 +	margin-bottom: 12px;
390  	color: var(--text);
391  	user-select: none;
392  }

395  }
396  .checkbox-label {
397  	display: inline-block;
    -	width: 20px;
    -	height: 20px;
398 +	width: 24px;
399 +	height: 24px;
400  	background-color: var(--background);
    -	border: 2px solid var(--accent);
401 +	border: 2.4px solid var(--accent);
402  	position: relative;
403  	cursor: pointer;
404  	box-sizing: border-box;
    -	border-radius: 4px;
405 +	border-radius: 4.8px;
406  }
407  .custom-checkbox:checked + .checkbox-label {
408  	background-color: var(--accent);

410  .custom-checkbox:checked + .checkbox-label::before {
411  	content: '';
412  	position: absolute;
    -	left: 6px;
    -	top: 2px;
    -	width: 5px;
    -	height: 10px;
413 +	left: 7.2px;
414 +	top: 2.4px;
415 +	width: 6px;
416 +	height: 12px;
417  	border: solid var(--background);
    -	border-width: 0 2px 2px 0;
418 +	border-width: 0 2.4px 2.4px 0;
419  	transform: rotate(45deg);
420  	box-sizing: border-box;
421  }

425  	left: 25%;
426  	right: 25%;
427  	top: 50%;
    -	height: 2px;
428 +	height: 2.4px;
429  	background-color: var(--accent);
430  	transform: translateY(-50%);
431  }
resources/main.js
132  	// re-pin the active caret's scroll once layout settles and once fonts load
133  	requestAnimationFrame(rescrollActiveCaret);
134  	if (document.fonts && document.fonts.ready) {
    -		document.fonts.ready.then(rescrollActiveCaret);
135 +		document.fonts.ready.then(() => {
136 +			// custom fonts can change row height, so re-snap the grid and scroll
137 +			alignRowHeights();
138 +			recenterActiveTask();
139 +			rescrollActiveCaret();
140 +		});
141  	}
142  });
resources/multi-select.js
365  
366  	var refocusId = getFocusedSelectedId();
367  
    -	if (navigate) {
    -		document.documentElement.style.scrollBehavior = 'auto';
    -	}
    -
368  	var subtasks = state.currentTask.subtasks;
369  	var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b);
370  	var topIndex = indices[0];

415  
416  	var refocusId = getFocusedSelectedId();
417  
    -	if (navigate) {
    -		document.documentElement.style.scrollBehavior = 'auto';
    -	}
    -
418  	var currentParent = state.currentTask;
419  	var grandParent = state.taskPath[state.taskPath.length - 2];
420  	if (!grandParent) return false;
resources/navigation.js
30  }
31  
32  function navigateIntoSubtask(subtask) {
   -	document.documentElement.style.scrollBehavior = 'auto';
   -
33  	if (subtask.subtasks.length > 0) {
34  		state.currentTask.selectedSubtaskId = subtask.id;
35  		state.taskPath.push(subtask);

52  }
53  
54  function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) {
   -	document.documentElement.style.scrollBehavior = 'auto';
   -
55  	state.currentTask.selectedSubtaskId = targetTask.id;
56  
57  	state.taskPath.push(targetTask);

71  			applyShakeAnimation(taskId);
72  		}
73  	} else if (state.taskPath.length > 1) {
   -		document.documentElement.style.scrollBehavior = 'auto';
   -
74  		var currentTaskId = state.currentTask.id;
75  		state.taskPath.pop();
76  		state.currentTask = state.taskPath[state.taskPath.length - 1];

93  		renderCurrentView();
94  		selectAndFocusTask(targetTask);
95  	} else if (state.taskPath.length > 1) {
   -		document.documentElement.style.scrollBehavior = 'auto';
   -
96  		state.taskPath.pop();
97  		state.currentTask = state.taskPath[state.taskPath.length - 1];
98  		updateBreadcrumbs(state.currentTask);
resources/render.js
216  	}
217  	var containerRect = container.getBoundingClientRect();
218  	var textRect = textEl.getBoundingClientRect();
    -	// client rects include the body zoom but style px get re-zoomed, so unscale
219 +	// rounding factor between client rects and style px (1 unless the box is scaled)
220  	var scale = container.offsetWidth ? containerRect.width / container.offsetWidth : 1;
221  	var x = textRect.left;
222  	var top = textRect.top;

243  	}
244  	// keep the caret inside the line's visible text box
245  	x = Math.max(textRect.left, Math.min(x, textRect.right - 1));
    -	// snap to whole zoomed pixels and size to exactly one, so the caret
    -	// never straddles a pixel boundary and fattens to 2px
246 +	// snap to whole pixels and size to exactly one, so the caret never
247 +	// straddles a pixel boundary and fattens to 2px
248  	caretEl.style.left = (Math.round(x - containerRect.left) / scale) + 'px';
249  	caretEl.style.top = ((top - containerRect.top) / scale) + 'px';
250  	caretEl.style.height = (height / scale) + 'px';

294  	state.lastSubtaskShiftDownReleased = isLastSubtask(bottomTask);
295  	input.focus();
296  
    -	// Center the active task in the viewport
    -	var activeTaskElement = input.closest('.task-container');
    -	if (activeTaskElement && activeTaskElement.parentElement && activeTaskElement.parentElement.tagName === 'LI') {
    -		activeTaskElement.scrollIntoView({
    -			behavior: 'auto',
    -			block: 'center',
    -			inline: 'nearest'
    -		});
    -
    -		setTimeout(() => {
    -			document.documentElement.style.scrollBehavior = 'smooth';
    -		}, 200);
297 +	centerActiveTask(input.closest('.task-container'));
298 +}
299 +
300 +// Vertical distance between consecutive subtask rows (one grid slot).
301 +function getRowStride() {
302 +	var rows = document.querySelectorAll('#subtasks-container li');
303 +	if (!rows.length) return 0;
304 +	return rows.length >= 2
305 +		? rows[1].getBoundingClientRect().top - rows[0].getBoundingClientRect().top
306 +		: rows[0].getBoundingClientRect().height;
307 +}
308 +
309 +// Scroll by whole row strides so the active subtask stays on a fixed slot grid
310 +// (no drift, no smooth animation): centered when possible, free at the top and
311 +// bottom where the scroll clamps. The parent task lives in the sticky header, so
312 +// selecting it returns to the top.
313 +function centerActiveTask(activeEl) {
314 +	if (!activeEl) return;
315 +	var inList = activeEl.parentElement && activeEl.parentElement.tagName === 'LI';
316 +	if (!inList) {
317 +		window.scrollTo(0, 0);
318 +		return;
319 +	}
320 +	var stride = getRowStride();
321 +	if (stride <= 0) return;
322 +	// pad the bottom so max scroll is a whole stride, else it sits off-grid
323 +	// (shifting every slot) or is unreachable
324 +	updateBottomSpacer(stride);
325 +	var rect = activeEl.getBoundingClientRect();
326 +	var center = rect.top + rect.height / 2 + window.scrollY - window.innerHeight / 2;
327 +	var snapped = Math.round(center / stride) * stride;
328 +	var maxScroll = document.documentElement.scrollHeight - window.innerHeight;
329 +	var target = Math.max(0, Math.min(snapped, maxScroll));
330 +	// snap to a device pixel so the browser doesn't re-round the scroll by a
331 +	// varying amount each step, which makes rows wiggle
332 +	var dpr = window.devicePixelRatio || 1;
333 +	window.scrollTo(0, Math.round(target * dpr) / dpr);
334 +}
335 +
336 +// Re-snap the scroll to the current row grid (e.g. after a theme change resizes
337 +// the rows), so slots stay aligned without waiting for the next navigation.
338 +function recenterActiveTask() {
339 +	var el = document.querySelector('.task-container.active .task-text');
340 +	var focused = document.activeElement;
341 +	if (focused && focused.classList && focused.classList.contains('task-text')) el = focused;
342 +	if (el) centerActiveTask(el.closest('.task-container'));
343 +}
344 +
345 +// Nudge each subtask row's height up to the next whole device pixel (at most a
346 +// ~1px bump) so the row stride lands on the device grid and rows don't jitter as
347 +// we scroll; the rows' 6px margins are already whole pixels.
348 +function alignRowHeights() {
349 +	var container = document.getElementById('subtasks-container');
350 +	if (!container) return;
351 +	var row = container.querySelector('.task-container');
352 +	if (!row) return;
353 +	container.style.removeProperty('--row-h');
354 +	var height = row.getBoundingClientRect().height;
355 +	if (!height) return;
356 +	var dpr = window.devicePixelRatio || 1;
357 +	container.style.setProperty('--row-h', Math.ceil(height * dpr) / dpr + 'px');
358 +}
359 +
360 +// Grow a trailing spacer so the document's max scroll is a whole number of row
361 +// strides (keeps the bottom on the slot grid and reachable) plus a little empty
362 +// space below the last row.
363 +function updateBottomSpacer(stride) {
364 +	var container = document.getElementById('subtasks-container');
365 +	if (!container) return;
366 +	var spacer = document.getElementById('bottom-spacer');
367 +	if (!spacer) {
368 +		spacer = document.createElement('div');
369 +		spacer.id = 'bottom-spacer';
370 +		container.appendChild(spacer);
371 +	}
372 +	var baseScrollHeight = document.documentElement.scrollHeight - spacer.getBoundingClientRect().height;
373 +	var baseMaxScroll = baseScrollHeight - window.innerHeight;
374 +	if (baseMaxScroll <= 0) {
375 +		spacer.style.height = '0px';
376 +		return;
377  	}
378 +	var gap = 16; // breathing room below the last row
379 +	var target = Math.ceil((baseMaxScroll + gap) / stride) * stride;
380 +	spacer.style.height = (target - baseMaxScroll) + 'px';
381  }
382  
383  function updateBreadcrumbs(selectedTask) {

466  	});
467  	subtasksContainer.appendChild(subtasksList);
468  	state.appContainer.appendChild(subtasksContainer);
469 +	alignRowHeights();
470  
471  	updateBreadcrumbs(state.currentTask);
472  	updatePageTitle(state.currentTask);
resources/reorganize.js
28  function pushSubtaskIntoTarget(subtask, direction, navigate = false) {
29  	if (subtask === state.currentTask) return false;
30  
   -	if (navigate) {
   -		document.documentElement.style.scrollBehavior = 'auto';
   -	}
   -
31  	var parentTask = findParentTask(subtask);
32  	if (!parentTask) return false;
33  

66  	if (subtask === state.currentTask) return false;
67  	if (state.taskPath.length <= 1) return false;
68  
   -	if (navigate) {
   -		document.documentElement.style.scrollBehavior = 'auto';
   -	}
   -
69  	var currentParent = findParentTask(subtask);
70  	if (!currentParent) return;
71  
resources/state.js
11  	currentThemeIndex: 0,
12  	isF2Pressed: false,
13  	themes: ['gak', 'swamp', 'sunflower', 'harvest', 'sugar', 'vineyard', 'woodstove', 'medieval', 'goblin'],
   -	isInWheelEvent: false,
14  	windowFocused: true,
15  
16  	// Multi-select state
resources/theme.js
24  	var backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
25  	document.getElementById('themeColor').setAttribute('content', backgroundColor);
26  	updateFavicon();
27 +	alignRowHeights();
28 +	recenterActiveTask();
29  	rescrollActiveCaret();
30  	if (document.fonts && document.fonts.ready) {
   -		document.fonts.ready.then(rescrollActiveCaret);
31 +		document.fonts.ready.then(() => {
32 +			alignRowHeights();
33 +			recenterActiveTask();
34 +			rescrollActiveCaret();
35 +		});
36  	}
37  }
38