Root causes found via Textual internals analysis:
1. Theme freeze — OptionCycler fired refresh_css() on EVERY click (no debounce).
Each click → app.theme = x → _watch_theme → call_next(refresh_css) →
stylesheet.reparse() + _refresh_layout on full widget tree (500+ nodes).
Fix: 150ms set_timer debounce in _apply_theme_debounced; rapid clicks
collapse into a single refresh_css call.
2. Sync file IO in main event loop — cfg.save() (yaml.dump + file.write)
was called directly inside every Settings Save handler, blocking the
Textual event loop for ~5-10ms (longer on slow disks).
Fix: all cfg.save() moved to run_in_executor via _async_save_config worker.
3. Theme double-render — app.theme was set both in _apply_theme AND again
in on_config_changed when ConfigChanged arrived from the observer.
Fix: app.py guards with 'if current != new', screen.py saves config
only on Save button (not on live preview).
4. Timer race on interval change — _setup_auto_save called timer.stop()
then set_interval() in the same dispatch cycle.
Fix: call_after_refresh(_setup_auto_save) + timer.pause() before stop().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>