diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8f4f92..25aba2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: - run: cargo check --all-features check-msrv: - name: Check (MSRV 1.81) + name: Check (MSRV 1.88) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.81" + toolchain: "1.88" - uses: Swatinem/rust-cache@v2 - run: cargo generate-lockfile - run: cargo check --features async,serde diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7b389a..d9e133e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,13 +29,13 @@ jobs: - run: cargo fmt -- --check ci-msrv: - name: CI Gate (MSRV 1.81) + name: CI Gate (MSRV 1.88) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.81" + toolchain: "1.88" - uses: Swatinem/rust-cache@v2 - run: cargo generate-lockfile - run: cargo check --features async,serde diff --git a/AGENTS.md b/AGENTS.md index a20e686..554b0cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ cargo deny check |-----|---------|---------------| | Format | `cargo fmt -- --check` | Yes | | Check (stable) | `cargo check --all-features` | Yes | -| Check (MSRV 1.81) | `cargo check --features async,serde` (toolchain 1.81) | Yes | +| Check (MSRV 1.88) | `cargo check --features async,serde` (toolchain 1.88) | Yes | | Clippy | `cargo clippy --all-features -- -D warnings` | Yes | | Test | `cargo test --all-features` | Yes | | Typos | `typos` | Yes | diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f91b17..c8541bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,112 @@ # Changelog +## [0.22.0] - 2026-06-11 + +Rust modernization and optimization release. No new public widgets or features — +this cycle migrates the crate to the 2024 edition, adopts let-chains across the +core, tightens the lint posture, removes a dead dependency, memoizes layout +intrinsic sizing, and fixes a measured rendering regression. The public API +surface is unchanged. + +### Changed + +- **Edition 2024** — both `superlighttui` and `slt-wasm` now build on the 2024 + edition. +- **MSRV 1.85 → 1.88** — the core adopts let-chains (`if let … && let …`, + flattening ~50 nested if-let sites across the codebase), stabilized in Rust + 1.88.0. The CI MSRV gate and docs are pinned to 1.88 accordingly. (MSRV bumps + are permitted in minor releases per the MSRV policy.) +- **Workspace lints** — shared `[workspace.lints]` config (`rust_2018_idioms`, + rustdoc broken/private intra-doc links), opted into by both packages via + `[lints] workspace = true`. Library-only hygiene lints (`missing_docs`, + `unreachable_pub`, clippy `unwrap_in_result`/`unwrap_used`/`dbg_macro`/ + `print_stdout`/`print_stderr`, and the cfg-conditional `unsafe_code` policy) + stay as lib-target inner attributes in both `lib.rs` files, since `[lints]` + is package-scoped and would otherwise fire on examples and integration tests. + `cargo clippy --all-features --all-targets -- -D warnings` is now clean. +- **`unicode-segmentation` bound relaxed** — `>=1.0, <1.13` → `1`; the upper + bound only existed to keep the dropped 1.81 MSRV gate green. +- **Error impls on `core::error::Error`** — `ColorParseError` and + `ThemeLoadError` now implement `core::error::Error` (drop-in; `std::error:: + Error` is the same trait re-exported). +- **`OnceLock` → `LazyLock`** where initialization is a plain constant + (`SEP_LINE`); fallible tree-sitter configs stay on `OnceLock`. + +### Removed + +- **`indexmap` direct dependency** — it had zero uses in `src/`; the manifest + entry existed only as an MSRV-compatibility version pin for the transitive + copy pulled by `toml` (`serde`/`theme-watch` features). Resolved dependency + tree shrinks from 13 → 10 crates with `default-features = false` and + 28 → 25 with default features (vs 68 for a ratatui + crossterm app). + +### Fixed + +- **Startup deadlock on terminals that never answer capability probes** — the + DA1/DA2, Kitty-graphics, XTGETTCAP, DECRQM, and OSC reply readers gated a + blocking `stdin` read behind `crossterm::event::poll()`, which reports + crossterm's *internal event queue*, not raw-descriptor readability — and + crossterm's poller consumes bytes from the same descriptor. On hosts that + answer nothing (detached tmux panes, `script`/CI PTY wrappers), applications + hung forever on a blank alternate screen before the first frame, and typed + keys were swallowed instead of unblocking the read. Replies are now pumped + through a dedicated reader thread and awaited with `recv_timeout`, so every + probe is hard-bounded by its documented budget (≤180 ms total at startup) + regardless of host behavior. Residual on fully silent hosts: the parked + reader can swallow at most one byte of typeahead — bounded, versus the + previous unbounded hang. Affects `run()` startup, `detect_color_scheme`, + `read_clipboard`, and the Kitty cell-size query. + +### Perf + +- **Layout intrinsic-size memoization** — `min_width` / `min_height` / + `min_height_for_width` recursed over the entire subtree at every ancestor + level each frame (O(nodes × depth)). They are now memoized per node per + frame via `Cell` fields on `LayoutNode`, invalidated at the + `resolve_axis_specs` points (row and column passes) where a Pct/Ratio + constraint resolves mid-layout. + Output is byte-identical (regression tests + full proptest suite); a new + `layout_deep_tree_120x40` bench (14-level nested panels) tracks the + asymptotic win. +- **`dim_buffer_around` / `dim_entire_buffer` rewrite** — the modal dimming + strips now OR the `DIM` modifier over contiguous buffer-row slices instead + of per-cell `get_mut` with bounds asserts. All `v020_perf_audit` dim arms + improved (small modal 61.9 → 53.5 µs, large modal 59.2 → 48.2 µs, full scan + 106.4 → 55.0 µs at 200×60 on the reference machine), fixing a measured + regression from the original strip optimization (#228). +- Audited the double-buffer line-hash path for redundant re-hashing of the + previous buffer: the existing `line_dirty` tracking already makes the second + `recompute_line_hashes` call near-free in steady state (verified by + `flush/static_200x60` at ~114 ns); no change needed. + +### Docs + +- **Lightness claims corrected to measured reality** — the "Dependencies: 2" + claim in `COMPETITIVE_ANALYSIS.md` was false and is replaced with measured + numbers (4 direct required deps; 25 resolved with default features vs 68 for + ratatui + crossterm). README taglines (all languages), `FEATURES.md`, and + `DESIGN_PRINCIPLES.md` now frame "light" as dependency footprint + API + surface, and explicitly do not claim a smaller stripped binary (a minimal + SLT hello is ~1.45× ratatui's) or faster cold build. +- **docs.rs feature badges** — 36 `#[cfg_attr(docsrs, doc(cfg(feature = + "…")))]` annotations added so every public feature-gated item (entry points, + `PtyBackend`, `TaskHandle`, `ThemeWatcher`, syntax/image/qrcode widgets, …) + shows its required feature on docs.rs. +- `#[must_use]` added to `ThemeBuilder` and `EventBuilder` (the remaining + builders and `Response` already carried it). + +### Internal + +- Edition-2024 fallout: `std::env::set_var` is now `unsafe`, so the + `#![forbid(unsafe_code)]` policy became cfg-conditional — `forbid` for every + shipping build, `deny` with two audited, mutex-serialized exemptions in + `#[cfg(test)]` env helpers. The published library remains 100% safe code. +- RPIT precise-capture (`+ use<>`) on `Rect::rows()` / `Rect::positions()`; + `expr_2021` → `expr` macro fragment in the internal `flush_run!` macro. +- 17 never-public `Terminal` / `InlineTerminal` / `KittyImageManager` / + `SelectionState` methods downgraded `pub` → `pub(crate)` (flagged by + `unreachable_pub`; none were reachable outside the crate). + ## [0.21.1] - 2026-05-30 Interaction-signal completeness, API ergonomics, WASM parity, perf, and diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a7c4a7..16bd35a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,7 +124,7 @@ git push --tags ``` The release workflow (`.github/workflows/release.yml`) will: -1. Run full CI (check, test, clippy, fmt) on stable + MSRV 1.81 +1. Run full CI (check, test, clippy, fmt) on stable + MSRV 1.88 2. Verify tag matches `Cargo.toml` version 3. Publish to crates.io 4. Create GitHub Release with notes extracted from CHANGELOG.md diff --git a/Cargo.lock b/Cargo.lock index b4dde7e..2b41b97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -463,6 +463,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -498,13 +504,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", + "serde_core", ] [[package]] @@ -1159,7 +1166,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slt-wasm" -version = "0.21.1" +version = "0.22.0" dependencies = [ "js-sys", "superlighttui", @@ -1187,14 +1194,13 @@ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" [[package]] name = "superlighttui" -version = "0.21.1" +version = "0.22.0" dependencies = [ "compact_str", "criterion", "crossterm", "flate2", "image", - "indexmap", "insta", "notify", "proptest", @@ -1544,9 +1550,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -1677,7 +1683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.1", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "semver", ] diff --git a/Cargo.toml b/Cargo.toml index a568a24..ae786b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,30 @@ [workspace] members = [".", "crates/slt-wasm"] +# Shared lint configuration applied to every workspace member via +# `[lints] workspace = true`. NOTE: `[lints]` is package-scoped, so these +# apply to ALL targets in a package (lib, examples, benches, tests), not just +# the library. Library-API hygiene lints are therefore deliberately NOT listed +# here and stay as lib-only inner attributes in src/lib.rs (mirrored in +# crates/slt-wasm/src/lib.rs): `missing_docs`, `unreachable_pub`, and the +# clippy unwrap/print/dbg family — example binaries and integration tests +# legitimately print to stdout and unwrap, so package-wide enforcement would +# flood them with warnings. The cfg-conditional `unsafe_code` policy (forbid +# in shipping builds, deny under #[cfg(test)] because edition 2024 made env +# mutation unsafe) likewise cannot be expressed here and stays in src/lib.rs. +[workspace.lints.rust] +# rust-2018-idioms is a lint group; priority -1 lets individual lints below +# override members of the group if needed. +rust_2018_idioms = { level = "warn", priority = -1 } + +[workspace.lints.rustdoc] +broken_intra_doc_links = "warn" +private_intra_doc_links = "warn" + [package] name = "superlighttui" -version = "0.21.1" -edition = "2021" +version = "0.22.0" +edition = "2024" description = "Super Light TUI - A lightweight, ergonomic terminal UI library" license = "MIT" repository = "https://github.com/subinium/SuperLightTUI" @@ -13,7 +33,9 @@ documentation = "https://docs.rs/superlighttui" readme = "README.md" keywords = ["tui", "terminal", "cli", "ui", "immediate-mode"] categories = ["command-line-interface"] -rust-version = "1.81" +# let-chains (`if let … && let …`) used across the core were stabilized in +# Rust 1.88.0; edition 2024 alone (1.85) is not enough. MSRV is therefore 1.88. +rust-version = "1.88" # Keep the published crate to library + docs only. The agent/skill scaffolding, # VHS recordings, and dev/CI config are not needed by downstream consumers # (they were leaking into 0.21.0's tarball — ~140KB of non-library content). @@ -41,20 +63,17 @@ exclude = [ # v0.20 release notes for why these were merged into tours. autoexamples = false +[lints] +workspace = true + [lib] name = "slt" [dependencies] crossterm = { version = "0.28", features = ["bracketed-paste"], optional = true } unicode-width = "0.2" -# Upper-bounded for MSRV 1.81: unicode-segmentation 1.13+ requires rustc 1.85. -unicode-segmentation = ">=1.0, <1.13" +unicode-segmentation = "1" smallvec = "1" -# MSRV 1.81 pin only (not used directly): a transitive dep via `toml` -# (the `serde`/`theme-watch` features). indexmap 2.10+ requires the -# `edition2024` Cargo feature (rustc 1.85), which breaks the MSRV gate's -# `cargo generate-lockfile` + 1.81 check. Bound it below that. -indexmap = ">=2.0, <2.10" tokio = { version = "1", features = ["rt", "sync", "macros", "time"], optional = true } serde = { version = "1", features = ["derive"], optional = true } toml = { version = "0.8", optional = true } diff --git a/README.md b/README.md index 46c548a..8324c84 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # SuperLightTUI -**Superfast** to write. **Superlight** to run. +**Superfast** to write. **Superlight** dependency tree (4 direct required deps, 25 resolved with default features vs 68 for ratatui + crossterm). [![Crate Badge]][Crate] [![Docs Badge]][Docs] @@ -57,7 +57,7 @@ fn main() -> std::io::Result<()> { 5 lines. No `App` trait. No `Model`/`Update`/`View`. No manual event loop. Ctrl+C just works. -MSRV: Rust 1.81. Default features enable the `crossterm` backend. +MSRV: Rust 1.88. Default features enable the `crossterm` backend. ## 60-Second Grammar diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index ff09a2a..a2c14a8 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -1,4 +1,4 @@ -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; use slt::buffer::Buffer; use slt::rect::Rect; use slt::style::Style; @@ -77,6 +77,57 @@ fn bench_layout_nested(c: &mut Criterion) { }); } +/// Deep-tree layout stress (release-mod-only relevance): 14 levels of +/// alternating bordered row/col panels, each level wrapping the next plus a +/// text leaf, on a 120x40 buffer. The existing `layout_nested_rows_cols` bench +/// is only depth 2-3, so it cannot observe the super-linear cost of the +/// top-down intrinsic-size recomputation (`min_width` / `min_height` / +/// `min_height_for_width` re-walking the same subtrees at every flex call +/// site). This bench drives a deliberately deep chain so that O(nodes x depth) +/// behavior shows up in the sample. +const DEEP_TREE_DEPTH: usize = 14; + +fn render_deep_tree(ui: &mut slt::Context, depth: usize) { + if depth == 0 { + ui.text("leaf"); + return; + } + // Alternate row/col at each level and wrap each panel in a border so the + // intrinsic-size queries traverse the full inset/padding/constraint path. + // Each level also carries a few sibling text leaves next to the recursive + // child: the extra siblings widen the tree so the parent's repeated + // `min_width` / `min_height` sweeps re-walk a non-trivial subtree at every + // level — the O(nodes x depth) pathology the memo targets. + if depth.is_multiple_of(2) { + let _ = ui.bordered(slt::Border::Single).row(move |ui| { + ui.text(format!("L{depth}")); + ui.text("a"); + ui.text("bb"); + render_deep_tree(ui, depth - 1); + }); + } else { + let _ = ui.bordered(slt::Border::Single).col(move |ui| { + ui.text(format!("L{depth}")); + ui.text("a"); + ui.text("bb"); + render_deep_tree(ui, depth - 1); + }); + } +} + +fn bench_layout_deep_tree(c: &mut Criterion) { + c.bench_function("layout_deep_tree_120x40", |b| { + let mut backend = TestBackend::new(120, 40); + b.iter(|| { + backend.render(|ui| { + let _ = ui.col(|ui| { + render_deep_tree(ui, black_box(DEEP_TREE_DEPTH)); + }); + }); + }); + }); +} + fn bench_full_render(c: &mut Criterion) { c.bench_function("full_render_120x40", |b| { let mut backend = TestBackend::new(120, 40); @@ -444,7 +495,7 @@ fn fill_realistic(buf: &mut Buffer, seed: u32) { let mut style = row_style; // Every 17th cell flips fg (single-cell break in the run). - if (x.wrapping_add(y.wrapping_mul(7)) + seed) % 17 == 0 { + if (x.wrapping_add(y.wrapping_mul(7)) + seed).is_multiple_of(17) { style = style.fg(colors[((x + seed) as usize) % colors.len()]); } // Every 31st cell toggles bold (modifier-only change). @@ -452,7 +503,7 @@ fn fill_realistic(buf: &mut Buffer, seed: u32) { style.modifiers |= Modifiers::BOLD; } // Every 53rd cell toggles underline. - if (x + y + seed) % 53 == 0 { + if (x + y + seed).is_multiple_of(53) { style.modifiers |= Modifiers::UNDERLINE; } @@ -467,7 +518,7 @@ fn fill_realistic(buf: &mut Buffer, seed: u32) { // One hyperlink span per few rows (8 cells) to exercise OSC 8. // Use the public `set_string_linked` helper so we never touch // `Cell::hyperlink`'s CompactString type directly from the bench. - if (y + seed) % 4 == 0 && width >= 8 { + if (y + seed).is_multiple_of(4) && width >= 8 { let start = ((y * 7) + seed) % (width - 7); buf.set_string_linked(start, y, "linkcell", row_style, "https://example.com/bench"); } @@ -810,6 +861,7 @@ criterion_group!( bench_buffer_diff, bench_layout_simple, bench_layout_nested, + bench_layout_deep_tree, bench_full_render, bench_full_render_dims, bench_animation_churn, diff --git a/crates/slt-wasm/Cargo.toml b/crates/slt-wasm/Cargo.toml index a1a91e4..cd6b63c 100644 --- a/crates/slt-wasm/Cargo.toml +++ b/crates/slt-wasm/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "slt-wasm" -version = "0.21.1" -edition = "2021" +version = "0.22.0" +edition = "2024" +rust-version = "1.88" description = "WASM/browser backend for SuperLightTUI" license = "MIT" repository = "https://github.com/subinium/SuperLightTUI" @@ -9,7 +10,7 @@ homepage = "https://github.com/subinium/SuperLightTUI" documentation = "https://docs.rs/slt-wasm" [dependencies] -superlighttui = { version = "0.21.1", path = "../..", default-features = false } +superlighttui = { version = "0.22.0", path = "../..", default-features = false } wasm-bindgen = "0.2" web-sys = { version = "0.3", features = [ "CssStyleDeclaration", @@ -27,3 +28,6 @@ web-sys = { version = "0.3", features = [ "console", ] } js-sys = "0.3" + +[lints] +workspace = true diff --git a/crates/slt-wasm/src/lib.rs b/crates/slt-wasm/src/lib.rs index ee74dd1..86a9aea 100644 --- a/crates/slt-wasm/src/lib.rs +++ b/crates/slt-wasm/src/lib.rs @@ -1,3 +1,19 @@ +//! WASM/browser backend for SuperLightTUI. +//! +//! Renders an SLT [`Context`] into a grid of `` elements inside a host +//! container and drives it from `requestAnimationFrame`, translating DOM +//! keyboard/mouse/wheel/resize/paste events into SLT [`Event`]s. + +// Mirror the library-only hygiene lints kept out of [workspace.lints]; this +// crate has no example targets so they apply cleanly to its single lib. +#![warn(missing_docs)] +#![warn(unreachable_pub)] +#![deny(clippy::unwrap_in_result)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::dbg_macro)] +#![warn(clippy::print_stdout)] +#![warn(clippy::print_stderr)] + use std::cell::RefCell; use std::io; use std::rc::Rc; @@ -6,14 +22,16 @@ use slt::{ AppState, Backend, Buffer, Color, Context, Event, KeyCode, KeyModifiers, Modifiers, MouseButton, MouseEvent as SltMouseEvent, MouseKind, Rect, RunConfig, }; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; use web_sys::{Document, HtmlElement, HtmlPreElement, KeyboardEvent, MouseEvent, Window}; /// Shared, re-entrant handle to the `requestAnimationFrame` callback so the /// closure can schedule its own next tick by name. type RafHandle = Rc>>>; +/// SLT [`Backend`] that paints into a DOM `
`/`` grid and diffs
+/// against the previously flushed frame so only changed cells are rewritten.
 pub struct DomBackend {
     buffer: Buffer,
     /// Snapshot of the buffer as it was last flushed to the DOM. Used to diff
@@ -28,6 +46,8 @@ pub struct DomBackend {
 }
 
 impl DomBackend {
+    /// Create a backend that renders a `width`×`height` cell grid into
+    /// `container`. The DOM grid is built lazily on the first flush.
     pub fn new(container: HtmlElement, width: u32, height: u32) -> Self {
         Self {
             buffer: Buffer::empty(Rect::new(0, 0, width, height)),
@@ -478,11 +498,7 @@ fn paste_event_text(target: &JsValue) -> Option {
         .call1(&clipboard_data, &JsValue::from_str("text"))
         .ok()?
         .as_string()?;
-    if text.is_empty() {
-        None
-    } else {
-        Some(text)
-    }
+    if text.is_empty() { None } else { Some(text) }
 }
 
 fn install_event_listeners(
@@ -656,6 +672,17 @@ fn install_event_listeners(
     Ok(())
 }
 
+/// Mount `app` into `container` as a `width`×`height` cell grid and run it on
+/// the browser's `requestAnimationFrame` loop until the closure requests exit.
+///
+/// Installs the DOM event listeners (keyboard, mouse, wheel, resize, focus,
+/// paste) that feed SLT [`Event`]s into the run loop. Returns once the loop is
+/// scheduled; the closure keeps running via the retained RAF callback.
+///
+/// # Errors
+///
+/// Returns a [`JsValue`] error when the `window` is unavailable, a listener
+/// fails to install, or the initial frame cannot be scheduled.
 pub fn run_wasm(container: HtmlElement, width: u32, height: u32, app: F) -> Result<(), JsValue>
 where
     F: FnMut(&mut Context) + 'static,
@@ -734,6 +761,10 @@ where
     Ok(())
 }
 
+/// `wasm-bindgen` entry point that mounts an empty SLT app into `container`.
+///
+/// Thin wrapper over [`run_wasm`] with a no-op closure, exported to JS so the
+/// browser harness can smoke-test the backend wiring. Errors are dropped.
 #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
 pub fn run_wasm_raw(container: HtmlElement, width: u32, height: u32) {
     let _ = run_wasm(container, width, height, |_ui| {});
diff --git a/docs/COMPETITIVE_ANALYSIS.md b/docs/COMPETITIVE_ANALYSIS.md
index 7f65040..0fb1854 100644
--- a/docs/COMPETITIVE_ANALYSIS.md
+++ b/docs/COMPETITIVE_ANALYSIS.md
@@ -14,12 +14,14 @@
 | Rendering Model | Immediate (closure) | Immediate (Widget trait) | Retained (Event + CSS) | Component (React) | Elm (MVU) |
 | Built-in Widgets | Broad built-in catalog | ~15 | **60+** | 6 (+12 @inkjs/ui) | ~12 (Bubbles) |
 | Ecosystem | Small | **2,928 deps, 50+ widget crates** | Moderate | **3.9K deps** | **25K deps** |
-| Dependencies | 2 (unicode-width, compact_str; crossterm optional) | 1+ (crossterm, etc.) | Many (Rich, etc.) | Many (React, Yoga) | 0 (pure Go) |
+| Dependencies | 4 direct required, 25 resolved (default features) | 68 resolved (crossterm) | Many (Rich, etc.) | Many (React, Yoga) | 0 (pure Go) |
 
 ---
 
 Widget counts are not perfectly apples-to-apples across frameworks, so this document avoids treating every public helper method or specialized primitive as a separate widget type.
 
+**On "light"**: SLT's lightness is its dependency footprint and small public API, not binary size or build speed. The four direct required deps are `unicode-width`, `unicode-segmentation`, `smallvec`, and `compact_str`; `crossterm` is a default feature, not a hard requirement. The full resolved tree is 10 crates with `default-features = false`, 25 with default features, and 67 with `--features full` — versus 68 for a ratatui + crossterm app (cargo tree, normal deduped edges). SLT does **not** claim a smaller stripped binary or faster cold build: on a minimal hello-world (ratatui 0.30, stripped, macOS arm64) SLT default features is ~905 KiB vs ratatui's ~624 KiB (~1.45× larger), and SLT's cold release build is slightly slower. The win is the dependency tree (25 vs 68) and API surface, not the artifact.
+
 ---
 
 ## Where SLT Leads
diff --git a/docs/DESIGN_PRINCIPLES.md b/docs/DESIGN_PRINCIPLES.md
index f601367..c5d35a9 100644
--- a/docs/DESIGN_PRINCIPLES.md
+++ b/docs/DESIGN_PRINCIPLES.md
@@ -420,12 +420,17 @@ SLT follows [Semantic Versioning](https://semver.org/).
 
 ### R9 — Dependencies
 
-**Minimal by design.**
+**Minimal by design.** Four direct required deps; 25 crates resolved with
+default features, 10 with `default-features = false` (cargo tree, deduped) —
+versus 68 for a ratatui + crossterm app. "Light" here means the dependency
+tree and public API surface, not stripped binary size or build speed.
 
 | Dependency | Purpose | Required? |
 |------------|---------|-----------|
 | `crossterm` | Built-in terminal runtime and terminal helpers | Default feature |
 | `unicode-width` | Character width measurement | Yes |
+| `unicode-segmentation` | Grapheme-cluster segmentation | Yes |
+| `smallvec` | Inline small-vector scratch buffers | Yes |
 | `compact_str` | String optimization | Yes |
 | `tokio` | Async runtime | Optional (`async` feature) |
 | `serde` | Serialization | Optional (`serde` feature) |
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index 1275f8f..b9b0777 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -7,8 +7,13 @@ For the canonical API surface, also check docs.rs and `src/lib.rs`.
 
 ## Core model
 
-- `unicode-width` and `compact_str` are always part of the small core.
+- `unicode-width`, `unicode-segmentation`, `smallvec`, and `compact_str` are
+  always part of the small core (4 direct required deps).
 - `crossterm` is a **default feature**, not a hard requirement.
+- Resolved tree (cargo tree, deduped): 10 crates with `default-features = false`,
+  25 with default features, 67 with `--features full` — versus 68 for a
+  ratatui + crossterm app. "Light" refers to this dependency footprint and the
+  small public API, not stripped binary size or build speed.
 - The low-level core (`Backend`, `AppState`, `frame()`, widgets, events, style/layout types) works without terminal I/O.
 
 ## Main flags
diff --git a/docs/README.es.md b/docs/README.es.md
index 297cbc2..012885a 100644
--- a/docs/README.es.md
+++ b/docs/README.es.md
@@ -2,7 +2,7 @@
 
 # SuperLightTUI
 
-**Se escribe rápido. Se ejecuta ligero.**
+**Rápido de escribir. Árbol de dependencias ligero** (4 dependencias directas requeridas, 25 resueltas con las características por defecto, frente a 68 de ratatui + crossterm).
 
 [![Crate Badge]][Crate]
 [![Docs Badge]][Docs]
diff --git a/docs/README.ja.md b/docs/README.ja.md
index c2595b4..5debd1d 100644
--- a/docs/README.ja.md
+++ b/docs/README.ja.md
@@ -2,7 +2,7 @@
 
 # SuperLightTUI
 
-**書くのは速く。動くのは軽く。**
+**書くのは速く。依存ツリーは軽量**(直接の必須依存は 4 つ、デフォルト機能で解決後 25 クレート。ratatui + crossterm は 68)。
 
 [![Crate Badge]][Crate]
 [![Docs Badge]][Docs]
diff --git a/docs/README.ko.md b/docs/README.ko.md
index 4c7a300..cc8364d 100644
--- a/docs/README.ko.md
+++ b/docs/README.ko.md
@@ -2,7 +2,7 @@
 
 # SuperLightTUI
 
-**빠르게 만들고. 가볍게 실행합니다.**
+**빠르게 작성하고. 가벼운 의존성 트리**(직접 필수 의존성 4개, 기본 기능 기준 25개 resolved — ratatui + crossterm은 68개).
 
 [![Crate Badge]][Crate]
 [![Docs Badge]][Docs]
diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md
index 294838d..2e5bbaa 100644
--- a/docs/README.zh-CN.md
+++ b/docs/README.zh-CN.md
@@ -2,7 +2,7 @@
 
 # SuperLightTUI
 
-**写得快。跑得轻。**
+**写得快。依赖树轻量**(4 个直接必需依赖,默认特性下解析为 25 个 crate;ratatui + crossterm 为 68 个)。
 
 [![Crate Badge]][Crate]
 [![Docs Badge]][Docs]
diff --git a/examples/canvas_tour.rs b/examples/canvas_tour.rs
index 4e5cbf5..519c56a 100644
--- a/examples/canvas_tour.rs
+++ b/examples/canvas_tour.rs
@@ -613,10 +613,11 @@ impl MinesweeperGame {
         while placed < MINE_COUNT {
             let x = (self.next_rand() % MINE_W as u64) as usize;
             let y = (self.next_rand() % MINE_H as u64) as usize;
-            if let Some((ax, ay)) = avoid {
-                if x == ax && y == ay {
-                    continue;
-                }
+            if let Some((ax, ay)) = avoid
+                && x == ax
+                && y == ay
+            {
+                continue;
             }
             if self.board[y][x].mine {
                 continue;
@@ -1258,7 +1259,7 @@ fn format_score(n: u64) -> String {
     let len = bytes.len();
     let mut out = String::with_capacity(len + len / 3);
     for (i, &b) in bytes.iter().enumerate() {
-        if i > 0 && (len - i) % 3 == 0 {
+        if i > 0 && (len - i).is_multiple_of(3) {
             out.push(',');
         }
         out.push(b as char);
@@ -1799,7 +1800,7 @@ fn checkerboard_image(
     let mut rgba = Vec::with_capacity((width * height * 4) as usize);
     for y in 0..height {
         for x in 0..width {
-            let is_dark = ((x / cell_size) + (y / cell_size)) % 2 == 0;
+            let is_dark = ((x / cell_size) + (y / cell_size)).is_multiple_of(2);
             let v = if is_dark { 40u8 } else { 200u8 };
             rgba.extend_from_slice(&[v, v, v, 255]);
         }
diff --git a/examples/cookbook_table.rs b/examples/cookbook_table.rs
index 1967a54..8ef3de9 100644
--- a/examples/cookbook_table.rs
+++ b/examples/cookbook_table.rs
@@ -109,10 +109,10 @@ pub fn render(ui: &mut Context, state: &mut DemoState) {
             .unwrap_or(0);
         state.table.sort_by(next);
     }
-    if ui.consume_key_code(KeyCode::Enter) {
-        if let Some(c) = state.table.sort_column {
-            state.table.toggle_sort(c);
-        }
+    if ui.consume_key_code(KeyCode::Enter)
+        && let Some(c) = state.table.sort_column
+    {
+        state.table.toggle_sort(c);
     }
 }
 
diff --git a/examples/demo.rs b/examples/demo.rs
index 43663ab..a514fab 100644
--- a/examples/demo.rs
+++ b/examples/demo.rs
@@ -1187,14 +1187,14 @@ fn render_v011(
         let _ = ui.col(|ui| {
             card(ui, |ui| {
                 ui.text("File Picker").bold().fg(theme.accent);
-                if ui.file_picker(file_picker).changed {
-                    if let Some(path) = file_picker.selected_file() {
-                        let name = path
-                            .file_name()
-                            .and_then(|s| s.to_str())
-                            .unwrap_or("selected file");
-                        ui.notify(&format!("Picked: {name}"), ToastLevel::Success);
-                    }
+                if ui.file_picker(file_picker).changed
+                    && let Some(path) = file_picker.selected_file()
+                {
+                    let name = path
+                        .file_name()
+                        .and_then(|s| s.to_str())
+                        .unwrap_or("selected file");
+                    ui.notify(&format!("Picked: {name}"), ToastLevel::Success);
                 }
                 ui.text(format!("Dir: {}", file_picker.current_dir.display()))
                     .fg(theme.surface_text)
diff --git a/examples/demo_pretext.rs b/examples/demo_pretext.rs
index 2064b0b..e2896ff 100644
--- a/examples/demo_pretext.rs
+++ b/examples/demo_pretext.rs
@@ -135,7 +135,7 @@ fluid. Text becomes alive. The terminal transforms from a static grid \
 into a dynamic, responsive canvas. SuperLightTUI makes this possible with \
 zero dependencies on ncurses, zero unsafe code in the widget layer, and a \
 single cargo add superlighttui to get started. The minimum supported Rust \
-version is 1.81. The library compiles to WebAssembly via the slt-wasm \
+version is 1.88. The library compiles to WebAssembly via the slt-wasm \
 crate, bringing terminal UIs to the browser. Feature flags control optional \
 functionality: async for tokio integration, serde for serialization. The \
 test suite runs over 250 tests across widgets, layout, animation, and \
diff --git a/examples/demo_trading.rs b/examples/demo_trading.rs
index a15ebc3..7704a99 100644
--- a/examples/demo_trading.rs
+++ b/examples/demo_trading.rs
@@ -465,7 +465,7 @@ fn tick(s: &mut St) {
     }
 
     // new trade every other tick
-    if s.tick % 2 == 0 {
+    if s.tick.is_multiple_of(2) {
         let is_buy = s.rng.coin();
         let px = s.price * (1.0 + s.rng.range(-0.0003, 0.0003));
         s.trades.push_front(Trade {
diff --git a/examples/demo_website.rs b/examples/demo_website.rs
index ee9f981..9d38334 100644
--- a/examples/demo_website.rs
+++ b/examples/demo_website.rs
@@ -871,7 +871,10 @@ const BLOG_POSTS: &[BlogPost] = &[
         date: "2025-03-10",
         title: "Announcing SLT v0.1.0",
         reading_time: "5 min read",
-        tags: &[("release", TagTone::Success), ("announcement", TagTone::Primary)],
+        tags: &[
+            ("release", TagTone::Success),
+            ("announcement", TagTone::Primary),
+        ],
         excerpt: "The first public release of Super Light TUI is here. Two dependencies, zero unsafe, 14 widgets, and an API that gets out of your way.",
         render: render_post_announcement,
     },
@@ -879,7 +882,10 @@ const BLOG_POSTS: &[BlogPost] = &[
         date: "2025-03-08",
         title: "Why Immediate Mode for TUIs?",
         reading_time: "8 min read",
-        tags: &[("architecture", TagTone::Accent), ("deep-dive", TagTone::Warning)],
+        tags: &[
+            ("architecture", TagTone::Accent),
+            ("deep-dive", TagTone::Warning),
+        ],
         excerpt: "How egui-style rendering makes terminal UI development 10x faster, and why retained-mode frameworks add complexity you don't need.",
         render: render_post_immediate_mode,
     },
@@ -887,7 +893,10 @@ const BLOG_POSTS: &[BlogPost] = &[
         date: "2025-03-05",
         title: "Building a Dashboard in 50 Lines",
         reading_time: "4 min read",
-        tags: &[("tutorial", TagTone::Secondary), ("beginner", TagTone::Success)],
+        tags: &[
+            ("tutorial", TagTone::Secondary),
+            ("beginner", TagTone::Success),
+        ],
         excerpt: "Step-by-step guide to building a real-time system dashboard with SLT. Metrics, tables, and live updates in under a minute of reading.",
         render: render_post_dashboard_tutorial,
     },
@@ -895,7 +904,10 @@ const BLOG_POSTS: &[BlogPost] = &[
         date: "2025-03-01",
         title: "The Case for u32 Coordinates",
         reading_time: "3 min read",
-        tags: &[("technical", TagTone::Error), ("design-decision", TagTone::Accent)],
+        tags: &[
+            ("technical", TagTone::Error),
+            ("design-decision", TagTone::Accent),
+        ],
         excerpt: "Why every TUI library using u16 coordinates has a latent overflow bug, and how SLT avoids it with u32 at zero runtime cost.",
         render: render_post_u32,
     },
@@ -1023,18 +1035,21 @@ fn render_post_announcement(ui: &mut Context) {
     );
 
     md_h2(ui, "What's Included in v0.1.0");
-    md_bullet(ui, &[
-        "14 widgets: TextInput, Textarea, Button, Checkbox, Toggle, Tabs, List, Table, Spinner, Progress, Scrollable, Toast, Separator, HelpBar",
-        "Flexbox layout engine with row/col, gap, grow, shrink, alignment",
-        "Double-buffer diff rendering (only changed cells hit the terminal)",
-        "Mouse support: click, hover, drag-to-scroll",
-        "Automatic focus management with Tab/Shift+Tab",
-        "Dark and light theme presets, or bring your own",
-        "Animation primitives: Tween with 9 easings, Spring physics",
-        "Inline mode for rendering below the prompt",
-        "Optional async/tokio integration",
-        "Layout debugger (F12)",
-    ]);
+    md_bullet(
+        ui,
+        &[
+            "14 widgets: TextInput, Textarea, Button, Checkbox, Toggle, Tabs, List, Table, Spinner, Progress, Scrollable, Toast, Separator, HelpBar",
+            "Flexbox layout engine with row/col, gap, grow, shrink, alignment",
+            "Double-buffer diff rendering (only changed cells hit the terminal)",
+            "Mouse support: click, hover, drag-to-scroll",
+            "Automatic focus management with Tab/Shift+Tab",
+            "Dark and light theme presets, or bring your own",
+            "Animation primitives: Tween with 9 easings, Spring physics",
+            "Inline mode for rendering below the prompt",
+            "Optional async/tokio integration",
+            "Layout debugger (F12)",
+        ],
+    );
 
     md_h2(ui, "What's Next");
     md_p(ui, "v0.2.0 will focus on:");
@@ -1335,11 +1350,14 @@ fn render_post_u32(ui: &mut Context) {
 
     md_h2(ui, "Why Not Just Use i32 or usize?");
     md_p(ui, "We considered all options:");
-    md_bullet(ui, &[
-        "i32: Negative coordinates are meaningless for layout. Wastes a sign bit and allows invalid states.",
-        "usize: 64-bit on most platforms. Wastes memory in the character buffer (millions of cells).",
-        "u32: 4 billion max. More than enough for intermediate arithmetic. Same size as u16 after padding on most structs.",
-    ]);
+    md_bullet(
+        ui,
+        &[
+            "i32: Negative coordinates are meaningless for layout. Wastes a sign bit and allows invalid states.",
+            "usize: 64-bit on most platforms. Wastes memory in the character buffer (millions of cells).",
+            "u32: 4 billion max. More than enough for intermediate arithmetic. Same size as u16 after padding on most structs.",
+        ],
+    );
 
     md_p(
         ui,
@@ -1405,10 +1423,13 @@ fn render_post_flexbox(ui: &mut Context) {
     md_h2(ui, "How Layout Works Internally");
     md_p(ui, "SLT's layout algorithm runs in two passes:");
 
-    md_numbered(ui, &[
-        "Measure pass: each node computes its minimum size. Text measures its string width. Containers sum their children (column = sum heights, row = sum widths).",
-        "Layout pass: starting from the root (terminal size), each container distributes space to children. Fixed-size children get their minimum. Remaining space goes to children with grow > 0, proportional to their grow factor.",
-    ]);
+    md_numbered(
+        ui,
+        &[
+            "Measure pass: each node computes its minimum size. Text measures its string width. Containers sum their children (column = sum heights, row = sum widths).",
+            "Layout pass: starting from the root (terminal size), each container distributes space to children. Fixed-size children get their minimum. Remaining space goes to children with grow > 0, proportional to their grow factor.",
+        ],
+    );
 
     md_h2(ui, "Where We Diverge from CSS");
     md_p(
diff --git a/examples/perf_regression.rs b/examples/perf_regression.rs
index 2e6d909..105759b 100644
--- a/examples/perf_regression.rs
+++ b/examples/perf_regression.rs
@@ -1,7 +1,7 @@
 use std::time::Instant;
 
-use slt::widgets::{TextInputState, TextareaState};
 use slt::TestBackend;
+use slt::widgets::{TextInputState, TextareaState};
 
 fn main() {
     let mut tb = TestBackend::new(120, 40);
diff --git a/examples/v020_modal_trap.rs b/examples/v020_modal_trap.rs
index dd56327..e5a7e80 100644
--- a/examples/v020_modal_trap.rs
+++ b/examples/v020_modal_trap.rs
@@ -20,7 +20,7 @@
 //!   └─────────────────────────────────────┘      └───────────┘
 
 use slt::{
-    context::ModalOptions, Border, ButtonVariant, Context, KeyCode, KeyModifiers, RunConfig,
+    Border, ButtonVariant, Context, KeyCode, KeyModifiers, RunConfig, context::ModalOptions,
 };
 
 /// Mutable demo state. Bundling these into a struct keeps `main()` minimal
diff --git a/examples/v020_static_log.rs b/examples/v020_static_log.rs
index 7640da5..34b8904 100644
--- a/examples/v020_static_log.rs
+++ b/examples/v020_static_log.rs
@@ -74,7 +74,7 @@ fn main() -> std::io::Result<()> {
 
         // Throttle so a held key cannot flood scrollback faster than the
         // user can read it.
-        if count != last_logged && count % LOG_EVERY == 0 {
+        if count != last_logged && count.is_multiple_of(LOG_EVERY) {
             ui.static_log(format!("[tick] counter reached {count}"));
             last_logged = count;
         }
diff --git a/src/buffer.rs b/src/buffer.rs
index 6e054b8..77c59c6 100644
--- a/src/buffer.rs
+++ b/src/buffer.rs
@@ -374,10 +374,10 @@ impl Buffer {
     /// Used for Sixel images and other passthrough sequences.
     /// Respects the clip stack: sequences fully outside the current clip are skipped.
     pub fn raw_sequence(&mut self, x: u32, y: u32, seq: String) {
-        if let Some(clip) = self.effective_clip() {
-            if x >= clip.right() || y >= clip.bottom() {
-                return;
-            }
+        if let Some(clip) = self.effective_clip()
+            && (x >= clip.right() || y >= clip.bottom())
+        {
+            return;
         }
         self.raw_sequences.push((x, y, seq));
     }
@@ -390,14 +390,13 @@ impl Buffer {
     /// `kitty_clip_info_stack` (set via [`Buffer::push_kitty_clip`]).
     pub(crate) fn kitty_place(&mut self, mut p: KittyPlacement) {
         // Apply clip check
-        if let Some(clip) = self.effective_clip() {
-            if p.x >= clip.right()
+        if let Some(clip) = self.effective_clip()
+            && (p.x >= clip.right()
                 || p.y >= clip.bottom()
                 || p.x + p.cols <= clip.x
-                || p.y + p.rows <= clip.y
-            {
-                return;
-            }
+                || p.y + p.rows <= clip.y)
+        {
+            return;
         }
 
         // Apply scroll crop info if any frame is active
@@ -430,14 +429,13 @@ impl Buffer {
     /// that build so a genuine dead-code signal still fires by default.
     #[cfg_attr(not(feature = "crossterm"), allow(dead_code))]
     pub(crate) fn sprixel_place(&mut self, p: SprixelPlacement) {
-        if let Some(clip) = self.effective_clip() {
-            if p.x >= clip.right()
+        if let Some(clip) = self.effective_clip()
+            && (p.x >= clip.right()
                 || p.y >= clip.bottom()
                 || p.x + p.cols <= clip.x
-                || p.y + p.rows <= clip.y
-            {
-                return;
-            }
+                || p.y + p.rows <= clip.y)
+        {
+            return;
         }
         self.sprixels.push(p);
     }
@@ -606,7 +604,7 @@ impl Buffer {
                 // Append zero-width char (combining mark, ZWJ, variation selector)
                 // to the previous cell so grapheme clusters stay intact.
                 if x > self.area.x {
-                    let prev_in_clip = clip.map_or(true, |clip| {
+                    let prev_in_clip = clip.is_none_or(|clip| {
                         (x - 1) >= clip.x
                             && (x - 1) < clip.right()
                             && y >= clip.y
@@ -622,7 +620,7 @@ impl Buffer {
                 continue;
             }
 
-            let in_clip = clip.map_or(true, |clip| {
+            let in_clip = clip.is_none_or(|clip| {
                 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
             });
 
@@ -655,9 +653,9 @@ impl Buffer {
     ///
     /// No-ops if `(x, y)` is out of bounds or outside the current clip region.
     pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
-        let in_clip = self.effective_clip().map_or(true, |clip| {
-            x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
-        });
+        let in_clip = self
+            .effective_clip()
+            .is_none_or(|clip| x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom());
         if !self.in_bounds(x, y) || !in_clip {
             return;
         }
diff --git a/src/chart.rs b/src/chart.rs
index 3f4dcfd..65b5f66 100644
--- a/src/chart.rs
+++ b/src/chart.rs
@@ -16,12 +16,12 @@ pub(crate) use bar::build_histogram_config;
 pub(crate) use grid::truncate_label;
 pub(crate) use render::render_chart;
 
-use axis::{build_tui_ticks, format_number, resolve_bounds, TickSpec};
+use axis::{TickSpec, build_tui_ticks, format_number, resolve_bounds};
 use bar::draw_bar_dataset;
 use braille::draw_braille_dataset;
 use grid::{
-    apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map, center_text,
-    map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count, GridSpec,
+    GridSpec, apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map,
+    center_text, map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count,
 };
 
 const BRAILLE_BASE: u32 = 0x2800;
diff --git a/src/chart/braille.rs b/src/chart/braille.rs
index 1c6b5ed..258c28e 100644
--- a/src/chart/braille.rs
+++ b/src/chart/braille.rs
@@ -63,11 +63,7 @@ pub(super) fn draw_braille_dataset(
             let a = points[idx];
             let b = points[idx + 1];
             let seg_color = if let (Some(up), Some(down)) = (dataset.up_color, dataset.down_color) {
-                if b.2 > a.2 {
-                    up
-                } else {
-                    down
-                }
+                if b.2 > a.2 { up } else { down }
             } else {
                 dataset.color
             };
diff --git a/src/context.rs b/src/context.rs
index a78ad4a..a0d0845 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -7,7 +7,8 @@
 //! functions and `docs/ARCHITECTURE.md` for the 5-layer model that
 //! organizes which method lives where.
 
-use crate::chart::{build_histogram_config, render_chart, Candle, ChartBuilder, HistogramBuilder};
+use crate::FrameState;
+use crate::chart::{Candle, ChartBuilder, HistogramBuilder, build_histogram_config, render_chart};
 use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
 use crate::halfblock::HalfBlockImage;
 use crate::layout::{BeginContainerArgs, BeginScrollableArgs, Command, Direction};
@@ -17,16 +18,15 @@ use crate::style::{
     Modifiers, Padding, Spacing, Style, Theme, ThemeColor, WidgetColors, WidgetTheme,
 };
 use crate::widgets::{
-    color_hex_label, parse_hex_color, ApprovalAction, BreadcrumbResponse, ButtonVariant, CalDate,
-    CalendarSelect, CalendarState, ColorPickerState, CommandPaletteState, ContextItem,
-    FilePickerState, FormField, FormState, GaugeResponse, GridColumn, GutterResponse,
-    HighlightRange, ListState, MultiSelectState, NumberInputState, PaginatorState, PaginatorStyle,
-    PickerMode, RadioState, SchedKind, SchedulerSlot, SchedulerState, ScreenState, ScrollState,
-    SelectState, SpinnerState, SplitPaneResponse, SplitPaneState, StreamingTextState, TableState,
-    TabsState, TextInputState, TextareaState, ToastLevel, ToastState, ToolApprovalState, TreeState,
-    ValidateTrigger,
+    ApprovalAction, BreadcrumbResponse, ButtonVariant, CalDate, CalendarSelect, CalendarState,
+    ColorPickerState, CommandPaletteState, ContextItem, FilePickerState, FormField, FormState,
+    GaugeResponse, GridColumn, GutterResponse, HighlightRange, ListState, MultiSelectState,
+    NumberInputState, PaginatorState, PaginatorStyle, PickerMode, RadioState, SchedKind,
+    SchedulerSlot, SchedulerState, ScreenState, ScrollState, SelectState, SpinnerState,
+    SplitPaneResponse, SplitPaneState, StreamingTextState, TableState, TabsState, TextInputState,
+    TextareaState, ToastLevel, ToastState, ToolApprovalState, TreeState, ValidateTrigger,
+    color_hex_label, parse_hex_color,
 };
-use crate::FrameState;
 use unicode_segmentation::UnicodeSegmentation;
 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
 
@@ -77,6 +77,7 @@ mod async_tasks;
 #[cfg(feature = "async")]
 pub(crate) use async_tasks::AsyncTasks;
 #[cfg(feature = "async")]
+#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
 pub use async_tasks::TaskHandle;
 
 mod helpers;
diff --git a/src/context/async_tasks_tests.rs b/src/context/async_tasks_tests.rs
index 9f7ab68..6f51ccd 100644
--- a/src/context/async_tasks_tests.rs
+++ b/src/context/async_tasks_tests.rs
@@ -127,8 +127,8 @@ async fn two_handles_same_type_do_not_cross_results() {
 
 #[tokio::test]
 async fn dropping_handle_cancels_task() {
-    use std::sync::atomic::{AtomicBool, Ordering};
     use std::sync::Arc;
+    use std::sync::atomic::{AtomicBool, Ordering};
 
     let mut tb = TestBackend::new(40, 3);
     tb.set_async_runtime(tokio::runtime::Handle::current());
diff --git a/src/context/container.rs b/src/context/container.rs
index 55c2760..529126a 100644
--- a/src/context/container.rs
+++ b/src/context/container.rs
@@ -1488,11 +1488,7 @@ impl<'a> ContainerBuilder<'a> {
     /// # });
     /// ```
     pub fn with_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
-        if cond {
-            f(self)
-        } else {
-            self
-        }
+        if cond { f(self) } else { self }
     }
 
     /// Override the active theme for all widgets rendered inside this container.
diff --git a/src/context/helpers.rs b/src/context/helpers.rs
index 25c2d44..e98ceed 100644
--- a/src/context/helpers.rs
+++ b/src/context/helpers.rs
@@ -95,7 +95,7 @@ pub(crate) fn clamp_table_cell(cell: &str, width: u32) -> String {
     if cell_width <= width {
         let mut out = String::with_capacity(width);
         out.push_str(cell);
-        out.extend(std::iter::repeat(' ').take(width - cell_width));
+        out.extend(std::iter::repeat_n(' ', width - cell_width));
         return out;
     }
     if width == 0 {
@@ -118,7 +118,7 @@ pub(crate) fn clamp_table_cell(cell: &str, width: u32) -> String {
     out.push('\u{2026}');
     // Pad in case the last char was wide and left a one-cell gap before `…`.
     let out_width = UnicodeWidthStr::width(out.as_str());
-    out.extend(std::iter::repeat(' ').take(width.saturating_sub(out_width)));
+    out.extend(std::iter::repeat_n(' ', width.saturating_sub(out_width)));
     out
 }
 
@@ -179,9 +179,9 @@ pub(crate) fn center_text(text: &str, width: usize) -> String {
     let left = total / 2;
     let right = total - left;
     let mut centered = String::with_capacity(width);
-    centered.extend(std::iter::repeat(' ').take(left));
+    centered.extend(std::iter::repeat_n(' ', left));
     centered.push_str(text);
-    centered.extend(std::iter::repeat(' ').take(right));
+    centered.extend(std::iter::repeat_n(' ', right));
     centered
 }
 
@@ -253,7 +253,7 @@ pub(crate) fn textarea_logical_to_visual(
         if logical_col == seg_end {
             let is_last_seg = vlines
                 .get(i + 1)
-                .map_or(true, |next| next.logical_row != logical_row);
+                .is_none_or(|next| next.logical_row != logical_row);
             if is_last_seg {
                 return (i, logical_col - vl.char_start);
             }
@@ -285,7 +285,7 @@ impl Context {
     /// The intrinsic `(width, height_in_rows)` `text` would occupy, in cells.
     ///
     /// Reuses the exact word-wrap kernel the layout engine runs
-    /// ([`wrap_lines`](crate::layout::wrap_lines) via this crate's `tree`
+    /// (`wrap_lines` via this crate's `tree`
     /// module), so the answer always matches what a `ui.text(text).wrap()`
     /// would actually render — width logic is never duplicated here.
     ///
diff --git a/src/context/runtime.rs b/src/context/runtime.rs
index e7a6df9..1aac3a8 100644
--- a/src/context/runtime.rs
+++ b/src/context/runtime.rs
@@ -100,7 +100,7 @@ impl Context {
             for &(fid, rect) in &layout_feedback.prev_focus_rects {
                 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
                     let area = rect.width as u64 * rect.height as u64;
-                    if best.map_or(true, |(_, ba)| area < ba) {
+                    if best.is_none_or(|(_, ba)| area < ba) {
                         best = Some((fid, area));
                     }
                 }
@@ -444,6 +444,7 @@ impl Context {
     /// # });
     /// ```
     #[cfg(feature = "crossterm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
     pub fn capabilities(&self) -> &crate::terminal::Capabilities {
         &self.capabilities
     }
@@ -1479,6 +1480,7 @@ impl Context {
     /// # }
     /// ```
     #[cfg(feature = "async")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn spawn(
         &mut self,
         fut: impl std::future::Future + Send + 'static,
@@ -1505,6 +1507,7 @@ impl Context {
     /// # }
     /// ```
     #[cfg(feature = "async")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn poll(&mut self, handle: &TaskHandle) -> Option {
         self.async_tasks.poll::(handle.id())
     }
@@ -1695,11 +1698,7 @@ impl Context {
 
     /// Returns `light` color if current theme is light mode, `dark` color if dark mode.
     pub fn light_dark(&self, light: Color, dark: Color) -> Color {
-        if self.theme.is_dark {
-            dark
-        } else {
-            light
-        }
+        if self.theme.is_dark { dark } else { light }
     }
 
     /// Show a toast notification without managing ToastState.
@@ -1923,7 +1922,7 @@ impl Context {
     ///    inherits the name.
     ///
     /// Names are re-registered each frame; the previous frame's map is
-    /// kept under `focus_name_map_prev` so [`focus_by_name`] can resolve
+    /// kept under `focus_name_map_prev` so [`focus_by_name`](Context::focus_by_name) can resolve
     /// a name that has already been registered.
     ///
     /// # Two valid usage shapes
@@ -2168,10 +2167,10 @@ impl Context {
     /// `ui.static_log(...)` emitted during a [`crate::TestBackend::render`]
     /// call.
     pub fn take_static_log(&mut self) -> Vec {
-        if let Some(boxed) = self.named_states.get_mut(STATIC_LOG_KEY) {
-            if let Some(buf) = boxed.downcast_mut::>() {
-                return std::mem::take(buf);
-            }
+        if let Some(boxed) = self.named_states.get_mut(STATIC_LOG_KEY)
+            && let Some(buf) = boxed.downcast_mut::>()
+        {
+            return std::mem::take(buf);
         }
         Vec::new()
     }
@@ -2228,10 +2227,10 @@ impl Context {
     /// Empty if no widget called [`Context::publish_keymap`] yet on the
     /// current frame. The registry is reset at the start of every frame.
     pub fn published_keymaps(&self) -> &[crate::keymap::PublishedKeymap] {
-        if let Some(boxed) = self.named_states.get(KEYMAP_REGISTRY_KEY) {
-            if let Some(vec) = boxed.downcast_ref::>() {
-                return vec;
-            }
+        if let Some(boxed) = self.named_states.get(KEYMAP_REGISTRY_KEY)
+            && let Some(vec) = boxed.downcast_ref::>()
+        {
+            return vec;
         }
         &[]
     }
diff --git a/src/context/scheduler_tests.rs b/src/context/scheduler_tests.rs
index 33637b3..d693e00 100644
--- a/src/context/scheduler_tests.rs
+++ b/src/context/scheduler_tests.rs
@@ -6,8 +6,8 @@
 //! pure interval arithmetic is proptested separately, without sleeps, via
 //! [`crate::widgets::intervals_elapsed`].
 
-use crate::test_utils::TestBackend;
 use crate::EventBuilder;
+use crate::test_utils::TestBackend;
 use std::time::Duration;
 
 fn sleep(ms: u64) {
diff --git a/src/context/tests.rs b/src/context/tests.rs
index 1bde65b..15a369e 100644
--- a/src/context/tests.rs
+++ b/src/context/tests.rs
@@ -1,6 +1,6 @@
 use super::*;
-use crate::test_utils::TestBackend;
 use crate::EventBuilder;
+use crate::test_utils::TestBackend;
 
 #[derive(Debug, PartialEq, Eq)]
 struct SnapshotShape {
@@ -93,8 +93,8 @@ fn use_memo_handle_releases_borrow() {
 
 #[test]
 fn use_memo_recomputes_only_on_dep_change() {
-    use std::sync::atomic::{AtomicUsize, Ordering};
     use std::sync::Arc;
+    use std::sync::atomic::{AtomicUsize, Ordering};
 
     let calls = Arc::new(AtomicUsize::new(0));
     let mut tb = TestBackend::new(20, 3);
diff --git a/src/context/widgets_display.rs b/src/context/widgets_display.rs
index bbf11f9..95c0931 100644
--- a/src/context/widgets_display.rs
+++ b/src/context/widgets_display.rs
@@ -368,13 +368,13 @@ fn render_highlighted_line(ui: &mut Context, line: &str) {
     while pos < trimmed.len() {
         let ch = trimmed.as_bytes()[pos];
 
-        if ch == b'"' {
-            if let Some(end) = trimmed[pos + 1..].find('"') {
-                let s = &trimmed[pos..pos + end + 2];
-                ui.text(s).fg(string_color);
-                pos += end + 2;
-                continue;
-            }
+        if ch == b'"'
+            && let Some(end) = trimmed[pos + 1..].find('"')
+        {
+            let s = &trimmed[pos..pos + end + 2];
+            ui.text(s).fg(string_color);
+            pos += end + 2;
+            continue;
         }
 
         if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
@@ -517,39 +517,48 @@ mod sixel_detection_tests {
     // concurrent test threads don't observe each other's set_var() calls.
     static ENV_GUARD: Mutex<()> = Mutex::new(());
 
+    // edition 2024: env mutation is `unsafe`; ENV_GUARD makes it sound here.
+    #[allow(unsafe_code)]
     fn with_env(term: Option<&str>, term_program: Option<&str>, force: bool, f: F) {
         let _g = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
         let prev_term = std::env::var("TERM").ok();
         let prev_program = std::env::var("TERM_PROGRAM").ok();
         let prev_force = std::env::var("SLT_FORCE_SIXEL").ok();
 
-        match term {
-            Some(v) => std::env::set_var("TERM", v),
-            None => std::env::remove_var("TERM"),
-        }
-        match term_program {
-            Some(v) => std::env::set_var("TERM_PROGRAM", v),
-            None => std::env::remove_var("TERM_PROGRAM"),
-        }
-        if force {
-            std::env::set_var("SLT_FORCE_SIXEL", "1");
-        } else {
-            std::env::remove_var("SLT_FORCE_SIXEL");
+        // SAFETY (edition 2024): set_var/remove_var are unsafe because env
+        // mutation races concurrent reads. ENV_GUARD above serializes these
+        // test helpers, so no other thread observes a torn update.
+        unsafe {
+            match term {
+                Some(v) => std::env::set_var("TERM", v),
+                None => std::env::remove_var("TERM"),
+            }
+            match term_program {
+                Some(v) => std::env::set_var("TERM_PROGRAM", v),
+                None => std::env::remove_var("TERM_PROGRAM"),
+            }
+            if force {
+                std::env::set_var("SLT_FORCE_SIXEL", "1");
+            } else {
+                std::env::remove_var("SLT_FORCE_SIXEL");
+            }
         }
 
         f();
 
-        match prev_term {
-            Some(v) => std::env::set_var("TERM", v),
-            None => std::env::remove_var("TERM"),
-        }
-        match prev_program {
-            Some(v) => std::env::set_var("TERM_PROGRAM", v),
-            None => std::env::remove_var("TERM_PROGRAM"),
-        }
-        match prev_force {
-            Some(v) => std::env::set_var("SLT_FORCE_SIXEL", v),
-            None => std::env::remove_var("SLT_FORCE_SIXEL"),
+        unsafe {
+            match prev_term {
+                Some(v) => std::env::set_var("TERM", v),
+                None => std::env::remove_var("TERM"),
+            }
+            match prev_program {
+                Some(v) => std::env::set_var("TERM_PROGRAM", v),
+                None => std::env::remove_var("TERM_PROGRAM"),
+            }
+            match prev_force {
+                Some(v) => std::env::set_var("SLT_FORCE_SIXEL", v),
+                None => std::env::remove_var("SLT_FORCE_SIXEL"),
+            }
         }
     }
 
@@ -631,39 +640,48 @@ mod iterm_detection_tests {
     // concurrent test threads don't observe each other's set_var() calls.
     static ENV_GUARD: Mutex<()> = Mutex::new(());
 
+    // edition 2024: env mutation is `unsafe`; ENV_GUARD makes it sound here.
+    #[allow(unsafe_code)]
     fn with_env(term: Option<&str>, term_program: Option<&str>, force: bool, f: F) {
         let _g = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
         let prev_term = std::env::var("TERM").ok();
         let prev_program = std::env::var("TERM_PROGRAM").ok();
         let prev_force = std::env::var("SLT_FORCE_ITERM").ok();
 
-        match term {
-            Some(v) => std::env::set_var("TERM", v),
-            None => std::env::remove_var("TERM"),
-        }
-        match term_program {
-            Some(v) => std::env::set_var("TERM_PROGRAM", v),
-            None => std::env::remove_var("TERM_PROGRAM"),
-        }
-        if force {
-            std::env::set_var("SLT_FORCE_ITERM", "1");
-        } else {
-            std::env::remove_var("SLT_FORCE_ITERM");
+        // SAFETY (edition 2024): set_var/remove_var are unsafe because env
+        // mutation races concurrent reads. ENV_GUARD above serializes these
+        // test helpers, so no other thread observes a torn update.
+        unsafe {
+            match term {
+                Some(v) => std::env::set_var("TERM", v),
+                None => std::env::remove_var("TERM"),
+            }
+            match term_program {
+                Some(v) => std::env::set_var("TERM_PROGRAM", v),
+                None => std::env::remove_var("TERM_PROGRAM"),
+            }
+            if force {
+                std::env::set_var("SLT_FORCE_ITERM", "1");
+            } else {
+                std::env::remove_var("SLT_FORCE_ITERM");
+            }
         }
 
         f();
 
-        match prev_term {
-            Some(v) => std::env::set_var("TERM", v),
-            None => std::env::remove_var("TERM"),
-        }
-        match prev_program {
-            Some(v) => std::env::set_var("TERM_PROGRAM", v),
-            None => std::env::remove_var("TERM_PROGRAM"),
-        }
-        match prev_force {
-            Some(v) => std::env::set_var("SLT_FORCE_ITERM", v),
-            None => std::env::remove_var("SLT_FORCE_ITERM"),
+        unsafe {
+            match prev_term {
+                Some(v) => std::env::set_var("TERM", v),
+                None => std::env::remove_var("TERM"),
+            }
+            match prev_program {
+                Some(v) => std::env::set_var("TERM_PROGRAM", v),
+                None => std::env::remove_var("TERM_PROGRAM"),
+            }
+            match prev_force {
+                Some(v) => std::env::set_var("SLT_FORCE_ITERM", v),
+                None => std::env::remove_var("SLT_FORCE_ITERM"),
+            }
         }
     }
 
diff --git a/src/context/widgets_display/gauge.rs b/src/context/widgets_display/gauge.rs
index cd1a5ff..00ea18c 100644
--- a/src/context/widgets_display/gauge.rs
+++ b/src/context/widgets_display/gauge.rs
@@ -340,27 +340,27 @@ fn compose_bar(
     let width_usize = width as usize;
     let filled = filled_cells(ratio, width);
 
-    if let LabelMode::Centered(label) = mode {
-        if !label.is_empty() {
-            let label_w = UnicodeWidthStr::width(label);
-            if label_w + 2 <= width_usize {
-                // Build the bar then overlay the centered label.
-                let mut cells: Vec = Vec::with_capacity(width_usize);
-                for i in 0..width {
-                    cells.push(if i < filled { fill_ch } else { empty_ch });
-                }
-                let label_start = (width_usize.saturating_sub(label_w)) / 2;
-                let label_end = label_start + label_w;
-                let mut out = String::with_capacity(width_usize * 4 + label.len());
-                for ch in cells.iter().take(label_start) {
-                    out.push(*ch);
-                }
-                out.push_str(label);
-                for ch in cells.iter().take(width_usize).skip(label_end) {
-                    out.push(*ch);
-                }
-                return out;
+    if let LabelMode::Centered(label) = mode
+        && !label.is_empty()
+    {
+        let label_w = UnicodeWidthStr::width(label);
+        if label_w + 2 <= width_usize {
+            // Build the bar then overlay the centered label.
+            let mut cells: Vec = Vec::with_capacity(width_usize);
+            for i in 0..width {
+                cells.push(if i < filled { fill_ch } else { empty_ch });
             }
+            let label_start = (width_usize.saturating_sub(label_w)) / 2;
+            let label_end = label_start + label_w;
+            let mut out = String::with_capacity(width_usize * 4 + label.len());
+            for ch in cells.iter().take(label_start) {
+                out.push(*ch);
+            }
+            out.push_str(label);
+            for ch in cells.iter().take(width_usize).skip(label_end) {
+                out.push(*ch);
+            }
+            return out;
         }
     }
 
diff --git a/src/context/widgets_display/layout.rs b/src/context/widgets_display/layout.rs
index 145db0f..93e4afd 100644
--- a/src/context/widgets_display/layout.rs
+++ b/src/context/widgets_display/layout.rs
@@ -1,10 +1,10 @@
 use super::*;
-use std::sync::OnceLock;
+use std::sync::LazyLock;
 
-static SEP_LINE: OnceLock = OnceLock::new();
+static SEP_LINE: LazyLock = LazyLock::new(|| "─".repeat(200));
 
 fn sep_line() -> &'static str {
-    SEP_LINE.get_or_init(|| "─".repeat(200))
+    &SEP_LINE
 }
 
 /// Compass-rose anchor for [`Context::overlay_at`] / [`Context::modal_at`].
diff --git a/src/context/widgets_display/line_wrap_tests.rs b/src/context/widgets_display/line_wrap_tests.rs
index 83a2f75..2d6f4d0 100644
--- a/src/context/widgets_display/line_wrap_tests.rs
+++ b/src/context/widgets_display/line_wrap_tests.rs
@@ -1,6 +1,6 @@
 use super::*;
-use crate::event::Event;
 use crate::FrameState;
+use crate::event::Event;
 
 #[test]
 fn line_wrap_without_links_compacts_to_rich_text() {
@@ -32,9 +32,10 @@ fn line_wrap_with_links_keeps_interactive_commands() {
         ctx.commands.first(),
         Some(Command::BeginContainer(_))
     ));
-    assert!(ctx
-        .commands
-        .iter()
-        .any(|cmd| matches!(cmd, Command::Link { text, .. } if text == "Docs")));
+    assert!(
+        ctx.commands
+            .iter()
+            .any(|cmd| matches!(cmd, Command::Link { text, .. } if text == "Docs"))
+    );
     assert!(matches!(ctx.commands.last(), Some(Command::EndContainer)));
 }
diff --git a/src/context/widgets_display/rich_output.rs b/src/context/widgets_display/rich_output.rs
index 1168d01..4bb67d3 100644
--- a/src/context/widgets_display/rich_output.rs
+++ b/src/context/widgets_display/rich_output.rs
@@ -211,6 +211,7 @@ impl Context {
     /// # });
     /// ```
     #[cfg(feature = "crossterm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
     pub fn sixel_image(
         &mut self,
         rgba: &[u8],
@@ -295,6 +296,7 @@ impl Context {
     /// # });
     /// ```
     #[cfg(feature = "crossterm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
     pub fn iterm_image(&mut self, data: &[u8], cols: u32, rows: u32) -> Response {
         // Issue #264 ladder integration: consult the negotiated capability
         // snapshot first, then the env allowlist / `SLT_FORCE_ITERM`. App code
@@ -348,6 +350,7 @@ impl Context {
     /// # });
     /// ```
     #[cfg(feature = "crossterm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
     pub fn iterm_image_fit(&mut self, data: &[u8], cols: u32) -> Response {
         let supported =
             self.is_real_terminal && (self.capabilities.iterm2 || terminal_supports_iterm());
@@ -471,7 +474,7 @@ impl Context {
     pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
         if state.streaming {
             state.cursor_tick = state.cursor_tick.wrapping_add(1);
-            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
+            state.cursor_visible = (state.cursor_tick / 8).is_multiple_of(2);
         }
 
         if state.content.is_empty() && state.streaming {
@@ -515,7 +518,7 @@ impl Context {
     ) -> Response {
         if state.streaming {
             state.cursor_tick = state.cursor_tick.wrapping_add(1);
-            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
+            state.cursor_visible = (state.cursor_tick / 8).is_multiple_of(2);
         }
 
         if state.content.is_empty() && state.streaming {
diff --git a/src/context/widgets_display/split.rs b/src/context/widgets_display/split.rs
index 42d65b8..0d8da25 100644
--- a/src/context/widgets_display/split.rs
+++ b/src/context/widgets_display/split.rs
@@ -232,16 +232,15 @@ impl Context {
         for (i, mouse) in events {
             match mouse.kind {
                 MouseKind::Down(MouseButton::Left) => {
-                    if let Some(rect) = handle_rect {
-                        if rect.width > 0
-                            && mouse.x >= rect.x
-                            && mouse.x < rect.right()
-                            && mouse.y >= rect.y
-                            && mouse.y < rect.bottom()
-                        {
-                            state.dragging = true;
-                            consumed.push(i);
-                        }
+                    if let Some(rect) = handle_rect
+                        && rect.width > 0
+                        && mouse.x >= rect.x
+                        && mouse.x < rect.right()
+                        && mouse.y >= rect.y
+                        && mouse.y < rect.bottom()
+                    {
+                        state.dragging = true;
+                        consumed.push(i);
                     }
                 }
                 MouseKind::Drag(MouseButton::Left) if state.dragging => {
diff --git a/src/context/widgets_display/status.rs b/src/context/widgets_display/status.rs
index dd07d76..4a244f8 100644
--- a/src/context/widgets_display/status.rs
+++ b/src/context/widgets_display/status.rs
@@ -125,29 +125,27 @@ impl Context {
         // no entry yet, so we fall back to assuming the row starts at (0,0)
         // — same behaviour as the prior implementation.
         let q_width = UnicodeWidthStr::width(question) as u32;
-        if !clicked {
-            if let Some((mx, my)) = self.click_pos {
-                let next_id = self.rollback.interaction_count;
-                let prev_rect = self.prev_hit_map.get(next_id).copied();
-                let row_x = prev_rect.map(|r| r.x).unwrap_or(0);
-                let in_row_y = match prev_rect {
-                    Some(r) if r.height > 0 => my >= r.y && my < r.bottom(),
-                    _ => true,
-                };
-                if in_row_y {
-                    let yes_start = row_x + q_width + 1;
-                    let yes_end = yes_start + 5;
-                    let no_start = yes_end + 1;
-                    let no_end = no_start + 4; // "[No]" = 4 display columns
-                    if mx >= yes_start && mx < yes_end {
-                        is_yes = true;
-                        *result = true;
-                        clicked = true;
-                    } else if mx >= no_start && mx < no_end {
-                        is_yes = false;
-                        *result = false;
-                        clicked = true;
-                    }
+        if !clicked && let Some((mx, my)) = self.click_pos {
+            let next_id = self.rollback.interaction_count;
+            let prev_rect = self.prev_hit_map.get(next_id).copied();
+            let row_x = prev_rect.map(|r| r.x).unwrap_or(0);
+            let in_row_y = match prev_rect {
+                Some(r) if r.height > 0 => my >= r.y && my < r.bottom(),
+                _ => true,
+            };
+            if in_row_y {
+                let yes_start = row_x + q_width + 1;
+                let yes_end = yes_start + 5;
+                let no_start = yes_end + 1;
+                let no_end = no_start + 4; // "[No]" = 4 display columns
+                if mx >= yes_start && mx < yes_end {
+                    is_yes = true;
+                    *result = true;
+                    clicked = true;
+                } else if mx >= no_start && mx < no_end {
+                    is_yes = false;
+                    *result = false;
+                    clicked = true;
                 }
             }
         }
@@ -286,7 +284,7 @@ impl Context {
                     let key_display_w = UnicodeWidthStr::width(*key);
                     let pad = max_key_width.saturating_sub(key_display_w);
                     let mut padded = String::with_capacity(key.len() + pad);
-                    padded.extend(std::iter::repeat(' ').take(pad));
+                    padded.extend(std::iter::repeat_n(' ', pad));
                     padded.push_str(key);
                     ui.text(padded).dim();
                     ui.text("  ");
diff --git a/src/context/widgets_display/text.rs b/src/context/widgets_display/text.rs
index 98c4b3e..a0727b9 100644
--- a/src/context/widgets_display/text.rs
+++ b/src/context/widgets_display/text.rs
@@ -49,10 +49,8 @@ impl Context {
 
         let activated = response.clicked || self.consume_activation_keys(focused);
 
-        if activated {
-            if let Err(e) = open_url(&url_str) {
-                eprintln!("[slt] failed to open URL: {e}");
-            }
+        if activated && let Err(e) = open_url(&url_str) {
+            eprintln!("[slt] failed to open URL: {e}");
         }
 
         let style = if focused {
@@ -461,10 +459,10 @@ impl Context {
 
     /// Enable word-boundary wrapping on the last rendered text element.
     pub fn wrap(&mut self) -> &mut Self {
-        if let Some(idx) = self.rollback.last_text_idx {
-            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
-                *wrap = true;
-            }
+        if let Some(idx) = self.rollback.last_text_idx
+            && let Command::Text { wrap, .. } = &mut self.commands[idx]
+        {
+            *wrap = true;
         }
         self
     }
@@ -472,10 +470,10 @@ impl Context {
     /// Truncate the last rendered text with `…` when it exceeds its allocated width.
     /// Use with `.w()` to set a fixed width, or let the parent container constrain it.
     pub fn truncate(&mut self) -> &mut Self {
-        if let Some(idx) = self.rollback.last_text_idx {
-            if let Command::Text { truncate, .. } = &mut self.commands[idx] {
-                *truncate = true;
-            }
+        if let Some(idx) = self.rollback.last_text_idx
+            && let Command::Text { truncate, .. } = &mut self.commands[idx]
+        {
+            *truncate = true;
         }
         self
     }
@@ -516,23 +514,22 @@ impl Context {
     /// A value of `1` causes the element to expand and fill remaining space
     /// along the main axis.
     pub fn grow(&mut self, value: u16) -> &mut Self {
-        if let Some(idx) = self.rollback.last_text_idx {
-            if let Command::Text { grow, .. } = &mut self.commands[idx] {
-                *grow = value;
-            }
+        if let Some(idx) = self.rollback.last_text_idx
+            && let Command::Text { grow, .. } = &mut self.commands[idx]
+        {
+            *grow = value;
         }
         self
     }
 
     /// Set the text alignment of the last rendered text element.
     pub fn align(&mut self, align: Align) -> &mut Self {
-        if let Some(idx) = self.rollback.last_text_idx {
-            if let Command::Text {
+        if let Some(idx) = self.rollback.last_text_idx
+            && let Command::Text {
                 align: text_align, ..
             } = &mut self.commands[idx]
-            {
-                *text_align = align;
-            }
+        {
+            *text_align = align;
         }
         self
     }
diff --git a/src/context/widgets_input/tests.rs b/src/context/widgets_input/tests.rs
index 151ad18..9badf15 100644
--- a/src/context/widgets_input/tests.rs
+++ b/src/context/widgets_input/tests.rs
@@ -58,7 +58,7 @@ fn text_input_empty_value_shows_no_suggestions() {
 
 // ── Form field validation triggers ────────────────────────────────────────
 
-use crate::widgets::{validators, FormField, ValidateTrigger};
+use crate::widgets::{FormField, ValidateTrigger, validators};
 
 /// Render one form field (focusable #0) followed by a button (focusable #1).
 /// `focus_index` selects which is focused; `prev_focus_count` is 2.
diff --git a/src/context/widgets_input/text_input.rs b/src/context/widgets_input/text_input.rs
index f99f34a..b9e2151 100644
--- a/src/context/widgets_input/text_input.rs
+++ b/src/context/widgets_input/text_input.rs
@@ -99,10 +99,10 @@ impl Context {
                         consumed_indices.push(i);
                     }
                     KeyCode::Char(ch) => {
-                        if let Some(max) = state.max_length {
-                            if grapheme_count(&state.value) >= max {
-                                continue;
-                            }
+                        if let Some(max) = state.max_length
+                            && grapheme_count(&state.value) >= max
+                        {
+                            continue;
                         }
                         let index = byte_index_for_grapheme(&state.value, state.cursor);
                         state.value.insert(index, ch);
@@ -193,10 +193,10 @@ impl Context {
                     if (ch as u32) < 0x20 || ch == '\u{7f}' {
                         continue;
                     }
-                    if let Some(max) = state.max_length {
-                        if char_count >= max {
-                            break;
-                        }
+                    if let Some(max) = state.max_length
+                        && char_count >= max
+                    {
+                        break;
                     }
                     let index = byte_index_for_grapheme(&state.value, state.cursor);
                     state.value.insert(index, ch);
diff --git a/src/context/widgets_input/textarea_progress.rs b/src/context/widgets_input/textarea_progress.rs
index f7ca5a7..427a593 100644
--- a/src/context/widgets_input/textarea_progress.rs
+++ b/src/context/widgets_input/textarea_progress.rs
@@ -296,10 +296,10 @@ impl Context {
                 // inside the loop would be O(n²) on large pastes.
                 let mut total_chars: usize = state.lines.iter().map(|l| grapheme_count(l)).sum();
                 for ch in text.chars() {
-                    if let Some(max) = state.max_length {
-                        if total_chars >= max {
-                            break;
-                        }
+                    if let Some(max) = state.max_length
+                        && total_chars >= max
+                    {
+                        break;
                     }
                     if ch == '\n' || ch == '\r' {
                         let split_index = byte_index_for_grapheme(
diff --git a/src/context/widgets_interactive/collections.rs b/src/context/widgets_interactive/collections.rs
index 1fc5af3..6b49a31 100644
--- a/src/context/widgets_interactive/collections.rs
+++ b/src/context/widgets_interactive/collections.rs
@@ -334,8 +334,9 @@ impl Context {
     /// `Alt+Up`/`Alt+Down`. Reordering operates on the underlying item order via
     /// [`ListState::move_item`], keeping the selection on the moved item.
     ///
-    /// Returns a [`ListResponse`] which derefs to the standard [`Response`] and
-    /// exposes [`reordered`](ListResponse::reordered) — `Some((from, to))` with
+    /// Returns a [`ListResponse`](crate::ListResponse) which derefs to the
+    /// standard [`Response`] and exposes
+    /// [`reordered`](crate::ListResponse::reordered) — `Some((from, to))` with
     /// the data indices when an item moved this frame, otherwise `None`.
     ///
     /// The plain [`list`](Context::list) entry point is unchanged; opt into
@@ -413,14 +414,12 @@ impl Context {
                         };
                         // Map both endpoints from view positions to data indices
                         // so reordering survives an active filter.
-                        if let Some(target_view) = target_view {
-                            if let (Some(&from), Some(&to)) =
+                        if let Some(target_view) = target_view
+                            && let (Some(&from), Some(&to)) =
                                 (visible.get(cur_view), visible.get(target_view))
-                            {
-                                if state.move_item(from, to) {
-                                    reordered = Some((from, to));
-                                }
-                            }
+                            && state.move_item(from, to)
+                        {
+                            reordered = Some((from, to));
                         }
                         // Consume regardless so a held modifier never also
                         // triggers a plain navigation step on the same key.
diff --git a/src/context/widgets_interactive/events.rs b/src/context/widgets_interactive/events.rs
index 77c36ec..a50ab9f 100644
--- a/src/context/widgets_interactive/events.rs
+++ b/src/context/widgets_interactive/events.rs
@@ -212,16 +212,9 @@ impl Context {
         {
             return false;
         }
-        let index =
-            self.available_key_presses().find_map(
-                |(i, key)| {
-                    if key.code == code {
-                        Some(i)
-                    } else {
-                        None
-                    }
-                },
-            );
+        let index = self
+            .available_key_presses()
+            .find_map(|(i, key)| if key.code == code { Some(i) } else { None });
         if let Some(index) = index {
             self.consume_indices([index]);
             true
@@ -266,10 +259,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(mouse.kind, MouseKind::Down(MouseButton::Left))
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -289,10 +282,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(mouse.kind, MouseKind::Drag(MouseButton::Left)) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(mouse.kind, MouseKind::Drag(MouseButton::Left))
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -311,10 +304,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(mouse.kind, MouseKind::Up(MouseButton::Left)) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(mouse.kind, MouseKind::Up(MouseButton::Left))
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -334,10 +327,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(&mouse.kind, MouseKind::Down(b) if *b == button) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(&mouse.kind, MouseKind::Down(b) if *b == button)
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -354,10 +347,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(&mouse.kind, MouseKind::Drag(b) if *b == button) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(&mouse.kind, MouseKind::Drag(b) if *b == button)
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -374,10 +367,10 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Mouse(mouse) = event {
-                if matches!(&mouse.kind, MouseKind::Up(b) if *b == button) {
-                    return Some((mouse.x, mouse.y));
-                }
+            if let Event::Mouse(mouse) = event
+                && matches!(&mouse.kind, MouseKind::Up(b) if *b == button)
+            {
+                return Some((mouse.x, mouse.y));
             }
             None
         })
@@ -402,7 +395,7 @@ impl Context {
             if self.consumed[i] {
                 return None;
             }
-            if let Event::Paste(ref text) = event {
+            if let Event::Paste(text) = event {
                 return Some(text.as_str());
             }
             None
diff --git a/src/context/widgets_interactive/rich_markdown.rs b/src/context/widgets_interactive/rich_markdown.rs
index 1c2b9ee..bf78a9d 100644
--- a/src/context/widgets_interactive/rich_markdown.rs
+++ b/src/context/widgets_interactive/rich_markdown.rs
@@ -1,5 +1,5 @@
 use super::*;
-use crate::{RichLogState, DEFAULT_CHORD_TIMEOUT_TICKS};
+use crate::{DEFAULT_CHORD_TIMEOUT_TICKS, RichLogState};
 
 impl Context {
     /// Render a scrollable rich log view with styled entries.
@@ -872,22 +872,22 @@ impl Context {
         let items = Self::split_md_links(text);
 
         // Fast path: no links/images found
-        if items.len() == 1 {
-            if let MdInline::Text(ref t) = items[0] {
-                let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
-                if segs.len() <= 1 {
-                    self.text(text)
-                        .wrap()
-                        .fg(text_style.fg.unwrap_or(Color::Reset));
-                } else {
-                    self.line_wrap(|ui| {
-                        for (s, st) in segs {
-                            ui.styled(s, st);
-                        }
-                    });
-                }
-                return;
+        if items.len() == 1
+            && let MdInline::Text(ref t) = items[0]
+        {
+            let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
+            if segs.len() <= 1 {
+                self.text(text)
+                    .wrap()
+                    .fg(text_style.fg.unwrap_or(Color::Reset));
+            } else {
+                self.line_wrap(|ui| {
+                    for (s, st) in segs {
+                        ui.styled(s, st);
+                    }
+                });
             }
+            return;
         }
 
         // Mixed content — line_wrap collects both Text and Link commands
@@ -953,29 +953,31 @@ impl Context {
 
         while i < chars.len() {
             // Image: ![alt](url)
-            if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
-                if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
-                    if !current.is_empty() {
-                        items.push(MdInline::Text(std::mem::take(&mut current)));
-                    }
-                    items.push(MdInline::Image { alt });
-                    i += 1 + consumed;
-                    continue;
+            if chars[i] == '!'
+                && i + 1 < chars.len()
+                && chars[i + 1] == '['
+                && let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1)
+            {
+                if !current.is_empty() {
+                    items.push(MdInline::Text(std::mem::take(&mut current)));
                 }
+                items.push(MdInline::Image { alt });
+                i += 1 + consumed;
+                continue;
             }
             // Link: [text](url)
-            if chars[i] == '[' {
-                if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
-                    if !current.is_empty() {
-                        items.push(MdInline::Text(std::mem::take(&mut current)));
-                    }
-                    items.push(MdInline::Link {
-                        text: link_text,
-                        url,
-                    });
-                    i += consumed;
-                    continue;
+            if chars[i] == '['
+                && let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i)
+            {
+                if !current.is_empty() {
+                    items.push(MdInline::Text(std::mem::take(&mut current)));
                 }
+                items.push(MdInline::Link {
+                    text: link_text,
+                    url,
+                });
+                i += consumed;
+                continue;
             }
             current.push(chars[i]);
             i += 1;
@@ -1061,20 +1063,22 @@ impl Context {
 
         while ci < chars.len() {
             // Image: ![alt](url) — char-based bracket scanner is reused as-is.
-            if chars[ci] == '!' && ci + 1 < chars.len() && chars[ci + 1] == '[' {
-                if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1) {
-                    result.push_str(&alt);
-                    ci += 1 + consumed;
-                    continue;
-                }
+            if chars[ci] == '!'
+                && ci + 1 < chars.len()
+                && chars[ci + 1] == '['
+                && let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1)
+            {
+                result.push_str(&alt);
+                ci += 1 + consumed;
+                continue;
             }
             // Link: [text](url)
-            if chars[ci] == '[' {
-                if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci) {
-                    result.push_str(&link_text);
-                    ci += consumed;
-                    continue;
-                }
+            if chars[ci] == '['
+                && let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci)
+            {
+                result.push_str(&link_text);
+                ci += consumed;
+                continue;
             }
 
             let bi = char_to_byte[ci];
@@ -1130,7 +1134,7 @@ impl Context {
     /// (vi `gg`, leader keys).
     ///
     /// Unlike a single-frame matcher, `key_chord` buffers partial input in
-    /// [`FrameState`](crate::FrameState) across frames: typing `g` on one frame
+    /// crate-internal `FrameState` across frames: typing `g` on one frame
     /// and `g` on the next returns `true` on the second frame. The partial
     /// prefix is cleared on a non-matching key press (vi semantics: `g` then
     /// `x` cancels a pending `gg`) or after
diff --git a/src/context/widgets_interactive/selection.rs b/src/context/widgets_interactive/selection.rs
index bb1c3fd..6f69e0f 100644
--- a/src/context/widgets_interactive/selection.rs
+++ b/src/context/widgets_interactive/selection.rs
@@ -518,11 +518,7 @@ impl Context {
                 let s = Style::new()
                     .fg(colors.accent.unwrap_or(self.theme.primary))
                     .bold();
-                if focused {
-                    s.underline()
-                } else {
-                    s
-                }
+                if focused { s.underline() } else { s }
             } else {
                 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
             };
@@ -1574,7 +1570,7 @@ impl Context {
             match state.mode {
                 PickerMode::Palette => match key.code {
                     KeyCode::Left | KeyCode::Char('h') => {
-                        if state.selected % columns > 0 {
+                        if !state.selected.is_multiple_of(columns) {
                             state.selected -= 1;
                         }
                         consumed_indices.push(i);
diff --git a/src/context/widgets_viz.rs b/src/context/widgets_viz.rs
index d8a9e39..7de538f 100644
--- a/src/context/widgets_viz.rs
+++ b/src/context/widgets_viz.rs
@@ -812,11 +812,7 @@ impl Context {
                     let (v1, _) = data[idx];
                     let (v2, _) = data[(idx + 1).min(data.len() - 1)];
                     let value = if v1.is_nan() || v2.is_nan() {
-                        if frac < 0.5 {
-                            v1
-                        } else {
-                            v2
-                        }
+                        if frac < 0.5 { v1 } else { v2 }
                     } else {
                         v1 * (1.0 - frac) + v2 * frac
                     };
@@ -1461,6 +1457,7 @@ impl Context {
     }
 
     #[cfg(feature = "qrcode")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "qrcode")))]
     /// Render a QR code using half-block characters.
     pub fn qr_code(&mut self, data: impl AsRef) -> Response {
         let code = match qrcode::QrCode::new(data.as_ref()) {
@@ -1502,11 +1499,7 @@ impl Context {
                                     matches!(modules.get(idx), Some(qrcode::types::Color::Dark))
                                 };
 
-                            if dark {
-                                theme_text
-                            } else {
-                                theme_bg
-                            }
+                            if dark { theme_text } else { theme_bg }
                         };
 
                         let upper = resolve_module_color(x, upper_y);
diff --git a/src/keymap.rs b/src/keymap.rs
index a9bacee..4365b55 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -324,7 +324,7 @@ fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
 ///
 /// In other words, this trait is the same shape as `std::fmt::Display`: zero
 /// blanket / built-in impls; you opt in by implementing it on your own type.
-/// If you prefer a free-function call, [`Context::publish_keymap`] takes the
+/// If you prefer a free-function call, [`publish_keymap`](crate::Context::publish_keymap) takes the
 /// same `(name, &'static [...])` signature without the trait.
 ///
 /// # Format
@@ -432,8 +432,8 @@ impl crate::Context {
 #[cfg(test)]
 mod dispatch_tests {
     use super::*;
-    use crate::event::Event;
     use crate::TestBackend;
+    use crate::event::Event;
 
     fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
         match Event::key_mod(code, modifiers) {
@@ -498,9 +498,10 @@ mod dispatch_tests {
         assert_eq!(save.description, "Save");
 
         // Non-matching key returns None.
-        assert!(km
-            .matched(&key_event(KeyCode::Char('z'), KeyModifiers::NONE))
-            .is_none());
+        assert!(
+            km.matched(&key_event(KeyCode::Char('z'), KeyModifiers::NONE))
+                .is_none()
+        );
     }
 
     #[test]
diff --git a/src/layout.rs b/src/layout.rs
index 61a90c0..3fc532b 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -17,12 +17,12 @@ mod flexbox;
 mod render;
 mod tree;
 
-pub(crate) use collect::{collect_all, FrameData};
+pub(crate) use collect::{FrameData, collect_all};
 pub use command::Direction;
 pub(crate) use command::{BeginContainerArgs, BeginScrollableArgs, Command};
 pub(crate) use flexbox::compute;
-pub(crate) use render::{render, render_debug_overlay, render_inspector, InspectorFocus};
-pub(crate) use tree::{build_tree, wrap_lines, wrap_segments, LayoutNode, NodeKind};
+pub(crate) use render::{InspectorFocus, render, render_debug_overlay, render_inspector};
+pub(crate) use tree::{LayoutNode, NodeKind, build_tree, wrap_lines, wrap_segments};
 
 /// Test-only entry point exposing `wrap_segments` for allocation-budget tests.
 ///
diff --git a/src/layout/collect.rs b/src/layout/collect.rs
index 4f16a74..1bb8077 100644
--- a/src/layout/collect.rs
+++ b/src/layout/collect.rs
@@ -66,13 +66,13 @@ pub(crate) fn collect_all(node: &LayoutNode, data: &mut FrameData) {
         data.scroll_rects
             .push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
     }
-    if let Some(id) = node.focus_id {
-        if node.pos.1 + node.size.1 > 0 {
-            data.focus_rects.push((
-                id,
-                Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
-            ));
-        }
+    if let Some(id) = node.focus_id
+        && node.pos.1 + node.size.1 > 0
+    {
+        data.focus_rects.push((
+            id,
+            Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
+        ));
     }
     if let Some(id) = node.interaction_id {
         let rect = if node.pos.1 + node.size.1 > 0 {
@@ -212,13 +212,14 @@ fn collect_all_inner(
     // collect-time handoff costs zero allocations regardless of group depth.
     let node_group_arc: Option> = node.group_name.clone();
 
-    if let Some(name) = &node_group_arc {
-        if node.pos.1 + node.size.1 > y_offset && node.pos.0 + node.size.0 > x_offset {
-            data.group_rects.push((
-                Arc::clone(name),
-                Rect::new(adj_x, adj_y, node.size.0, node.size.1),
-            ));
-        }
+    if let Some(name) = &node_group_arc
+        && node.pos.1 + node.size.1 > y_offset
+        && node.pos.0 + node.size.0 > x_offset
+    {
+        data.group_rects.push((
+            Arc::clone(name),
+            Rect::new(adj_x, adj_y, node.size.0, node.size.1),
+        ));
     }
 
     if matches!(node.kind, NodeKind::Container(_)) {
@@ -231,11 +232,12 @@ fn collect_all_inner(
         data.content_areas.push((full, content));
     }
 
-    if let Some(id) = node.focus_id {
-        if node.pos.1 + node.size.1 > y_offset && node.pos.0 + node.size.0 > x_offset {
-            data.focus_rects
-                .push((id, Rect::new(adj_x, adj_y, node.size.0, node.size.1)));
-        }
+    if let Some(id) = node.focus_id
+        && node.pos.1 + node.size.1 > y_offset
+        && node.pos.0 + node.size.0 > x_offset
+    {
+        data.focus_rects
+            .push((id, Rect::new(adj_x, adj_y, node.size.0, node.size.1)));
     }
 
     let current_group = node_group_arc.as_ref().or(active_group);
diff --git a/src/layout/flexbox.rs b/src/layout/flexbox.rs
index 8c707fa..e6f6122 100644
--- a/src/layout/flexbox.rs
+++ b/src/layout/flexbox.rs
@@ -525,6 +525,12 @@ fn layout_row_line(
 
     for child in children.iter_mut() {
         resolve_axis_specs(&mut child.constraints, area);
+        // Resolving `Pct`/`Ratio` -> `Fixed` is the only mid-frame mutation
+        // that can change a child's intrinsic min sizes, so drop any value
+        // memoized during an ancestor's measurement pass (which saw the
+        // pre-resolution constraint). For `Auto`/`Fixed`/`MinMax` this is a
+        // no-op resolution and a cheap cache clear.
+        child.invalidate_size_cache();
     }
 
     let n = children.len() as u32;
@@ -725,6 +731,9 @@ fn layout_column(node: &mut LayoutNode, area: Rect, depth: usize) {
 
     for child in &mut node.children {
         resolve_axis_specs(&mut child.constraints, area);
+        // See `layout_row_line`: discard any intrinsic-size memo cached before
+        // this child's `Pct`/`Ratio` constraints were resolved.
+        child.invalidate_size_cache();
     }
 
     let n = node.children.len() as u32;
diff --git a/src/layout/render.rs b/src/layout/render.rs
index 6f1efd2..1ceb9ec 100644
--- a/src/layout/render.rs
+++ b/src/layout/render.rs
@@ -34,12 +34,27 @@ pub(crate) fn render(node: &LayoutNode, buf: &mut Buffer) {
 /// Retained for the fallback path in [`render`] when a modal has zero size
 /// (degenerate, but possible during transitions). The hot path uses
 /// [`dim_buffer_around`] which scans only the four strips outside the modal.
+///
+/// Walks `buf.content` as one contiguous slice rather than per-cell
+/// `get_mut` so the per-cell bounds assert is paid zero times and the
+/// compiler can vectorize the modifier OR over the whole grid.
 fn dim_entire_buffer(buf: &mut Buffer) {
-    for y in buf.area.y..buf.area.bottom() {
-        for x in buf.area.x..buf.area.right() {
-            let cell = buf.get_mut(x, y);
-            cell.style.modifiers |= crate::style::Modifiers::DIM;
-        }
+    for cell in &mut buf.content {
+        cell.style.modifiers |= crate::style::Modifiers::DIM;
+    }
+}
+
+/// OR the `DIM` modifier into a contiguous run of cells `[start, end)` in
+/// `buf.content`. `end` is clamped to the content length defensively, but
+/// callers always pass in-bounds indices derived from `area`.
+#[inline]
+fn dim_cell_range(content: &mut [crate::cell::Cell], start: usize, end: usize) {
+    let end = end.min(content.len());
+    if start >= end {
+        return;
+    }
+    for cell in &mut content[start..end] {
+        cell.style.modifiers |= crate::style::Modifiers::DIM;
     }
 }
 
@@ -76,33 +91,41 @@ fn dim_buffer_around(buf: &mut Buffer, modal_rect: Rect) {
         return;
     }
 
-    // Top strip: rows above the modal, full width of the buffer.
-    for y in area.y..clip_y {
-        for x in area.x..area.right() {
-            let cell = buf.get_mut(x, y);
-            cell.style.modifiers |= crate::style::Modifiers::DIM;
-        }
-    }
-    // Bottom strip: rows below the modal, full width of the buffer.
-    for y in clip_bottom..area.bottom() {
-        for x in area.x..area.right() {
-            let cell = buf.get_mut(x, y);
-            cell.style.modifiers |= crate::style::Modifiers::DIM;
-        }
-    }
-    // Left strip: columns left of the modal, only across the modal's row band.
+    // Operate on `buf.content` directly as contiguous per-row slices: each
+    // strip becomes a flat index range with no per-cell bounds assert, which
+    // the compiler can vectorize. Row-major layout means a single row's
+    // columns `[col_lo, col_hi)` are the contiguous range
+    // `row_base + (col_lo - area.x) .. row_base + (col_hi - area.x)`.
+    let width = area.width;
+    let content = &mut buf.content;
+
+    // Column offsets within a row, relative to the buffer's left edge
+    // (`area.x`). The left strip starts at offset 0, so it has no explicit
+    // `full_lo`.
+    let full_hi = (area.right() - area.x) as usize;
+    let left_hi = (clip_x - area.x) as usize;
+    let right_lo = (clip_right - area.x) as usize;
+
+    // Top strip: full-width rows above the modal. These rows are fully
+    // contiguous from content index 0, so dim them as one span.
+    if clip_y > area.y {
+        let end = ((clip_y - area.y) * width) as usize;
+        dim_cell_range(content, 0, end);
+    }
+    // Bottom strip: full-width rows below the modal — one contiguous span.
+    if area.bottom() > clip_bottom {
+        let start = ((clip_bottom - area.y) * width) as usize;
+        let end = ((area.bottom() - area.y) * width) as usize;
+        dim_cell_range(content, start, end);
+    }
+    // Left and right strips: only across the modal's row band. Each row's
+    // left/right segment is contiguous within that row.
     for y in clip_y..clip_bottom {
-        for x in area.x..clip_x {
-            let cell = buf.get_mut(x, y);
-            cell.style.modifiers |= crate::style::Modifiers::DIM;
-        }
-    }
-    // Right strip: columns right of the modal, only across the modal's row band.
-    for y in clip_y..clip_bottom {
-        for x in clip_right..area.right() {
-            let cell = buf.get_mut(x, y);
-            cell.style.modifiers |= crate::style::Modifiers::DIM;
-        }
+        let row_base = ((y - area.y) * width) as usize;
+        // Left segment: columns [area.x, clip_x) -> offsets [0, left_hi).
+        dim_cell_range(content, row_base, row_base + left_hi);
+        // Right segment: columns [clip_right, area.right()).
+        dim_cell_range(content, row_base + right_lo, row_base + full_hi);
     }
 }
 
@@ -863,13 +886,13 @@ fn render_inner(
         }
         NodeKind::Spacer | NodeKind::RawDraw(_) => {}
         NodeKind::Container(_) => {
-            if let Some(color) = node.bg_color {
-                if let Some(area) = visible_area(node, x_offset, y_offset) {
-                    let fill_style = Style::new().bg(color);
-                    for y in area.y..area.bottom() {
-                        for x in area.x..area.right() {
-                            buf.set_string(x, y, " ", fill_style);
-                        }
+            if let Some(color) = node.bg_color
+                && let Some(area) = visible_area(node, x_offset, y_offset)
+            {
+                let fill_style = Style::new().bg(color);
+                for y in area.y..area.bottom() {
+                    for x in area.x..area.right() {
+                        buf.set_string(x, y, " ", fill_style);
                     }
                 }
             }
@@ -1057,35 +1080,36 @@ fn render_container_border(
         }
     }
 
-    if sides.top && top_i >= 0 {
-        if let Some((title, title_style)) = &node.title {
-            let mut ts = *title_style;
-            if ts.bg.is_none() {
-                ts.bg = inherit_bg;
-            }
-            let y = top_i as u32;
-            let title_x = left_i + 2;
-            // The right corner sits at `right_i`. When the right side is drawn
-            // we must keep that column intact, so the writable title area ends
-            // at `right_i - 1`. With no right border we can use the full row.
-            let title_right = if sides.right { right_i - 1 } else { right_i };
-            if title_x <= title_right && title_right >= 0 {
-                // `max_width` is the title window width measured from `title_x`
-                // (which may be negative when scrolled left); the clipped writer
-                // trims any leading columns past the left edge (#247).
-                let max_width = (title_right - title_x + 1).max(0) as usize;
-                let mut trimmed = String::new();
-                let mut col_used = 0usize;
-                for ch in title.chars() {
-                    let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
-                    if col_used + cw > max_width {
-                        break;
-                    }
-                    trimmed.push(ch);
-                    col_used += cw;
+    if sides.top
+        && top_i >= 0
+        && let Some((title, title_style)) = &node.title
+    {
+        let mut ts = *title_style;
+        if ts.bg.is_none() {
+            ts.bg = inherit_bg;
+        }
+        let y = top_i as u32;
+        let title_x = left_i + 2;
+        // The right corner sits at `right_i`. When the right side is drawn
+        // we must keep that column intact, so the writable title area ends
+        // at `right_i - 1`. With no right border we can use the full row.
+        let title_right = if sides.right { right_i - 1 } else { right_i };
+        if title_x <= title_right && title_right >= 0 {
+            // `max_width` is the title window width measured from `title_x`
+            // (which may be negative when scrolled left); the clipped writer
+            // trims any leading columns past the left edge (#247).
+            let max_width = (title_right - title_x + 1).max(0) as usize;
+            let mut trimmed = String::new();
+            let mut col_used = 0usize;
+            for ch in title.chars() {
+                let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
+                if col_used + cw > max_width {
+                    break;
                 }
-                set_string_clipped_x(buf, title_x, y, &trimmed, ts, None);
+                trimmed.push(ch);
+                col_used += cw;
             }
+            set_string_clipped_x(buf, title_x, y, &trimmed, ts, None);
         }
     }
 }
diff --git a/src/layout/tests.rs b/src/layout/tests.rs
index 50a204d..6eff517 100644
--- a/src/layout/tests.rs
+++ b/src/layout/tests.rs
@@ -1,7 +1,7 @@
 #![allow(clippy::print_stderr)]
 #![allow(clippy::unwrap_used)]
 
-use super::tree::{default_container_config, ContainerConfig};
+use super::tree::{ContainerConfig, default_container_config};
 use super::*;
 
 #[test]
@@ -2546,3 +2546,110 @@ mod hscroll_proptest {
         }
     }
 }
+
+// ---------------------------------------------------------------------------
+// Per-frame intrinsic-size memo (v0.22.0): the memoization of `min_width` /
+// `min_height` / `min_height_for_width` must stay byte-identical to the
+// uncached computation. The subtle case is a `Pct`/`Ratio` constraint: such a
+// node is measured by its grandparent while still unresolved, then resolved
+// (`Pct` -> `Fixed`) by its parent's layout pass. The cache must be discarded
+// at the resolution point so the post-resolution query reflects the resolved
+// width, not the stale pre-resolution value.
+// ---------------------------------------------------------------------------
+
+/// Build a `Direction::Row` container holding `children`.
+fn row_with(children: Vec) -> LayoutNode {
+    let mut n = LayoutNode::container(Direction::Row, default_container_config());
+    n.children = children;
+    n
+}
+
+/// Build a `Direction::Column` container holding `children`.
+fn col_with(children: Vec) -> LayoutNode {
+    let mut n = LayoutNode::container(Direction::Column, default_container_config());
+    n.children = children;
+    n
+}
+
+#[test]
+fn pct_width_child_resolves_after_being_measured_unresolved() {
+    // Tree: col > row > [ pct-50 col > text , fixed text ]
+    // The inner pct column is measured by the outer row/col chain while its
+    // `Pct(50)` width is still unresolved (min_width returns None for Pct),
+    // then the row resolves it to Fixed(width/2). The memo must not pin the
+    // pre-resolution value.
+    let leaf = LayoutNode::text(
+        "x".to_string(),
+        Style::new(),
+        0,
+        Align::Start,
+        (None, false, false),
+        Margin::default(),
+        Constraints::default(),
+    );
+    let mut pct_col = col_with(vec![leaf]);
+    pct_col.constraints = Constraints::default().w_pct(50);
+
+    let fixed_text = LayoutNode::text(
+        "y".to_string(),
+        Style::new(),
+        0,
+        Align::Start,
+        (None, false, false),
+        Margin::default(),
+        Constraints::default(),
+    );
+
+    let mut root = col_with(vec![row_with(vec![pct_col, fixed_text])]);
+    let area = crate::rect::Rect::new(0, 0, 40, 10);
+    compute(&mut root, area);
+
+    // The pct child must occupy exactly 50% of the 40-wide area = 20 cells,
+    // identical to the uncached path. A stale `min_width` (cached as the
+    // unresolved Pct min of 0) would have collapsed it.
+    let row = &root.children[0];
+    assert_eq!(
+        row.children[0].constraints.width,
+        crate::style::WidthSpec::Fixed(20),
+        "pct constraint must resolve to Fixed(20)"
+    );
+    assert_eq!(
+        row.children[0].size.0, 20,
+        "pct child must be sized to its resolved 50% width, not a stale memo"
+    );
+}
+
+#[test]
+fn min_width_memo_matches_uncached_after_resolution() {
+    // Direct check that the cached `min_width` equals a freshly-resolved
+    // computation. Build a node, force a cold `min_width` while its Pct is
+    // unresolved, then resolve + invalidate and confirm the value updates.
+    let mut node = LayoutNode::container(Direction::Column, default_container_config());
+    node.constraints = Constraints::default().w_pct(50);
+    node.children.push(LayoutNode::text(
+        "abcd".to_string(),
+        Style::new(),
+        0,
+        Align::Start,
+        (None, false, false),
+        Margin::default(),
+        Constraints::default(),
+    ));
+
+    // Cold query with Pct unresolved: Pct contributes no min, so the min_width
+    // is driven by the child text width (4).
+    let unresolved = node.min_width();
+    assert_eq!(unresolved, 4);
+    // A second call hits the memo and returns the same value.
+    assert_eq!(node.min_width(), 4);
+
+    // Resolve the Pct against a 40-wide area -> Fixed(20), then invalidate as
+    // the flex loop does. The memo must now reflect the resolved min (20).
+    super::flexbox::resolve_axis_specs(&mut node.constraints, crate::rect::Rect::new(0, 0, 40, 4));
+    node.invalidate_size_cache();
+    assert_eq!(
+        node.min_width(),
+        20,
+        "after Pct resolves to Fixed(20), min_width must update, not return the stale cached 4"
+    );
+}
diff --git a/src/layout/tree.rs b/src/layout/tree.rs
index 0f4fb9e..fde9df7 100644
--- a/src/layout/tree.rs
+++ b/src/layout/tree.rs
@@ -25,7 +25,19 @@ use super::*;
 /// `scroll_offset_x: u32` (4 bytes) and `content_width: u32` (4 bytes), the
 /// x-axis mirror of `scroll_offset` / `content_height`. Same scalar rationale
 /// as #258 — boxing 4-byte fields would cost more than it saves.
-const _ASSERT_LAYOUT_NODE_SIZE: () = assert!(std::mem::size_of::() <= 336);
+/// Bumped 336 → 360 for the per-frame intrinsic-size memo (v0.22.0):
+/// `cached_min_width` / `cached_min_height` (`Cell>`, 8 bytes
+/// each) and `cached_min_height_for_width` (`Cell>`,
+/// 12 bytes). These memoize the previously-recomputed top-down intrinsic
+/// queries (`min_width` / `min_height` / `min_height_for_width`) so each
+/// node is measured at most once per width per frame instead of being
+/// re-walked O(depth) times by the flexbox call sites. The fields are
+/// `Cell`s so memoization works through the `&self` query signatures; the
+/// tree is rebuilt fresh every frame, so they start `None` and need no
+/// invalidation. Scalars (no heap), so this is the minimum footprint —
+/// boxing the cache would cost a per-node allocation every frame, defeating
+/// the optimization.
+const _ASSERT_LAYOUT_NODE_SIZE: () = assert!(std::mem::size_of::() <= 360);
 
 #[derive(Debug, Clone)]
 pub(crate) struct OverlayLayer {
@@ -157,6 +169,19 @@ pub(crate) struct LayoutNode {
     /// rather than a fresh `String` → `Arc` allocation per group node.
     pub(crate) group_name: Option>,
     pub(crate) overlays: Vec,
+    /// Per-frame memo for [`LayoutNode::min_width`]. `None` until the first
+    /// query; the value is width-independent, so it never needs a key. The
+    /// tree is rebuilt fresh every frame, so a new node starts `None` — no
+    /// invalidation logic exists or is needed.
+    pub(crate) cached_min_width: std::cell::Cell>,
+    /// Per-frame memo for [`LayoutNode::min_height`]. Same width-independent
+    /// semantics as [`LayoutNode::cached_min_width`].
+    pub(crate) cached_min_height: std::cell::Cell>,
+    /// Per-frame memo for [`LayoutNode::min_height_for_width`], keyed by the
+    /// `available_width` it was computed for: `Some((width, result))`. A query
+    /// at a different width recomputes and overwrites (the flex pipeline queries
+    /// each node at one settled width, so this is effectively a one-slot cache).
+    pub(crate) cached_min_height_for_width: std::cell::Cell>,
 }
 
 #[derive(Debug, Clone)]
@@ -267,6 +292,9 @@ impl LayoutNode {
             link_url: None,
             group_name: None,
             overlays: Vec::new(),
+            cached_min_width: std::cell::Cell::new(None),
+            cached_min_height: std::cell::Cell::new(None),
+            cached_min_height_for_width: std::cell::Cell::new(None),
         }
     }
 
@@ -320,6 +348,9 @@ impl LayoutNode {
             link_url: None,
             group_name: None,
             overlays: Vec::new(),
+            cached_min_width: std::cell::Cell::new(None),
+            cached_min_height: std::cell::Cell::new(None),
+            cached_min_height_for_width: std::cell::Cell::new(None),
         }
     }
 
@@ -360,6 +391,9 @@ impl LayoutNode {
             link_url: None,
             group_name: None,
             overlays: Vec::new(),
+            cached_min_width: std::cell::Cell::new(None),
+            cached_min_height: std::cell::Cell::new(None),
+            cached_min_height_for_width: std::cell::Cell::new(None),
         }
     }
 
@@ -418,6 +452,9 @@ impl LayoutNode {
             link_url: None,
             group_name: None,
             overlays: Vec::new(),
+            cached_min_width: std::cell::Cell::new(None),
+            cached_min_height: std::cell::Cell::new(None),
+            cached_min_height_for_width: std::cell::Cell::new(None),
         }
     }
 
@@ -458,15 +495,14 @@ impl LayoutNode {
             link_url: None,
             group_name: None,
             overlays: Vec::new(),
+            cached_min_width: std::cell::Cell::new(None),
+            cached_min_height: std::cell::Cell::new(None),
+            cached_min_height_for_width: std::cell::Cell::new(None),
         }
     }
 
     pub(crate) fn border_inset(&self) -> u32 {
-        if self.border.is_some() {
-            1
-        } else {
-            0
-        }
+        if self.border.is_some() { 1 } else { 0 }
     }
 
     pub(crate) fn border_left_inset(&self) -> u32 {
@@ -509,7 +545,27 @@ impl LayoutNode {
         self.padding.vertical() + self.border_top_inset() + self.border_bottom_inset()
     }
 
+    /// Width-independent intrinsic minimum width, memoized for the frame.
+    ///
+    /// The result is a pure function of this node's (post-build, immutable)
+    /// subtree and its own constraints. The flexbox call sites query the same
+    /// node's `min_width` repeatedly (base widths, cross-align, overflow sums),
+    /// and `min_width` itself recurses over the whole subtree, so without the
+    /// memo the cost is O(nodes x depth). The cache is cleared whenever a
+    /// parent resolves this node's `Pct`/`Ratio` constraints into `Fixed`
+    /// (see [`LayoutNode::invalidate_size_cache`]) — the only mutation that can
+    /// change the result mid-frame — so the memoized value stays byte-identical
+    /// to the uncached computation.
     pub(crate) fn min_width(&self) -> u32 {
+        if let Some(cached) = self.cached_min_width.get() {
+            return cached;
+        }
+        let width = self.min_width_uncached();
+        self.cached_min_width.set(Some(width));
+        width
+    }
+
+    fn min_width_uncached(&self) -> u32 {
         let width = match self.kind {
             NodeKind::Text => self.size.0,
             NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
@@ -542,7 +598,37 @@ impl LayoutNode {
         width.saturating_add(self.margin.horizontal())
     }
 
+    /// Clear this node's per-frame intrinsic-size memos.
+    ///
+    /// Called by the flexbox layout loops at the exact point a parent resolves
+    /// a child's `Pct`/`Ratio` constraints into concrete `Fixed` values
+    /// (`flexbox::resolve_axis_specs`). That resolution is the only mid-frame
+    /// mutation that can change `min_width` / `min_height` /
+    /// `min_height_for_width`, so discarding any value cached during an
+    /// ancestor's measurement pass (when the constraint was still unresolved)
+    /// keeps the post-resolution query byte-identical to the uncached path.
+    /// For the common `Auto` / `Fixed` / `MinMax` case the resolution is a
+    /// no-op and the clear is cheap (three `Cell::set(None)`).
+    #[inline]
+    pub(crate) fn invalidate_size_cache(&self) {
+        self.cached_min_width.set(None);
+        self.cached_min_height.set(None);
+        self.cached_min_height_for_width.set(None);
+    }
+
+    /// Width-independent intrinsic minimum height, memoized for the frame.
+    ///
+    /// Same caching contract as [`LayoutNode::min_width`].
     pub(crate) fn min_height(&self) -> u32 {
+        if let Some(cached) = self.cached_min_height.get() {
+            return cached;
+        }
+        let height = self.min_height_uncached();
+        self.cached_min_height.set(Some(height));
+        height
+    }
+
+    fn min_height_uncached(&self) -> u32 {
         let height = match self.kind {
             NodeKind::Text => 1,
             NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
@@ -606,6 +692,12 @@ impl LayoutNode {
     pub(crate) fn min_height_for_width(&mut self, available_width: u32) -> u32 {
         match self.kind {
             NodeKind::Text if self.wrap => {
+                // Not memoized via `cached_min_height_for_width`: the wrap
+                // cache populated by `ensure_wrapped_for_width` (keyed by its
+                // own `cached_wrap_width`) is the side effect `compute_body` /
+                // `render` depend on, and it already short-circuits repeated
+                // same-width calls. Adding a second memo here would risk
+                // skipping that population, so we defer to the existing cache.
                 let inner_width = available_width.saturating_sub(self.margin.horizontal());
                 let lines = self.ensure_wrapped_for_width(inner_width);
                 lines.saturating_add(self.margin.vertical())
@@ -615,9 +707,24 @@ impl LayoutNode {
             // width-independent `min_height`. Partition the children greedily
             // (mirroring `flexbox::layout_row`'s wrap pass) and sum each line's
             // tallest child plus the between-line cross-axis gap. Closes #258.
+            //
+            // Memoized keyed by `available_width`: the partition re-walks the
+            // children, so caching the (width, result) pair avoids recomputing
+            // it when the flex pipeline re-queries the same row at the same
+            // settled width. A query at a different width recomputes and
+            // overwrites the single slot.
             NodeKind::Container(Direction::Row) if self.wrap_children => {
-                self.wrapped_min_height(available_width)
+                if let Some((w, h)) = self.cached_min_height_for_width.get()
+                    && w == available_width
+                {
+                    return h;
+                }
+                let h = self.wrapped_min_height(available_width);
+                self.cached_min_height_for_width
+                    .set(Some((available_width, h)));
+                h
             }
+            // Width-independent: delegates to the (cached) `min_height`.
             _ => self.min_height(),
         }
     }
diff --git a/src/lib.rs b/src/lib.rs
index d050751..46bc9f2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -19,20 +19,27 @@
 //! }
 //! ```
 
-// Safety
-#![forbid(unsafe_code)]
-// Documentation
-#![cfg_attr(docsrs, feature(doc_cfg))]
-#![warn(rustdoc::broken_intra_doc_links)]
+// Safety: the shipping library is 100% safe. Unit tests are excused only
+// because edition 2024 made `std::env::set_var`/`remove_var` `unsafe`, and a
+// few `#[cfg(test)]` terminal-detection helpers must mutate process env (they
+// serialize via a mutex). `forbid` stays on for every non-test build.
+#![cfg_attr(not(test), forbid(unsafe_code))]
+#![cfg_attr(test, deny(unsafe_code))]
+// Cross-target lints (rustdoc links, rust-2018-idioms) are configured
+// centrally in [workspace.lints] and applied via `[lints] workspace = true` in
+// Cargo.toml. The lints below stay here as lib-only inner attributes on
+// purpose: `[lints]` is package-scoped and would otherwise fire on the
+// package's example binaries and integration tests, which legitimately expose
+// undocumented `pub` helpers, print to stdout, and unwrap. The cfg-conditional
+// unsafe_code policy above likewise can't live in workspace.lints.
 #![warn(missing_docs)]
-#![warn(rustdoc::private_intra_doc_links)]
-// Correctness
+#![warn(unreachable_pub)]
 #![deny(clippy::unwrap_in_result)]
 #![warn(clippy::unwrap_used)]
-// Library hygiene — a library must not write to stdout/stderr
 #![warn(clippy::dbg_macro)]
 #![warn(clippy::print_stdout)]
 #![warn(clippy::print_stderr)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
 
 //! # SLT — Super Light TUI
 //!
@@ -156,9 +163,11 @@ pub use terminal::{__BenchSprixelFixture, __bench_flush_sprixels, __bench_new_sp
 /// snapshot plus the [`Blitter`] ladder it drives. Diagnostics-only — image
 /// rendering routes through the ladder automatically.
 #[cfg(feature = "crossterm")]
-pub use terminal::{capabilities, Blitter, BlitterSupport, Capabilities};
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
+pub use terminal::{Blitter, BlitterSupport, Capabilities, capabilities};
 #[cfg(feature = "crossterm")]
-pub use terminal::{detect_color_scheme, read_clipboard, ColorScheme};
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
+pub use terminal::{ColorScheme, detect_color_scheme, read_clipboard};
 #[cfg(feature = "crossterm")]
 use terminal::{InlineTerminal, Terminal};
 
@@ -166,6 +175,7 @@ pub use crate::test_utils::{EventBuilder, FrameRecord, TestBackend, TestSequence
 /// PTY/sink test harness for end-to-end escape-byte assertions (issue #274).
 /// Gated behind the dev-only `pty-test` feature; absent from default builds.
 #[cfg(feature = "pty-test")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pty-test")))]
 pub use crate::test_utils::{PtyBackend, PtyFrame};
 // Animation primitives (builder types) are re-exported at crate root for
 // ergonomic `use slt::{Tween, Spring, ...}`. The easing functions and `lerp`
@@ -185,6 +195,7 @@ pub use context::{
 };
 // Issue #234: opaque handle from `Context::spawn`, gated behind `async`.
 #[cfg(feature = "async")]
+#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
 pub use context::TaskHandle;
 pub use event::{
     Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, ModifierKey, MouseButton, MouseEvent,
@@ -196,6 +207,7 @@ pub use layout::Direction;
 pub use palette::Palette;
 pub use rect::Rect;
 #[cfg(feature = "theme-watch")]
+#[cfg_attr(docsrs, doc(cfg(feature = "theme-watch")))]
 pub use style::ThemeWatcher;
 pub use style::{
     Align, Border, BorderSides, Breakpoint, Color, ColorDepth, ColorParseError, Constraints,
@@ -203,21 +215,23 @@ pub use style::{
     Theme, ThemeBuilder, ThemeColor, UnderlineStyle, WidgetColors, WidgetTheme, WidthSpec,
 };
 #[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
 pub use style::{ThemeFile, ThemeLoadError};
-pub use widgets::validators;
 #[cfg(feature = "async")]
+#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
 pub use widgets::AsyncValidation;
+pub use widgets::validators;
 pub use widgets::{
     AlertLevel, ApprovalAction, BreadcrumbResponse, ButtonVariant, CalDate, CalendarSelect,
     CalendarState, ChordState, ColorPickerState, CommandPaletteState, ContextItem,
-    DirectoryTreeState, FileEntry, FilePickerState, FormField, FormState, GaugeResponse,
-    GridColumn, GutterResponse, HighlightRange, ListResponse, ListState, ModeState,
-    MultiSelectState, NumberInputState, PaginatorState, PaginatorStyle, PaletteCommand, PickerMode,
-    RadioState, RichLogEntry, RichLogState, SchedulerState, ScreenState, ScrollState, SelectState,
-    SpinnerPreset, SpinnerState, SplitPaneResponse, SplitPaneState, StaticOutput,
+    DEFAULT_CHORD_TIMEOUT_TICKS, DirectoryTreeState, FileEntry, FilePickerState, FormField,
+    FormState, GaugeResponse, GridColumn, GutterResponse, HighlightRange, ListResponse, ListState,
+    ModeState, MultiSelectState, NumberInputState, PaginatorState, PaginatorStyle, PaletteCommand,
+    PickerMode, RadioState, RichLogEntry, RichLogState, SchedulerState, ScreenState, ScrollState,
+    SelectState, SpinnerPreset, SpinnerState, SplitPaneResponse, SplitPaneState, StaticOutput,
     StreamingMarkdownState, StreamingTextState, TableColumn, TableState, TabsState, TextInputState,
     TextareaState, ToastLevel, ToastMessage, ToastState, ToolApprovalState, TreeNode, TreeState,
-    Trend, ValidateTrigger, Validator, DEFAULT_CHORD_TIMEOUT_TICKS,
+    Trend, ValidateTrigger, Validator,
 };
 
 /// Rendering backend for SLT.
@@ -1060,6 +1074,7 @@ pub(crate) struct FrameState {
 /// }
 /// ```
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
     run_with(RunConfig::default(), f)
 }
@@ -1094,6 +1109,7 @@ fn set_terminal_title(title: &Option) {
 /// }
 /// ```
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
     if !io::stdout().is_terminal() {
         return Ok(());
@@ -1184,6 +1200,7 @@ pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Resul
 /// # }
 /// ```
 #[cfg(all(feature = "crossterm", feature = "async"))]
+#[cfg_attr(docsrs, doc(cfg(all(feature = "crossterm", feature = "async"))))]
 pub fn run_async(
     f: impl FnMut(&mut Context, &mut Vec) + Send + 'static,
 ) -> io::Result> {
@@ -1197,6 +1214,7 @@ pub fn run_async(
 ///
 /// Returns a [`tokio::sync::mpsc::Sender`] for pushing messages into the UI.
 #[cfg(all(feature = "crossterm", feature = "async"))]
+#[cfg_attr(docsrs, doc(cfg(all(feature = "crossterm", feature = "async"))))]
 pub fn run_async_with(
     config: RunConfig,
     f: impl FnMut(&mut Context, &mut Vec) + Send + 'static,
@@ -1323,6 +1341,7 @@ fn run_async_loop(
 /// }
 /// ```
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
     run_inline_with(height, RunConfig::default(), f)
 }
@@ -1332,6 +1351,7 @@ pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
 /// Like [`run_inline`], but accepts a [`RunConfig`] to control tick rate,
 /// mouse support, and theming.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run_inline_with(
     height: u32,
     config: RunConfig,
@@ -1413,6 +1433,7 @@ pub fn run_inline_with(
 ///
 /// Use this when you want a log-style output stream above a live inline UI.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run_static(
     output: &mut StaticOutput,
     dynamic_height: u32,
@@ -1426,6 +1447,7 @@ pub fn run_static(
 /// Like [`run_static`] but accepts a [`RunConfig`] for theme, mouse, tick rate,
 /// and other settings.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn run_static_with(
     output: &mut StaticOutput,
     dynamic_height: u32,
@@ -1538,10 +1560,10 @@ pub(crate) const KEYMAP_REGISTRY_NAMED_STATE_KEY: &str = "__slt_keymap_registry"
 /// `Context::publish_keymap` always sees a fresh empty buffer. Capacity is
 /// preserved by clearing the inner `Vec` rather than removing the entry.
 pub(crate) fn clear_keymap_registry(state: &mut FrameState) {
-    if let Some(boxed) = state.named_states.get_mut(KEYMAP_REGISTRY_NAMED_STATE_KEY) {
-        if let Some(vec) = boxed.downcast_mut::>() {
-            vec.clear();
-        }
+    if let Some(boxed) = state.named_states.get_mut(KEYMAP_REGISTRY_NAMED_STATE_KEY)
+        && let Some(vec) = boxed.downcast_mut::>()
+    {
+        vec.clear();
     }
 }
 
@@ -1555,10 +1577,10 @@ pub(crate) fn clear_keymap_registry(state: &mut FrameState) {
 /// drop them with a debug warning).
 #[cfg(feature = "crossterm")]
 pub(crate) fn drain_static_log(state: &mut FrameState) -> Vec {
-    if let Some(boxed) = state.named_states.get_mut(STATIC_LOG_NAMED_STATE_KEY) {
-        if let Some(buf) = boxed.downcast_mut::>() {
-            return std::mem::take(buf);
-        }
+    if let Some(boxed) = state.named_states.get_mut(STATIC_LOG_NAMED_STATE_KEY)
+        && let Some(buf) = boxed.downcast_mut::>()
+    {
+        return std::mem::take(buf);
     }
     Vec::new()
 }
diff --git a/src/rect.rs b/src/rect.rs
index a61cc98..7d0afc5 100644
--- a/src/rect.rs
+++ b/src/rect.rs
@@ -177,7 +177,7 @@ impl Rect {
     /// assert_eq!(rows, vec![2, 3, 4]);
     /// ```
     #[inline]
-    pub fn rows(&self) -> impl Iterator {
+    pub fn rows(&self) -> impl Iterator + use<> {
         self.y..self.bottom()
     }
 
@@ -194,7 +194,7 @@ impl Rect {
     /// assert_eq!(positions, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
     /// ```
     #[inline]
-    pub fn positions(&self) -> impl Iterator {
+    pub fn positions(&self) -> impl Iterator + use<> {
         let x_start = self.x;
         let x_end = self.right();
         let y_start = self.y;
diff --git a/src/style.rs b/src/style.rs
index 4cd5d21..98cf09e 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -10,8 +10,10 @@ mod theme_io;
 pub use color::{Color, ColorDepth, ColorParseError};
 pub use theme::{Spacing, SyntaxPalette, Theme, ThemeBuilder, ThemeColor};
 #[cfg(feature = "theme-watch")]
+#[cfg_attr(docsrs, doc(cfg(feature = "theme-watch")))]
 pub use theme_io::ThemeWatcher;
 #[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
 pub use theme_io::{ThemeFile, ThemeLoadError};
 
 /// Terminal size breakpoint for responsive layouts.
diff --git a/src/style/color.rs b/src/style/color.rs
index a2a2c3a..a03903e 100644
--- a/src/style/color.rs
+++ b/src/style/color.rs
@@ -189,11 +189,7 @@ impl Color {
     pub fn contrast_ratio(a: Color, b: Color) -> f32 {
         let la = a.luminance() + 0.05;
         let lb = b.luminance() + 0.05;
-        if la > lb {
-            la / lb
-        } else {
-            lb / la
-        }
+        if la > lb { la / lb } else { lb / la }
     }
 
     /// Returns `true` if the contrast ratio between two colors meets WCAG AA
@@ -419,7 +415,7 @@ impl std::fmt::Display for ColorParseError {
     }
 }
 
-impl std::error::Error for ColorParseError {}
+impl core::error::Error for ColorParseError {}
 
 impl std::str::FromStr for Color {
     type Err = ColorParseError;
@@ -609,11 +605,7 @@ fn hue_sextant(h: f32, c: f32, x: f32) -> (f32, f32, f32) {
 #[inline]
 fn wrap_hue(h: f32) -> f32 {
     let h = h % 360.0;
-    if h < 0.0 {
-        h + 360.0
-    } else {
-        h
-    }
+    if h < 0.0 { h + 360.0 } else { h }
 }
 
 /// Scale a `[0.0, 1.0]` channel to a rounded, clamped `u8`.
@@ -724,7 +716,7 @@ impl<'de> serde::Deserialize<'de> for Color {
         impl serde::de::Visitor<'_> for ColorVisitor {
             type Value = Color;
 
-            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                 f.write_str("a color token like \"#ff6b6b\", \"cyan\", or \"indexed:245\"")
             }
 
@@ -806,10 +798,10 @@ impl ColorDepth {
                 return Self::TrueColor;
             }
         }
-        if let Ok(term) = std::env::var("TERM") {
-            if term.contains("256color") {
-                return Self::EightBit;
-            }
+        if let Ok(term) = std::env::var("TERM")
+            && term.contains("256color")
+        {
+            return Self::EightBit;
         }
         Self::Basic
     }
diff --git a/src/style/theme.rs b/src/style/theme.rs
index 2f8493c..9b8d8e9 100644
--- a/src/style/theme.rs
+++ b/src/style/theme.rs
@@ -875,6 +875,7 @@ impl Theme {
     /// assert_eq!(theme.primary, slt::Color::Rgb(255, 107, 107));
     /// ```
     #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
     pub fn from_toml_str(src: &str) -> Result {
         ThemeFile::from_toml_str(src).map(|tf| tf.theme)
     }
@@ -897,12 +898,14 @@ impl Theme {
     /// println!("primary = {:?}", theme.primary);
     /// ```
     #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
     pub fn load(path: impl AsRef) -> Result {
         ThemeFile::load(path).map(|tf| tf.theme)
     }
 }
 
 /// Builder for creating custom themes with defaults from `Theme::dark()`.
+#[must_use = "ThemeBuilder does nothing until .build() is called"]
 pub struct ThemeBuilder {
     primary: Option,
     secondary: Option,
@@ -1036,7 +1039,7 @@ impl ThemeBuilder {
     /// Build the theme. Unfilled fields use [`Theme::dark()`] defaults.
     ///
     /// `match` is used in place of [`Option::unwrap_or`] so the entire
-    /// builder chain compiles in `const` context (MSRV 1.81). All fields
+    /// builder chain compiles in `const` context. All fields
     /// involved are `Copy`, so `Some(c) => c` is a plain bit-copy.
     pub const fn build(self) -> Theme {
         let d = Theme::dark();
diff --git a/src/style/theme_io.rs b/src/style/theme_io.rs
index bd45854..2911f24 100644
--- a/src/style/theme_io.rs
+++ b/src/style/theme_io.rs
@@ -45,8 +45,8 @@ impl std::fmt::Display for ThemeLoadError {
     }
 }
 
-impl std::error::Error for ThemeLoadError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+impl core::error::Error for ThemeLoadError {
+    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
         match self {
             ThemeLoadError::Io(e) => Some(e),
             ThemeLoadError::Parse(_) => None,
@@ -185,6 +185,7 @@ impl ThemeFile {
 /// .unwrap();
 /// ```
 #[cfg(feature = "theme-watch")]
+#[cfg_attr(docsrs, doc(cfg(feature = "theme-watch")))]
 pub struct ThemeWatcher {
     // Held to keep the watch alive; dropping it stops the background thread.
     _watcher: notify::RecommendedWatcher,
diff --git a/src/syntax.rs b/src/syntax.rs
index d2f9f24..563f516 100644
--- a/src/syntax.rs
+++ b/src/syntax.rs
@@ -902,12 +902,14 @@ mod tests {
     #[test]
     fn highlight_java_basic() {
         let theme = Theme::dark();
-        assert!(highlight_code(
-            "public class Main { public static void main(String[] args) {} }",
-            "java",
-            &theme
-        )
-        .is_some());
+        assert!(
+            highlight_code(
+                "public class Main { public static void main(String[] args) {} }",
+                "java",
+                &theme
+            )
+            .is_some()
+        );
     }
 
     #[cfg(feature = "syntax-ruby")]
diff --git a/src/terminal.rs b/src/terminal.rs
index e91f18c..617c4c1 100644
--- a/src/terminal.rs
+++ b/src/terminal.rs
@@ -91,7 +91,7 @@ pub(crate) struct KittyImageManager {
 
 impl KittyImageManager {
     /// Construct a new image manager with no uploaded images.
-    pub fn new() -> Self {
+    pub(crate) fn new() -> Self {
         Self {
             next_id: 1,
             uploaded: HashMap::new(),
@@ -107,7 +107,7 @@ impl KittyImageManager {
     /// diff comparison against `prev_placements`. Stored placements always
     /// include the offset (the displayed `y`) so re-emit detection works
     /// across resize even when the offset itself changes (issue #206).
-    pub fn flush(
+    pub(crate) fn flush(
         &mut self,
         stdout: &mut impl Write,
         current: &[KittyPlacement],
@@ -133,15 +133,15 @@ impl KittyImageManager {
         if !self.prev_placements.is_empty() {
             self.scratch_ids.clear();
             for p in &self.prev_placements {
-                if let Some(&img_id) = self.uploaded.get(&p.content_hash) {
-                    if !self.scratch_ids.contains(&img_id) {
-                        self.scratch_ids.push(img_id);
-                        // Delete all placements of this image (but keep image data)
-                        queue!(
-                            stdout,
-                            Print(format!("\x1b_Ga=d,d=i,i={},q=2\x1b\\", img_id))
-                        )?;
-                    }
+                if let Some(&img_id) = self.uploaded.get(&p.content_hash)
+                    && !self.scratch_ids.contains(&img_id)
+                {
+                    self.scratch_ids.push(img_id);
+                    // Delete all placements of this image (but keep image data)
+                    queue!(
+                        stdout,
+                        Print(format!("\x1b_Ga=d,d=i,i={},q=2\x1b\\", img_id))
+                    )?;
                 }
             }
         }
@@ -261,7 +261,7 @@ impl KittyImageManager {
     }
 
     /// Delete all images from the terminal (used on drop/cleanup).
-    pub fn delete_all(&self, stdout: &mut impl Write) -> io::Result<()> {
+    pub(crate) fn delete_all(&self, stdout: &mut impl Write) -> io::Result<()> {
         queue!(stdout, Print("\x1b_Ga=d,d=A,q=2\x1b\\"))
     }
 }
@@ -299,15 +299,15 @@ fn placement_eq_with_offset(
 fn compress_rgba(data: &[u8]) -> (Cow<'_, [u8]>, &'static str) {
     #[cfg(feature = "kitty-compress")]
     {
-        use flate2::write::ZlibEncoder;
         use flate2::Compression;
+        use flate2::write::ZlibEncoder;
         let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
-        if encoder.write_all(data).is_ok() {
-            if let Ok(compressed) = encoder.finish() {
-                // Only use compression if it actually saves space
-                if compressed.len() < data.len() {
-                    return (Cow::Owned(compressed), "o=z,");
-                }
+        if encoder.write_all(data).is_ok()
+            && let Ok(compressed) = encoder.finish()
+        {
+            // Only use compression if it actually saves space
+            if compressed.len() < data.len() {
+                return (Cow::Owned(compressed), "o=z,");
             }
         }
     }
@@ -320,7 +320,7 @@ fn compress_rgba(data: &[u8]) -> (Cow<'_, [u8]>, &'static str) {
 /// detection fails. Used by `kitty_image_fit` for accurate aspect ratio.
 ///
 /// Cached after first successful detection.
-pub fn cell_pixel_size() -> (u32, u32) {
+pub(crate) fn cell_pixel_size() -> (u32, u32) {
     use std::sync::OnceLock;
     static CACHED: OnceLock<(u32, u32)> = OnceLock::new();
     *CACHED.get_or_init(|| detect_cell_pixel_size().unwrap_or((8, 16)))
@@ -335,18 +335,23 @@ fn detect_cell_pixel_size() -> Option<(u32, u32)> {
     let response = read_osc_response(Duration::from_millis(100))?;
 
     // Parse: ESC [ 6 ;  ;  t
-    let body = response.strip_prefix("\x1b[6;").or_else(|| {
-        // CSI can also start with 0x9B (single-byte CSI)
-        let bytes = response.as_bytes();
-        if bytes.len() > 3 && bytes[0] == 0x9b && bytes[1] == b'6' && bytes[2] == b';' {
-            Some(&response[3..])
-        } else {
-            None
-        }
-    })?;
-    let body = body
-        .strip_suffix('t')
-        .or_else(|| body.strip_suffix("t\x1b"))?;
+    // Locate the reply anywhere in the buffer rather than anchoring to its
+    // start/end: interleaved control bytes — e.g. a pump-retirement nudge
+    // answer (`CSI 0 n`) from a previous reply session — may surround it.
+    let bytes = response.as_bytes();
+    let start = bytes
+        .windows(4)
+        .position(|w| w == b"\x1b[6;")
+        .map(|pos| pos + 4)
+        .or_else(|| {
+            // CSI can also start with 0x9B (single-byte CSI).
+            bytes
+                .windows(3)
+                .position(|w| w == [0x9b, b'6', b';'])
+                .map(|pos| pos + 3)
+        })?;
+    let tail = response.get(start..)?;
+    let body = &tail[..tail.find('t')?];
     let mut parts = body.split(';');
     let ch: u32 = parts.next()?.parse().ok()?;
     let cw: u32 = parts.next()?.parse().ok()?;
@@ -511,6 +516,7 @@ impl Capabilities {
 /// infrastructure with a bounded total timeout (≤150ms). On no reply every
 /// field falls back to a conservative default. Repeated calls are free.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn capabilities() -> Capabilities {
     use std::sync::OnceLock;
     static CACHED: OnceLock = OnceLock::new();
@@ -533,28 +539,31 @@ fn probe_capabilities() -> Capabilities {
     let mut out = io::stdout();
     // DA1 then DA2 in one write — both terminate with `c`, so a single
     // DA-aware read drains both replies (in order) when supported.
-    if write!(out, "\x1b[c\x1b[>c").is_ok() && out.flush().is_ok() {
-        if let Some(resp) = read_da_response(Duration::from_millis(90)) {
-            parse_da1(&resp, &mut caps);
-            parse_da2(&resp, &mut caps);
-        }
+    if write!(out, "\x1b[c\x1b[>c").is_ok()
+        && out.flush().is_ok()
+        && let Some(resp) = read_da_response(Duration::from_millis(90))
+    {
+        parse_da1(&resp, &mut caps);
+        parse_da2(&resp, &mut caps);
     }
 
     // Kitty graphics query: APC G a=q (query) with a 1×1 RGB direct payload.
     // Supporting terminals ack with `APC G i=31;OK ST`; others stay silent so
     // the bounded read just times out. Base64 of three zero bytes = "AAAA".
-    if write!(out, "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\").is_ok() && out.flush().is_ok() {
-        if let Some(resp) = read_osc_response(Duration::from_millis(30)) {
-            parse_kitty_graphics_ack(&resp, &mut caps);
-        }
+    if write!(out, "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\").is_ok()
+        && out.flush().is_ok()
+        && let Some(resp) = read_osc_response(Duration::from_millis(30))
+    {
+        parse_kitty_graphics_ack(&resp, &mut caps);
     }
 
     // XTGETTCAP for the `Tc` (truecolor) capname: DCS + q  ST.
     // `Tc` -> hex "5463".
-    if write!(out, "\x1bP+q5463\x1b\\").is_ok() && out.flush().is_ok() {
-        if let Some(resp) = read_osc_response(Duration::from_millis(30)) {
-            parse_xtgettcap_truecolor(&resp, &mut caps);
-        }
+    if write!(out, "\x1bP+q5463\x1b\\").is_ok()
+        && out.flush().is_ok()
+        && let Some(resp) = read_osc_response(Duration::from_millis(30))
+    {
+        parse_xtgettcap_truecolor(&resp, &mut caps);
     }
 
     // DECRQM for synchronized output (mode ?2026): CSI ? 2026 $ p. A supporting
@@ -564,18 +573,19 @@ fn probe_capabilities() -> Capabilities {
     // DECRPM-aware reader. A silent terminal leaves the resolution `Unknown`,
     // which the flush gate treats as "keep emitting" — preserving the historic
     // always-emit behavior on headless / non-answering hosts.
-    if write!(out, "\x1b[?2026$p").is_ok() && out.flush().is_ok() {
-        if let Some(resp) = read_decrpm_response(Duration::from_millis(30)) {
-            match parse_decrpm_sync_output(&resp) {
-                Some(true) => {
-                    caps.sync_output = true;
-                    let _ = SYNC_OUTPUT_RESOLUTION.set(SyncOutputResolution::Supported);
-                }
-                Some(false) => {
-                    let _ = SYNC_OUTPUT_RESOLUTION.set(SyncOutputResolution::Unsupported);
-                }
-                None => {}
+    if write!(out, "\x1b[?2026$p").is_ok()
+        && out.flush().is_ok()
+        && let Some(resp) = read_decrpm_response(Duration::from_millis(30))
+    {
+        match parse_decrpm_sync_output(&resp) {
+            Some(true) => {
+                caps.sync_output = true;
+                let _ = SYNC_OUTPUT_RESOLUTION.set(SyncOutputResolution::Supported);
+            }
+            Some(false) => {
+                let _ = SYNC_OUTPUT_RESOLUTION.set(SyncOutputResolution::Unsupported);
             }
+            None => {}
         }
     }
 
@@ -634,40 +644,137 @@ fn term_is_kitty_graphics_host() -> bool {
     term.contains("kitty") || matches!(term_program.as_str(), "ghostty" | "wezterm" | "kitty")
 }
 
-/// Read a Device-Attributes reply, which (unlike OSC) terminates with the byte
-/// `c` rather than BEL / ST. Drains up to two `c`-terminated CSI replies
-/// (DA1 + DA2) within the timeout so a combined `CSI c CSI > c` query yields
-/// both answers in one string.
+/// Process-wide pump that owns the only blocking `stdin` read used for
+/// terminal-reply probing. See [`read_stdin_reply`] for why it exists.
 #[cfg(feature = "crossterm")]
-fn read_da_response(timeout: Duration) -> Option {
+struct ReplyPump {
+    rx: std::sync::mpsc::Receiver,
+    /// `true` while a reader session wants bytes. The pump thread re-checks it
+    /// after every successful read and exits once it is cleared.
+    serve: std::sync::Arc,
+    /// Set by the pump thread on exit, distinguishing "parked inside a
+    /// blocking `read()`" (reusable) from "gone" (must respawn).
+    exited: std::sync::Arc,
+}
+
+#[cfg(feature = "crossterm")]
+static REPLY_PUMP: std::sync::Mutex> = std::sync::Mutex::new(None);
+
+/// Read one terminal reply from raw stdin, hard-bounded by `timeout`, stopping
+/// early once `is_complete` recognizes a full reply (or at the 4096-byte cap).
+///
+/// Why a pump thread: the previous readers gated a blocking
+/// `io::stdin().read()` behind `crossterm::event::poll()`. Those two observe
+/// different things — `poll()` answers "does crossterm's *internal event
+/// queue* have something?", while the raw `read()` waits for bytes on the
+/// stdin descriptor — and crossterm's poller consumes bytes from that same
+/// descriptor into its own parser. On a host that never answers probe queries
+/// (a detached tmux pane, `script`-style PTY wrappers, CI runners), `poll()`
+/// could return `true` for a queued non-byte event while raw stdin stayed
+/// empty, so the one-byte `read()` blocked forever *inside* the deadline loop
+/// and the application hung on a blank alternate screen before its first
+/// frame; later keystrokes were swallowed by crossterm's queue instead of
+/// unblocking it. Moving the only blocking `read()` onto a dedicated thread
+/// and waiting on a channel with `recv_timeout` makes every reply read
+/// genuinely bounded by its budget no matter what the host does.
+///
+/// The pump is a process-wide singleton so back-to-back probes share one byte
+/// stream instead of racing two readers for the same reply. After each
+/// session the thread is retired: `serve` is cleared and a DSR status query
+/// (`CSI 5 n`) nudges the terminal — an answering host replies `CSI 0 n`,
+/// which wakes the parked `read()`, the thread observes `serve == false` and
+/// exits, and the nudge bytes stay in the channel where the next session's
+/// drain discards them (they never reach the application's input stream). A
+/// host that answers nothing leaves the thread parked; it is reused by the
+/// next session, and at worst it swallows one byte of typeahead on a host
+/// class where, before this fix, startup deadlocked outright.
+#[cfg(feature = "crossterm")]
+fn read_stdin_reply(
+    timeout: Duration,
+    mut is_complete: impl FnMut(&[u8]) -> bool,
+) -> Option {
+    use std::sync::atomic::{AtomicBool, Ordering};
+    use std::sync::{Arc, mpsc};
+
     let deadline = Instant::now() + timeout;
-    let mut stdin = io::stdin();
-    let mut bytes = Vec::new();
-    let mut buf = [0u8; 1];
-    let mut terminators = 0usize;
 
-    while Instant::now() < deadline {
-        if !crossterm::event::poll(Duration::from_millis(10)).ok()? {
-            continue;
-        }
-        let read = stdin.read(&mut buf).ok()?;
-        if read == 0 {
-            continue;
+    let Ok(mut slot) = REPLY_PUMP.lock() else {
+        // Poisoned: a prior session panicked mid-read. Skip probing entirely
+        // rather than risk a second fault; every caller treats `None` as "the
+        // terminal stayed silent".
+        return None;
+    };
+
+    let pump = match slot.take().filter(|p| !p.exited.load(Ordering::Acquire)) {
+        Some(pump) => {
+            // A parked pump from an earlier session: its thread is still
+            // blocked in `read()` on a silent host. Reusing it (instead of
+            // spawning a second thread) is what prevents two readers from
+            // racing each other for the same reply bytes.
+            pump.serve.store(true, Ordering::Release);
+            pump
         }
-        bytes.push(buf[0]);
-        // `c` is the final byte of a DA reply. Stop once we have collected the
-        // expected pair (DA1 + DA2); also stop on a lone reply so a terminal
-        // that ignores DA2 does not stall the whole timeout.
-        if buf[0] == b'c' {
-            terminators += 1;
-            if terminators >= 2 {
-                break;
+        None => {
+            let (tx, rx) = mpsc::channel::();
+            let serve = Arc::new(AtomicBool::new(true));
+            let exited = Arc::new(AtomicBool::new(false));
+            let thread_serve = Arc::clone(&serve);
+            let thread_exited = Arc::clone(&exited);
+            let spawned = std::thread::Builder::new()
+                .name("slt-reply-pump".into())
+                .spawn(move || {
+                    let mut stdin = io::stdin();
+                    // One byte per read on purpose: a parked thread that wakes
+                    // on real key input forwards at most this single byte
+                    // before observing `serve == false` and exiting, so the
+                    // worst-case typeahead loss on a silent host is exactly
+                    // one byte (replies are short; the syscall-per-byte cost
+                    // is irrelevant for one-shot probes).
+                    let mut buf = [0u8; 1];
+                    loop {
+                        match stdin.read(&mut buf) {
+                            Ok(0) | Err(_) => break,
+                            Ok(_) => {
+                                if tx.send(buf[0]).is_err() {
+                                    thread_exited.store(true, Ordering::Release);
+                                    return;
+                                }
+                            }
+                        }
+                        if !thread_serve.load(Ordering::Acquire) {
+                            break;
+                        }
+                    }
+                    thread_exited.store(true, Ordering::Release);
+                });
+            if spawned.is_err() {
+                return None;
             }
+            ReplyPump { rx, serve, exited }
         }
-        if bytes.len() >= 4096 {
-            break;
-        }
+    };
+
+    // Discard bytes left over from a previous session: a reply that arrived
+    // after its deadline, or the retirement nudge's `CSI 0 n` answer.
+    while pump.rx.try_recv().is_ok() {}
+
+    let bytes = collect_reply(&pump.rx, deadline, &mut is_complete);
+
+    // Retire the thread so it does not sit on a pending `read()` competing
+    // with crossterm's event loop for real key input once the session ends.
+    // The nudge fires only under raw mode (the `run()` / session-enter probe
+    // paths): in cooked mode — e.g. a standalone `detect_color_scheme()`
+    // call — the terminal would *echo* its `CSI 0 n` answer into the user's
+    // scrollback as visible garbage, so there the parked thread is simply
+    // left for the next session to reuse.
+    pump.serve.store(false, Ordering::Release);
+    if crossterm::terminal::is_raw_mode_enabled().unwrap_or(false) {
+        let mut out = io::stdout();
+        let _ = write!(out, "\x1b[5n");
+        let _ = out.flush();
     }
+    *slot = Some(pump);
+    drop(slot);
 
     if bytes.is_empty() {
         return None;
@@ -675,6 +782,74 @@ fn read_da_response(timeout: Duration) -> Option {
     String::from_utf8(bytes).ok()
 }
 
+/// Deadline-bounded accumulation loop shared by every reply reader: pull bytes
+/// off the pump channel until `is_complete` fires, the 4096-byte cap is hit,
+/// the deadline passes, or the pump disconnects (stdin EOF). Returns whatever
+/// arrived — callers map an empty buffer to "no reply" and a partial buffer to
+/// a best-effort parse, matching the pre-pump readers exactly.
+#[cfg(feature = "crossterm")]
+fn collect_reply(
+    rx: &std::sync::mpsc::Receiver,
+    deadline: Instant,
+    is_complete: &mut dyn FnMut(&[u8]) -> bool,
+) -> Vec {
+    let mut bytes = Vec::new();
+    loop {
+        let now = Instant::now();
+        if now >= deadline {
+            break;
+        }
+        match rx.recv_timeout(deadline - now) {
+            Ok(byte) => {
+                bytes.push(byte);
+                if is_complete(&bytes) || bytes.len() >= 4096 {
+                    break;
+                }
+            }
+            // Timed out, or the pump thread is gone (stdin EOF / error).
+            Err(_) => break,
+        }
+    }
+    bytes
+}
+
+/// Completion predicate for OSC / DCS / CSI-`t` style replies, which terminate
+/// with BEL (`\x07`) or ST (`ESC \`).
+#[cfg(feature = "crossterm")]
+fn osc_reply_complete(bytes: &[u8]) -> bool {
+    let len = bytes.len();
+    bytes[len - 1] == b'\x07' || (len >= 2 && bytes[len - 2] == 0x1B && bytes[len - 1] == b'\\')
+}
+
+/// Completion predicate builder for Device-Attributes replies: `c` is the
+/// final byte of each DA reply, and a combined `CSI c CSI > c` query yields
+/// two of them, so completion fires on the second `c`.
+#[cfg(feature = "crossterm")]
+fn da_reply_complete() -> impl FnMut(&[u8]) -> bool {
+    let mut terminators = 0usize;
+    move |bytes: &[u8]| {
+        if bytes[bytes.len() - 1] == b'c' {
+            terminators += 1;
+        }
+        terminators >= 2
+    }
+}
+
+/// Completion predicate for DECRPM replies (`CSI ?  ;  $ y`).
+#[cfg(feature = "crossterm")]
+fn decrpm_reply_complete(bytes: &[u8]) -> bool {
+    bytes[bytes.len() - 1] == b'y'
+}
+
+/// Read a Device-Attributes reply, which (unlike OSC) terminates with the byte
+/// `c` rather than BEL / ST. Drains up to two `c`-terminated CSI replies
+/// (DA1 + DA2) within the timeout so a combined `CSI c CSI > c` query yields
+/// both answers in one string.
+#[cfg(feature = "crossterm")]
+fn read_da_response(timeout: Duration) -> Option {
+    read_stdin_reply(timeout, da_reply_complete())
+}
+
 /// Parse a DA1 reply (`CSI ?  c`). Attribute `4` indicates Sixel
 /// support. Only the DA1 segment is consulted; a trailing DA2 segment in the
 /// same string is ignored here.
@@ -807,33 +982,7 @@ fn should_emit_synchronized_update() -> bool {
 /// so a terminal that ignores the query cannot stall startup.
 #[cfg(feature = "crossterm")]
 fn read_decrpm_response(timeout: Duration) -> Option {
-    let deadline = Instant::now() + timeout;
-    let mut stdin = io::stdin();
-    let mut bytes = Vec::new();
-    let mut buf = [0u8; 1];
-
-    while Instant::now() < deadline {
-        if !crossterm::event::poll(Duration::from_millis(10)).ok()? {
-            continue;
-        }
-        let read = stdin.read(&mut buf).ok()?;
-        if read == 0 {
-            continue;
-        }
-        bytes.push(buf[0]);
-        // `y` is the final byte of a DECRPM reply (`CSI ?  ;  $ y`).
-        if buf[0] == b'y' {
-            break;
-        }
-        if bytes.len() >= 4096 {
-            break;
-        }
-    }
-
-    if bytes.is_empty() {
-        return None;
-    }
-    String::from_utf8(bytes).ok()
+    read_stdin_reply(timeout, decrpm_reply_complete)
 }
 
 /// Parse a DECRPM reply for synchronized output (mode `2026`):
@@ -982,7 +1131,7 @@ impl Terminal {
     /// alternate screen and optionally enables mouse capture and the
     /// kitty keyboard protocol. When `report_all_keys` is set (and
     /// `kitty_keyboard` is too), bare modifier presses are reported.
-    pub fn new(
+    pub(crate) fn new(
         mouse: bool,
         kitty_keyboard: bool,
         report_all_keys: bool,
@@ -1014,19 +1163,19 @@ impl Terminal {
     }
 
     /// Return the fullscreen terminal's current `(cols, rows)`.
-    pub fn size(&self) -> (u32, u32) {
+    pub(crate) fn size(&self) -> (u32, u32) {
         (self.current.area.width, self.current.area.height)
     }
 
     /// Mutable access to the back buffer used by the next render pass.
-    pub fn buffer_mut(&mut self) -> &mut Buffer {
+    pub(crate) fn buffer_mut(&mut self) -> &mut Buffer {
         &mut self.current
     }
 
     /// Diff the back buffer against the front buffer, write the changed
     /// cells to stdout under a synchronized-output guard, then swap
     /// front and back buffers.
-    pub fn flush(&mut self) -> io::Result<()> {
+    pub(crate) fn flush(&mut self) -> io::Result<()> {
         if self.current.area.width < self.previous.area.width {
             execute!(self.stdout, terminal::Clear(terminal::ClearType::All))?;
         }
@@ -1088,7 +1237,7 @@ impl Terminal {
 
     /// Re-query the terminal size and resize the front and back buffers
     /// to match. Called from the SIGWINCH handler.
-    pub fn handle_resize(&mut self) -> io::Result<()> {
+    pub(crate) fn handle_resize(&mut self) -> io::Result<()> {
         let (cols, rows) = terminal::size()?;
         let area = Rect::new(0, 0, cols as u32, rows as u32);
         self.current.resize(area);
@@ -1170,7 +1319,7 @@ impl InlineTerminal {
     /// Optionally enables mouse capture and the kitty keyboard protocol.
     /// When `report_all_keys` is set (and `kitty_keyboard` is too), bare
     /// modifier presses are reported.
-    pub fn new(
+    pub(crate) fn new(
         height: u32,
         mouse: bool,
         kitty_keyboard: bool,
@@ -1213,19 +1362,19 @@ impl InlineTerminal {
     }
 
     /// Return the inline terminal's current `(cols, rows)`.
-    pub fn size(&self) -> (u32, u32) {
+    pub(crate) fn size(&self) -> (u32, u32) {
         (self.current.area.width, self.current.area.height)
     }
 
     /// Mutable access to the back buffer used by the next render pass.
-    pub fn buffer_mut(&mut self) -> &mut Buffer {
+    pub(crate) fn buffer_mut(&mut self) -> &mut Buffer {
         &mut self.current
     }
 
     /// Diff the back buffer against the front buffer, write changed
     /// cells to stdout under a synchronized-output guard at the
     /// inline rows reserved below the cursor, then swap buffers.
-    pub fn flush(&mut self) -> io::Result<()> {
+    pub(crate) fn flush(&mut self) -> io::Result<()> {
         if self.current.area.width < self.previous.area.width {
             execute!(self.stdout, terminal::Clear(terminal::ClearType::All))?;
         }
@@ -1299,7 +1448,7 @@ impl InlineTerminal {
 
     /// Re-query the terminal size and resize the inline buffers to match
     /// the new column count, preserving the inline row height.
-    pub fn handle_resize(&mut self) -> io::Result<()> {
+    pub(crate) fn handle_resize(&mut self) -> io::Result<()> {
         let (cols, _) = terminal::size()?;
         let area = Rect::new(0, 0, cols as u32, self.height);
         self.current.resize(area);
@@ -1345,13 +1494,14 @@ impl Drop for InlineTerminal {
 }
 
 mod selection;
-pub(crate) use selection::{apply_selection_overlay, extract_selection_text, SelectionState};
+pub(crate) use selection::{SelectionState, apply_selection_overlay, extract_selection_text};
 #[cfg(test)]
 pub(crate) use selection::{find_innermost_rect, normalize_selection};
 
 /// Detected terminal color scheme from OSC 11.
 #[non_exhaustive]
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum ColorScheme {
     /// Dark background detected.
@@ -1362,47 +1512,15 @@ pub enum ColorScheme {
     Unknown,
 }
 
+/// Read an OSC-style reply (BEL- or ST-terminated), hard-bounded by `timeout`.
 #[cfg(feature = "crossterm")]
 fn read_osc_response(timeout: Duration) -> Option {
-    let deadline = Instant::now() + timeout;
-    let mut stdin = io::stdin();
-    let mut bytes = Vec::new();
-    let mut buf = [0u8; 1];
-
-    while Instant::now() < deadline {
-        if !crossterm::event::poll(Duration::from_millis(10)).ok()? {
-            continue;
-        }
-
-        let read = stdin.read(&mut buf).ok()?;
-        if read == 0 {
-            continue;
-        }
-
-        bytes.push(buf[0]);
-
-        if buf[0] == b'\x07' {
-            break;
-        }
-        let len = bytes.len();
-        if len >= 2 && bytes[len - 2] == 0x1B && bytes[len - 1] == b'\\' {
-            break;
-        }
-
-        if bytes.len() >= 4096 {
-            break;
-        }
-    }
-
-    if bytes.is_empty() {
-        return None;
-    }
-
-    String::from_utf8(bytes).ok()
+    read_stdin_reply(timeout, osc_reply_complete)
 }
 
 /// Query the terminal's background color via OSC 11 and return the detected scheme.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn detect_color_scheme() -> ColorScheme {
     let mut stdout = io::stdout();
     if write!(stdout, "\x1b]11;?\x07").is_err() {
@@ -1547,6 +1665,7 @@ fn parse_osc52_response(response: &str) -> Option {
 ///   * For *writing* the clipboard there is no such hazard — that path only
 ///     emits bytes and never reads stdin.
 #[cfg(feature = "crossterm")]
+#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
 pub fn read_clipboard() -> Option {
     let mut stdout = io::stdout();
     write!(stdout, "\x1b]52;c;?\x07").ok()?;
@@ -1713,7 +1832,7 @@ fn flush_buffer_diff(
                 // SGR sequence exactly matching the per-cell flush.
                 has_updates = true;
 
-                let need_move = last_cursor.map_or(true, |(lx, ly)| lx != x || ly != abs_y);
+                let need_move = last_cursor.is_none_or(|(lx, ly)| lx != x || ly != abs_y);
                 if need_move {
                     queue!(stdout, cursor::MoveTo(sat_u16(x), sat_u16(abs_y)))?;
                 }
@@ -2661,6 +2780,134 @@ mod tests {
     #![allow(clippy::unwrap_used)]
     use super::*;
 
+    /// Feed `bytes` to a channel from a helper thread after `delay`, then run
+    /// [`collect_reply`] against it with the given budget and predicate.
+    fn collect_with_feed(
+        bytes: &'static [u8],
+        delay: Duration,
+        budget: Duration,
+        is_complete: &mut dyn FnMut(&[u8]) -> bool,
+    ) -> (Vec, Duration) {
+        let (tx, rx) = std::sync::mpsc::channel::();
+        std::thread::spawn(move || {
+            std::thread::sleep(delay);
+            for &b in bytes {
+                if tx.send(b).is_err() {
+                    return;
+                }
+            }
+            // Keep the sender alive past the collector's budget: the real
+            // pump thread only drops its sender on stdin EOF, so dropping it
+            // here right after the payload would disconnect the channel and
+            // end the wait early, masking deadline behavior.
+            std::thread::sleep(Duration::from_secs(3));
+        });
+        let start = Instant::now();
+        let out = collect_reply(&rx, start + budget, is_complete);
+        (out, start.elapsed())
+    }
+
+    #[test]
+    fn collect_reply_osc_bel_terminator_completes_early() {
+        let reply = b"\x1b]11;rgb:0000/0000/0000\x07";
+        let (out, elapsed) = collect_with_feed(
+            reply,
+            Duration::ZERO,
+            Duration::from_secs(2),
+            &mut osc_reply_complete,
+        );
+        assert_eq!(out, reply);
+        assert!(
+            elapsed < Duration::from_secs(1),
+            "should not wait out the budget"
+        );
+    }
+
+    #[test]
+    fn collect_reply_osc_st_terminator_completes_early() {
+        let reply = b"\x1bP>|tmux 3.5a\x1b\\";
+        let (out, elapsed) = collect_with_feed(
+            reply,
+            Duration::ZERO,
+            Duration::from_secs(2),
+            &mut osc_reply_complete,
+        );
+        assert_eq!(out, reply);
+        assert!(elapsed < Duration::from_secs(1));
+    }
+
+    #[test]
+    fn collect_reply_silence_returns_empty_at_deadline() {
+        // The silent-host case that used to deadlock startup: no bytes ever
+        // arrive. The collector must give up at the deadline, not block.
+        let budget = Duration::from_millis(150);
+        let (out, elapsed) =
+            collect_with_feed(b"", Duration::from_secs(5), budget, &mut osc_reply_complete);
+        assert!(out.is_empty());
+        assert!(elapsed >= budget);
+        assert!(
+            elapsed < Duration::from_secs(2),
+            "must not block past the budget"
+        );
+    }
+
+    #[test]
+    fn collect_reply_da_drains_two_replies() {
+        let reply = b"\x1b[?62;4c\x1b[>1;10;0c";
+        let (out, elapsed) = collect_with_feed(
+            reply,
+            Duration::ZERO,
+            Duration::from_secs(2),
+            &mut da_reply_complete(),
+        );
+        assert_eq!(out, reply);
+        assert!(elapsed < Duration::from_secs(1));
+    }
+
+    #[test]
+    fn collect_reply_da_lone_reply_returns_partial_at_deadline() {
+        // A terminal that answers DA1 but ignores DA2: the collector waits out
+        // the budget, then hands back the partial reply for best-effort parse
+        // (pre-pump behavior, preserved).
+        let budget = Duration::from_millis(150);
+        let (out, elapsed) = collect_with_feed(
+            b"\x1b[?62;4c",
+            Duration::ZERO,
+            budget,
+            &mut da_reply_complete(),
+        );
+        assert_eq!(out, b"\x1b[?62;4c");
+        assert!(elapsed >= budget);
+    }
+
+    #[test]
+    fn collect_reply_unterminated_caps_at_4096_bytes() {
+        static BIG: std::sync::OnceLock> = std::sync::OnceLock::new();
+        let big = BIG.get_or_init(|| vec![b'x'; 5000]).as_slice();
+        let (tx, rx) = std::sync::mpsc::channel::();
+        for &b in big {
+            tx.send(b).unwrap();
+        }
+        let out = collect_reply(
+            &rx,
+            Instant::now() + Duration::from_secs(2),
+            &mut osc_reply_complete,
+        );
+        assert_eq!(out.len(), 4096);
+    }
+
+    #[test]
+    fn decrpm_predicate_terminates_on_y() {
+        let reply = b"\x1b[?2026;1$y";
+        let (out, _) = collect_with_feed(
+            reply,
+            Duration::ZERO,
+            Duration::from_secs(2),
+            &mut decrpm_reply_complete,
+        );
+        assert_eq!(out, reply);
+    }
+
     #[test]
     fn reset_current_buffer_applies_theme_background() {
         let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 1));
diff --git a/src/terminal/selection.rs b/src/terminal/selection.rs
index ebe3b63..f0f54ca 100644
--- a/src/terminal/selection.rs
+++ b/src/terminal/selection.rs
@@ -12,7 +12,7 @@ impl SelectionState {
     /// Record a left-mouse-down anchor point and identify the widget
     /// rect under the cursor; resets the active flag so a single click
     /// without drag clears any prior selection.
-    pub fn mouse_down(&mut self, x: u32, y: u32, hit_map: &[(Rect, Rect)]) {
+    pub(crate) fn mouse_down(&mut self, x: u32, y: u32, hit_map: &[(Rect, Rect)]) {
         self.anchor = Some((x, y));
         self.current = Some((x, y));
         self.widget_rect = find_innermost_rect(hit_map, x, y);
@@ -23,22 +23,22 @@ impl SelectionState {
     /// Marks the selection active once the cursor has moved more than one
     /// cell horizontally or any cell vertically. Re-resolves the owning
     /// widget rect if the cursor has left the original widget.
-    pub fn mouse_drag(&mut self, x: u32, y: u32, hit_map: &[(Rect, Rect)]) {
+    pub(crate) fn mouse_drag(&mut self, x: u32, y: u32, hit_map: &[(Rect, Rect)]) {
         if let Some(anchor) = self.anchor {
             self.current = Some((x, y));
             if x.abs_diff(anchor.0) > 1 || y.abs_diff(anchor.1) > 0 {
                 self.active = true;
             }
-            if let Some(rect) = self.widget_rect {
-                if y < rect.y || y >= rect.bottom() || x < rect.x || x >= rect.right() {
-                    self.widget_rect = find_containing_rect(hit_map, anchor, (x, y));
-                }
+            if let Some(rect) = self.widget_rect
+                && (y < rect.y || y >= rect.bottom() || x < rect.x || x >= rect.right())
+            {
+                self.widget_rect = find_containing_rect(hit_map, anchor, (x, y));
             }
         }
     }
 
     /// Reset the selection back to the empty default state.
-    pub fn clear(&mut self) {
+    pub(crate) fn clear(&mut self) {
         *self = Self::default();
     }
 }
diff --git a/src/test_utils.rs b/src/test_utils.rs
index 0287545..e6a0b41 100644
--- a/src/test_utils.rs
+++ b/src/test_utils.rs
@@ -12,7 +12,7 @@ use crate::event::{
 };
 use crate::rect::Rect;
 use crate::style::Style;
-use crate::{run_frame_kernel, FrameState, RunConfig};
+use crate::{FrameState, RunConfig, run_frame_kernel};
 
 /// Builder for constructing a sequence of input [`Event`]s.
 ///
@@ -32,6 +32,7 @@ use crate::{run_frame_kernel, FrameState, RunConfig};
 ///     .build();
 /// assert_eq!(events.len(), 2);
 /// ```
+#[must_use = "EventBuilder does nothing until .build() is called"]
 pub struct EventBuilder {
     events: Vec,
 }
@@ -995,6 +996,7 @@ impl std::fmt::Display for TestBackend {
 ///
 /// Since 0.21.0.
 #[cfg(feature = "pty-test")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pty-test")))]
 #[derive(Clone, Debug)]
 pub struct PtyFrame {
     /// Raw bytes emitted for this frame (SGR runs, OSC 8, Sixel, Kitty).
@@ -1034,6 +1036,7 @@ pub struct PtyFrame {
 /// # }
 /// ```
 #[cfg(feature = "pty-test")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pty-test")))]
 pub struct PtyBackend {
     width: u32,
     height: u32,
diff --git a/src/widgets.rs b/src/widgets.rs
index 920f05c..4c11b7c 100644
--- a/src/widgets.rs
+++ b/src/widgets.rs
@@ -10,14 +10,14 @@ use std::path::PathBuf;
 use std::time::{SystemTime, UNIX_EPOCH};
 use unicode_width::UnicodeWidthStr;
 
-use crate::context::Response;
 use crate::Style;
+use crate::context::Response;
 
 /// Bare function-pointer validator used by the deprecated positional
 /// [`FormState::validate`](crate::widgets::FormState::validate) API.
 ///
 /// Retained for backward compatibility. New code should attach
-/// [`Validator`](crate::widgets::Validator) closures per field via
+/// [`Validator`] closures per field via
 /// [`FormField::validate`](crate::widgets::FormField::validate); see the
 /// [`validators`] module for built-ins.
 pub type FormValidator = fn(&str) -> Result<(), String>;
diff --git a/src/widgets/collections.rs b/src/widgets/collections.rs
index bce84e1..a1fdc9a 100644
--- a/src/widgets/collections.rs
+++ b/src/widgets/collections.rs
@@ -241,11 +241,11 @@ impl ListState {
         }
 
         // Keep per-item heights aligned with `items` when present.
-        if let Some(heights) = self.item_heights.as_mut() {
-            if from < heights.len() {
-                let h = heights.remove(from);
-                heights.insert(to.min(heights.len()), h);
-            }
+        if let Some(heights) = self.item_heights.as_mut()
+            && from < heights.len()
+        {
+            let h = heights.remove(from);
+            heights.insert(to.min(heights.len()), h);
         }
         self.heights_dirty = true;
 
@@ -920,10 +920,10 @@ impl TableState {
     fn prune_selection(&mut self) {
         let view_len = self.view_indices.len();
         self.multi_selected.retain(|&idx| idx < view_len);
-        if let Some(anchor) = self.selection_anchor {
-            if anchor >= view_len {
-                self.selection_anchor = None;
-            }
+        if let Some(anchor) = self.selection_anchor
+            && anchor >= view_len
+        {
+            self.selection_anchor = None;
         }
     }
 
diff --git a/src/widgets/feedback.rs b/src/widgets/feedback.rs
index ba9f9cc..26f74fe 100644
--- a/src/widgets/feedback.rs
+++ b/src/widgets/feedback.rs
@@ -62,12 +62,12 @@ impl RichLogState {
     pub fn push_segments(&mut self, segments: Vec<(String, Style)>) {
         self.entries.push(RichLogEntry { segments });
 
-        if let Some(max_entries) = self.max_entries {
-            if self.entries.len() > max_entries {
-                let remove_count = self.entries.len() - max_entries;
-                self.entries.drain(0..remove_count);
-                self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
-            }
+        if let Some(max_entries) = self.max_entries
+            && self.entries.len() > max_entries
+        {
+            let remove_count = self.entries.len() - max_entries;
+            self.entries.drain(0..remove_count);
+            self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
         }
 
         if self.auto_scroll {
diff --git a/src/widgets/input.rs b/src/widgets/input.rs
index fd8f7c4..a73192f 100644
--- a/src/widgets/input.rs
+++ b/src/widgets/input.rs
@@ -235,7 +235,7 @@ impl Default for TextInputState {
 /// Unlike the deprecated [`FormValidator`] function pointer, a `Validator`
 /// wraps a closure, so it can capture surrounding state — a compiled matcher,
 /// a min/max pulled from config, or a sibling field's value. Built-in
-/// constructors live in the [`validators`](crate::widgets::validators) module.
+/// constructors live in the [`validators`] module.
 ///
 /// You rarely construct one directly: [`FormField::validate`] accepts a closure
 /// and boxes it for you. Use [`Validator::new`] when you need to build a
@@ -282,6 +282,7 @@ impl std::fmt::Debug for Validator {
 /// [`Context::form_field`](crate::Context::form_field) (or directly via
 /// [`FormField::poll_async`]). Gated behind the `async` feature.
 #[cfg(feature = "async")]
+#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
 pub struct AsyncValidation {
     rx: tokio::sync::oneshot::Receiver>,
 }
@@ -391,7 +392,7 @@ impl FormField {
     ///
     /// The closure may capture state, unlike the deprecated positional
     /// [`FormValidator`]. Built-ins live in
-    /// [`validators`](crate::widgets::validators).
+    /// [`validators`].
     ///
     /// # Example
     ///
@@ -489,6 +490,7 @@ impl FormField {
     /// # }
     /// ```
     #[cfg(feature = "async")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn validate_async(&mut self, future: F)
     where
         F: std::future::Future> + Send + 'static,
@@ -505,6 +507,7 @@ impl FormField {
     ///
     /// Requires the `async` feature.
     #[cfg(feature = "async")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn is_validating(&self) -> bool {
         self.pending.is_some()
     }
@@ -517,6 +520,7 @@ impl FormField {
     ///
     /// Requires the `async` feature.
     #[cfg(feature = "async")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn poll_async(&mut self) -> bool {
         use tokio::sync::oneshot::error::TryRecvError;
         let Some(pending) = self.pending.as_mut() else {
diff --git a/src/widgets/selection.rs b/src/widgets/selection.rs
index 4f94160..a2b2337 100644
--- a/src/widgets/selection.rs
+++ b/src/widgets/selection.rs
@@ -333,11 +333,10 @@ fn selected_label_in_nodes<'a>(
             return Some(node.label.as_str());
         }
         *cursor += 1;
-        if node.expanded {
-            if let Some(found) = selected_label_in_nodes(&node.children, target, cursor) {
+        if node.expanded
+            && let Some(found) = selected_label_in_nodes(&node.children, target, cursor) {
                 return Some(found);
             }
-        }
     }
     None
 }
@@ -534,11 +533,10 @@ impl ColorPickerState {
     /// assert_eq!(picker.selected(), Color::Rgb(59, 130, 246));
     /// ```
     pub fn selected(&self) -> crate::Color {
-        if self.mode == PickerMode::Hex {
-            if let Some(c) = parse_hex_color(&self.hex_input.value) {
+        if self.mode == PickerMode::Hex
+            && let Some(c) = parse_hex_color(&self.hex_input.value) {
                 return c;
             }
-        }
         self.colors
             .get(self.selected)
             .copied()
diff --git a/tests/backend_contract.rs b/tests/backend_contract.rs
index 4c6c87e..d1029f5 100644
--- a/tests/backend_contract.rs
+++ b/tests/backend_contract.rs
@@ -1,6 +1,6 @@
 use std::io;
 
-use slt::{frame, AppState, Backend, Buffer, Context, EventBuilder, Rect, RunConfig};
+use slt::{AppState, Backend, Buffer, Context, EventBuilder, Rect, RunConfig, frame};
 
 struct ContractBackend {
     buffer: Buffer,
diff --git a/tests/gallery_manifest.rs b/tests/gallery_manifest.rs
index 05bd274..e99b1d8 100644
--- a/tests/gallery_manifest.rs
+++ b/tests/gallery_manifest.rs
@@ -76,10 +76,10 @@ fn repo_tape_stems(root: &Path) -> BTreeSet {
     for entry in fs::read_dir(root).expect("read repo root") {
         let entry = entry.expect("dir entry");
         let path = entry.path();
-        if path.extension().and_then(|e| e.to_str()) == Some("tape") {
-            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
-                stems.insert(stem.to_string());
-            }
+        if path.extension().and_then(|e| e.to_str()) == Some("tape")
+            && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
+        {
+            stems.insert(stem.to_string());
         }
     }
     stems
diff --git a/tests/kernel_parity.rs b/tests/kernel_parity.rs
index 283fc0f..d4b5a93 100644
--- a/tests/kernel_parity.rs
+++ b/tests/kernel_parity.rs
@@ -3,7 +3,7 @@ mod support;
 use slt::widgets::TabsState;
 use slt::{AppState, Context, EventBuilder, TestBackend};
 
-use support::{render_with_frame_backend, RecordingBackend};
+use support::{RecordingBackend, render_with_frame_backend};
 
 fn render_counter(ui: &mut Context) {
     let count = ui.use_state(|| 0i32);
diff --git a/tests/kernel_proptest.rs b/tests/kernel_proptest.rs
index 6fdee6f..5c315a4 100644
--- a/tests/kernel_proptest.rs
+++ b/tests/kernel_proptest.rs
@@ -3,7 +3,7 @@ mod support;
 use proptest::prelude::*;
 use slt::{AppState, Context, EventBuilder, TestBackend};
 
-use support::{render_with_frame_backend, RecordingBackend};
+use support::{RecordingBackend, render_with_frame_backend};
 
 fn render_counter(ui: &mut Context) {
     let count = ui.use_state(|| 0i32);
diff --git a/tests/pty_ansi.rs b/tests/pty_ansi.rs
index b4bc6a2..430d9a8 100644
--- a/tests/pty_ansi.rs
+++ b/tests/pty_ansi.rs
@@ -45,7 +45,9 @@ fn sixel_image_emits_envelope() {
     // SAFETY-equivalent note: `set_var` is process-global. This test owns the
     // var for its duration and restores it; it asserts on the forced path.
     let prev = std::env::var("SLT_FORCE_SIXEL").ok();
-    std::env::set_var("SLT_FORCE_SIXEL", "1");
+    // SAFETY (edition 2024): set_var is unsafe; this single-threaded test owns
+    // the var for its duration and restores it below.
+    unsafe { std::env::set_var("SLT_FORCE_SIXEL", "1") };
 
     let mut pb = PtyBackend::new(20, 2);
     // 2x2 red square (RGBA: 4 pixels x 4 bytes).
@@ -56,9 +58,11 @@ fn sixel_image_emits_envelope() {
     pb.assert_emits("\u{1b}Pq");
     pb.assert_emits("\u{1b}\\");
 
-    match prev {
-        Some(v) => std::env::set_var("SLT_FORCE_SIXEL", v),
-        None => std::env::remove_var("SLT_FORCE_SIXEL"),
+    unsafe {
+        match prev {
+            Some(v) => std::env::set_var("SLT_FORCE_SIXEL", v),
+            None => std::env::remove_var("SLT_FORCE_SIXEL"),
+        }
     }
 }
 
diff --git a/tests/snapshot_format_stability.rs b/tests/snapshot_format_stability.rs
index 4c81f42..9140750 100644
--- a/tests/snapshot_format_stability.rs
+++ b/tests/snapshot_format_stability.rs
@@ -8,9 +8,9 @@
 //! See the rustdoc on [`slt::Buffer::snapshot_format`] for the format spec
 //! and stability guarantees.
 
+use slt::Rect;
 use slt::buffer::Buffer;
 use slt::style::{Color, Modifiers, Style};
-use slt::Rect;
 
 /// Default-style buffer renders trailing spaces verbatim, no markers.
 #[test]
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
index 8ab71f2..9764379 100644
--- a/tests/support/mod.rs
+++ b/tests/support/mod.rs
@@ -1,6 +1,6 @@
 use std::io;
 
-use slt::{frame, AppState, Backend, Buffer, Context, Event, Rect, RunConfig};
+use slt::{AppState, Backend, Buffer, Context, Event, Rect, RunConfig, frame};
 
 pub struct RecordingBackend {
     buffer: Buffer,
diff --git a/tests/tooltip.rs b/tests/tooltip.rs
index e04a6df..c5e5d43 100644
--- a/tests/tooltip.rs
+++ b/tests/tooltip.rs
@@ -1,6 +1,6 @@
 use slt::event::Event;
 use slt::rect::Rect;
-use slt::{frame, Backend, Buffer, RunConfig};
+use slt::{Backend, Buffer, RunConfig, frame};
 
 struct FrameBackend {
     buffer: Buffer,
diff --git a/tests/v020_dx_shortcuts.rs b/tests/v020_dx_shortcuts.rs
index 3d86905..6f25142 100644
--- a/tests/v020_dx_shortcuts.rs
+++ b/tests/v020_dx_shortcuts.rs
@@ -9,7 +9,7 @@
 use slt::anim::DEFAULT_ANIMATE_TICKS;
 use slt::event::Event;
 use slt::{
-    frame, AppState, Backend, Buffer, ContainerStyle, Context, Rect, RunConfig, TestBackend,
+    AppState, Backend, Buffer, ContainerStyle, Context, Rect, RunConfig, TestBackend, frame,
 };
 
 /// Minimal Backend impl that drives `frame()` against an in-memory buffer.
diff --git a/tests/v020_interaction_regression.rs b/tests/v020_interaction_regression.rs
index 72d8096..d78ffa0 100644
--- a/tests/v020_interaction_regression.rs
+++ b/tests/v020_interaction_regression.rs
@@ -31,7 +31,7 @@ mod named_focus_demo;
 #[allow(dead_code)]
 mod modal_trap_demo;
 
-use slt::{context::ModalOptions, Border, ButtonVariant, Context, EventBuilder, TestBackend};
+use slt::{Border, ButtonVariant, Context, EventBuilder, TestBackend, context::ModalOptions};
 use std::cell::RefCell;
 use std::rc::Rc;
 
diff --git a/tests/v020_perf_alloc.rs b/tests/v020_perf_alloc.rs
index b8c5726..d88e596 100644
--- a/tests/v020_perf_alloc.rs
+++ b/tests/v020_perf_alloc.rs
@@ -27,8 +27,8 @@
 #![allow(clippy::unwrap_used)]
 
 use std::alloc::{GlobalAlloc, Layout, System};
-use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
 use std::sync::Mutex;
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
 
 struct CountingAllocator;
 
@@ -37,14 +37,16 @@ static MEASURING: AtomicBool = AtomicBool::new(false);
 
 unsafe impl GlobalAlloc for CountingAllocator {
     unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
-        if MEASURING.load(Ordering::Relaxed) {
-            ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
+        unsafe {
+            if MEASURING.load(Ordering::Relaxed) {
+                ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
+            }
+            System.alloc(layout)
         }
-        System.alloc(layout)
     }
 
     unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
-        System.dealloc(ptr, layout)
+        unsafe { System.dealloc(ptr, layout) }
     }
 }
 
diff --git a/tests/v020_regression_panel_demo.rs b/tests/v020_regression_panel_demo.rs
index d5adb9e..ecb8ecf 100644
--- a/tests/v020_regression_panel_demo.rs
+++ b/tests/v020_regression_panel_demo.rs
@@ -20,7 +20,7 @@ use slt::TestBackend;
 #[path = "../examples/v020_regression_panel.rs"]
 mod v020_regression_panel;
 
-use v020_regression_panel::{render, DemoState};
+use v020_regression_panel::{DemoState, render};
 
 /// Width / height generous enough to fit the full panel (gauges row,
 /// table + gutter row, footer, plus four corner anchors and the center
diff --git a/tests/v020_theme_modal.rs b/tests/v020_theme_modal.rs
index e9597b2..d83016a 100644
--- a/tests/v020_theme_modal.rs
+++ b/tests/v020_theme_modal.rs
@@ -9,7 +9,7 @@
 
 #![allow(unused_must_use)]
 
-use slt::{context::ModalOptions, Color, EventBuilder, KeyCode, Spacing, TestBackend, Theme};
+use slt::{Color, EventBuilder, KeyCode, Spacing, TestBackend, Theme, context::ModalOptions};
 
 // ── #226 per-subtree theme override ─────────────────────────────────
 
diff --git a/tests/v020_theme_modal_demos.rs b/tests/v020_theme_modal_demos.rs
index 8a9bd8a..eb696b9 100644
--- a/tests/v020_theme_modal_demos.rs
+++ b/tests/v020_theme_modal_demos.rs
@@ -9,7 +9,7 @@
 
 #![allow(unused_must_use)]
 
-use slt::{context::ModalOptions, Border, ButtonVariant, Context, TestBackend, Theme};
+use slt::{Border, ButtonVariant, Context, TestBackend, Theme, context::ModalOptions};
 
 // ── v020_theme_subtree demo ──────────────────────────────────────────
 
diff --git a/tests/v020_widgets.rs b/tests/v020_widgets.rs
index a6d3273..73050b7 100644
--- a/tests/v020_widgets.rs
+++ b/tests/v020_widgets.rs
@@ -397,8 +397,8 @@ fn issue_235_highlight_next_scrolls_viewport() {
     state.set_highlights(&[HighlightRange::line(30)]);
     // Manually populate bounds. We use the public scroll API to set offset.
     state.scroll_down(0); // no-op; bounds set by widget on first frame.
-                          // The set_bounds path is private, so simulate by accessing the
-                          // highlight scroll-to via the public API; the math should clamp.
+    // The set_bounds path is private, so simulate by accessing the
+    // highlight scroll-to via the public API; the math should clamp.
     state.scroll_to_current_highlight();
     // With viewport_height=0 (no widget run yet), scroll math no-ops, so
     // offset stays at 0 — but at least the call must not panic.
diff --git a/tests/widgets.rs b/tests/widgets.rs
index 81e54f7..9e0ec33 100644
--- a/tests/widgets.rs
+++ b/tests/widgets.rs
@@ -376,10 +376,12 @@ fn file_picker_lists_directories_before_files() {
     state.refresh();
 
     assert!(state.entries.iter().any(|e| e.name == "alpha" && e.is_dir));
-    assert!(state
-        .entries
-        .iter()
-        .any(|e| e.name == "zeta.txt" && !e.is_dir));
+    assert!(
+        state
+            .entries
+            .iter()
+            .any(|e| e.name == "zeta.txt" && !e.is_dir)
+    );
 
     let first_file = state.entries.iter().position(|e| !e.is_dir);
     if let Some(first_file_idx) = first_file {
@@ -432,10 +434,12 @@ fn file_picker_extension_filter() {
     state.refresh();
 
     assert!(state.entries.iter().any(|e| e.name == "dir_a" && e.is_dir));
-    assert!(state
-        .entries
-        .iter()
-        .any(|e| e.name == "main.rs" && !e.is_dir));
+    assert!(
+        state
+            .entries
+            .iter()
+            .any(|e| e.name == "main.rs" && !e.is_dir)
+    );
     assert!(!state.entries.iter().any(|e| e.name == "notes.txt"));
 
     fs::remove_dir_all(root).expect("failed to clean temp dir");
@@ -2450,9 +2454,11 @@ fn tree_renders_root() {
 #[test]
 fn tree_renders_expanded() {
     let mut tb = TestBackend::new(80, 24);
-    let mut state = TreeState::new(vec![TreeNode::new("Root")
-        .expanded()
-        .children(vec![TreeNode::new("Child A"), TreeNode::new("Child B")])]);
+    let mut state = TreeState::new(vec![
+        TreeNode::new("Root")
+            .expanded()
+            .children(vec![TreeNode::new("Child A"), TreeNode::new("Child B")]),
+    ]);
 
     tb.render(|ui| {
         ui.tree(&mut state);
@@ -2467,7 +2473,7 @@ fn tree_renders_expanded() {
 fn tree_renders_collapsed() {
     let mut tb = TestBackend::new(80, 24);
     let mut state = TreeState::new(vec![
-        TreeNode::new("Root").children(vec![TreeNode::new("Hidden Child")])
+        TreeNode::new("Root").children(vec![TreeNode::new("Hidden Child")]),
     ]);
 
     tb.render(|ui| {
@@ -2614,7 +2620,7 @@ fn virtual_list_variable_page_down_by_rows() {
     let mut tb = TestBackend::new(40, 12);
     // 20 items; first six heights are [3, 3, 1, 1, 1, 1, ...] then 1s.
     let mut heights = vec![3u32, 3, 1, 1, 1, 1];
-    heights.extend(std::iter::repeat(1u32).take(14));
+    heights.extend(std::iter::repeat_n(1u32, 14));
     let items: Vec = (0..20).map(|i| format!("Item {i}")).collect();
     let mut state = ListState::new(items).with_item_heights(heights);