git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commitb4feec934a2222bd3ac0ba3235993e6e6ee83333
authorLucas Galante <[email protected]>
date2026-05-19 23:17
Initial port to clear-ui framework

Replace iced with wgpu+glyphon custom UI framework.
All 8 pages (Power, Audio, Display, Radios, Layout, Input,
System Info, Status, Storage) ported with async tokio
background refresh via mpsc channels.

 .gitignore               |    5 +
 Cargo.lock               | 3442 ++++++++++++++++++++++++++++++++++++++++++++++
 Cargo.toml               |   25 +
 src/app.rs               |   94 ++
 src/lib.rs               |    3 +
 src/main.rs              |  686 +++++++++
 src/pages/audio.rs       |  355 +++++
 src/pages/display.rs     |  196 +++
 src/pages/input.rs       |  251 ++++
 src/pages/keybindings.rs |    1 +
 src/pages/layout.rs      |  312 +++++
 src/pages/mod.rs         |   55 +
 src/pages/network.rs     |  300 ++++
 src/pages/power.rs       |  321 +++++
 src/pages/status.rs      |  123 ++
 src/pages/storage.rs     |  111 ++
 src/pages/system_info.rs |  114 ++
 src/widgets.rs           |   30 +
 18 files changed, 6424 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4c6b78
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+target/
+*.swp
+*.swo
+*~
+.DS_Store
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..3ef71a9
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3442 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ab_glyph"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "android-activity"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
+dependencies = [
+ "android-properties",
+ "bitflags 2.11.1",
+ "cc",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys 0.6.0+11769913",
+ "num_enum",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[package]]
+name = "ash"
+version = "0.38.0+1.3.281"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.11.1",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clear-system-interface"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+ "clear-ui",
+ "futures",
+ "glyphon",
+ "pollster",
+ "serde",
+ "taffy",
+ "tokio",
+ "wgpu",
+ "winit",
+ "zbus",
+]
+
+[[package]]
+name = "clear-ui"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+ "glyphon",
+ "pollster",
+ "taffy",
+ "tokio",
+ "wgpu",
+ "winit",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cosmic-text"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2"
+dependencies = [
+ "bitflags 2.11.1",
+ "fontdb",
+ "log",
+ "rangemap",
+ "rayon",
+ "rustc-hash 1.1.0",
+ "rustybuzz",
+ "self_cell",
+ "swash",
+ "sys-locale",
+ "ttf-parser 0.21.1",
+ "unicode-bidi",
+ "unicode-linebreak",
+ "unicode-script",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "cursor-icon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dlib"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "font-types"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "fontconfig-parser"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
+dependencies = [
+ "roxmltree",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
+dependencies = [
+ "fontconfig-parser",
+ "log",
+ "memmap2",
+ "slotmap",
+ "tinyvec",
+ "ttf-parser 0.20.0",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gethostname"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
+dependencies = [
+ "rustix 1.1.4",
+ "windows-link",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "glow"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "glyphon"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc83cc30ed66a0a9e77b7974c91360a13cad5e7ab65aa51e299c383749a018ab"
+dependencies = [
+ "cosmic-text",
+ "etagere",
+ "lru",
+ "rustc-hash 2.1.2",
+ "wgpu",
+]
+
+[[package]]
+name = "gpu-alloc"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
+dependencies = [
+ "bitflags 2.11.1",
+ "gpu-alloc-types",
+]
+
+[[package]]
+name = "gpu-alloc-types"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "gpu-allocator"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
+dependencies = [
+ "log",
+ "presser",
+ "thiserror 1.0.69",
+ "windows",
+]
+
+[[package]]
+name = "gpu-descriptor"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
+dependencies = [
+ "bitflags 2.11.1",
+ "gpu-descriptor-types",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "gpu-descriptor-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "grid"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hexf-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.18",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "khronos-egl"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
+dependencies = [
+ "libc",
+ "libloading",
+ "pkg-config",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "bitflags 2.11.1",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.5",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memmap2"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metal"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
+dependencies = [
+ "bitflags 2.11.1",
+ "block",
+ "core-graphics-types",
+ "foreign-types",
+ "log",
+ "objc",
+ "paste",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "naga"
+version = "24.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bitflags 2.11.1",
+ "cfg_aliases",
+ "codespan-reporting",
+ "hexf-parse",
+ "indexmap",
+ "log",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "strum",
+ "termcolor",
+ "thiserror 2.0.18",
+ "unicode-xid",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.11.1",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys 0.6.0+11769913",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.5.0+25.2.9519653"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-contacts",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation",
+ "objc2-link-presentation",
+ "objc2-quartz-core",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "orbclient"
+version = "0.3.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a570f6bca41d29acb2139229a7c873ec99bc9a313bd10804081d89bfac8ff329"
+dependencies = [
+ "libc",
+ "libredox",
+]
+
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
+dependencies = [
+ "ttf-parser 0.25.1",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "pollster"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
+
+[[package]]
+name = "presser"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+
+[[package]]
+name = "quick-xml"
+version = "0.39.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "range-alloc"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+
+[[package]]
+name = "rangemap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "read-fonts"
+version = "0.22.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f"
+dependencies = [
+ "bytemuck",
+ "font-types",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "renderdoc-sys"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+
+[[package]]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "rustybuzz"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
+dependencies = [
+ "bitflags 2.11.1",
+ "bytemuck",
+ "libm",
+ "smallvec",
+ "ttf-parser 0.21.1",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-properties",
+ "unicode-script",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sctk-adwaita"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+dependencies = [
+ "ab_glyph",
+ "log",
+ "memmap2",
+ "smithay-client-toolkit",
+ "tiny-skia",
+]
+
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "skrifa"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe"
+dependencies = [
+ "bytemuck",
+ "read-fonts",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+dependencies = [
+ "bitflags 2.11.1",
+ "calloop",
+ "calloop-wayland-source",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 0.38.44",
+ "thiserror 1.0.69",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spirv"
+version = "0.3.0+sdk-1.3.268.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
+
+[[package]]
+name = "swash"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2"
+dependencies = [
+ "skrifa",
+ "yazi",
+ "zeno",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "taffy"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859"
+dependencies = [
+ "arrayvec",
+ "grid",
+ "serde",
+ "slotmap",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "log",
+ "tiny-skia-path",
+]
+
+[[package]]
+name = "tiny-skia-path"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.11+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
+
+[[package]]
+name = "ttf-parser"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-script"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 1.1.4",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
+dependencies = [
+ "bitflags 2.11.1",
+ "rustix 1.1.4",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.11.1",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d"
+dependencies = [
+ "rustix 1.1.4",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
+dependencies = [
+ "bitflags 2.11.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
+dependencies = [
+ "bitflags 2.11.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
+dependencies = [
+ "bitflags 2.11.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
+dependencies = [
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wgpu"
+version = "24.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.11.1",
+ "cfg_aliases",
+ "document-features",
+ "js-sys",
+ "log",
+ "naga",
+ "parking_lot",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "24.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499"
+dependencies = [
+ "arrayvec",
+ "bit-vec",
+ "bitflags 2.11.1",
+ "cfg_aliases",
+ "document-features",
+ "indexmap",
+ "log",
+ "naga",
+ "once_cell",
+ "parking_lot",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "24.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set",
+ "bitflags 2.11.1",
+ "block",
+ "bytemuck",
+ "cfg_aliases",
+ "core-graphics-types",
+ "glow",
+ "glutin_wgl_sys",
+ "gpu-alloc",
+ "gpu-allocator",
+ "gpu-descriptor",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading",
+ "log",
+ "metal",
+ "naga",
+ "ndk-sys 0.5.0+25.2.9519653",
+ "objc",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "renderdoc-sys",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "web-sys",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "24.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c"
+dependencies = [
+ "bitflags 2.11.1",
+ "js-sys",
+ "log",
+ "web-sys",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+dependencies = [
+ "windows-core",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-result",
+ "windows-strings",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winit"
+version = "0.30.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.11.1",
+ "block2",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "sctk-adwaita",
+ "smithay-client-toolkit",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.1",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix 1.1.4",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
+
+[[package]]
+name = "xcursor"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.11.1",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+
+[[package]]
+name = "yazi"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
+
+[[package]]
+name = "zbus"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.4",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
+dependencies = [
+ "serde",
+ "winnow",
+ "zvariant",
+]
+
+[[package]]
+name = "zeno"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+ "winnow",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..54b66da
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "clear-system-interface"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clear-ui = { path = "../ui-framework" }
+winit = "0.30"
+wgpu = "24"
+bytemuck = { version = "1", features = ["derive"] }
+pollster = "0.4"
+taffy = "0.10.1"
+glyphon = "0.8"
+tokio = { version = "1", features = ["full"] }
+zbus = "5"
+futures = "0.3"
+serde = { version = "1", features = ["derive"] }
+
+[lib]
+name = "clear_system_interface"
+path = "src/lib.rs"
+
+[[bin]]
+path = "src/main.rs"
+name = "clear-system-interface"
diff --git a/src/app.rs b/src/app.rs
new file mode 100644
index 0000000..b9b0107
--- /dev/null
+++ b/src/app.rs
@@ -0,0 +1,94 @@
+use crate::pages::audio;
+use crate::pages::display;
+use crate::pages::input;
+use crate::pages::layout;
+use crate::pages::network;
+use crate::pages::power;
+use crate::pages::status;
+use crate::pages::storage;
+use crate::pages::system_info;
+use crate::pages::Page;
+
+pub struct AppState {
+    pub current_page: Page,
+    pub power: power::PowerState,
+    pub audio: audio::AudioState,
+    pub display: display::DisplayState,
+    pub network: network::NetworkState,
+    pub layout: layout::LayoutState,
+    pub input: input::InputState,
+    pub system_info: system_info::SystemState,
+    pub status: status::StatusState,
+    pub storage: storage::StorageState,
+}
+
+impl Default for AppState {
+    fn default() -> Self {
+        Self {
+            current_page: Page::ALL[0],
+            power: power::PowerState::default(),
+            audio: audio::AudioState::default(),
+            display: display::DisplayState::default(),
+            network: network::NetworkState::default(),
+            layout: layout::LayoutState::default(),
+            input: input::InputState::default(),
+            system_info: system_info::SystemState::default(),
+            status: status::StatusState::default(),
+            storage: storage::StorageState::default(),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum AppAction {
+    Power(power::PowerMessage),
+    Audio(audio::AudioMessage),
+    Display(display::DisplayMessage),
+    Radios(network::NetworkMessage),
+    Layout(layout::LayoutMessage),
+    Input(input::InputMessage),
+    SystemInfo(system_info::SystemMessage),
+    Status(status::StatusMessage),
+    Storage(storage::StorageMessage),
+}
+
+pub struct PageContent {
+    pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
+    pub texts: Vec<(String, f32, f32, f32, [f32; 4])>,
+    pub buttons: Vec<ContentButton>,
+}
+
+#[derive(Clone)]
+pub struct ContentButton {
+    pub x: f32, pub y: f32, pub w: f32, pub h: f32,
+    pub bg: [f32; 4],
+    pub hover_bg: [f32; 4],
+    pub label: String,
+    pub label_size: f32,
+    pub label_color: [f32; 4],
+    pub action: AppAction,
+}
+
+impl PageContent {
+    pub fn new() -> Self {
+        Self { rects: Vec::new(), texts: Vec::new(), buttons: Vec::new() }
+    }
+
+    pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
+        self.rects.push((color, x, y, w, h));
+    }
+
+    pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
+        self.texts.push((content.to_string(), size, x, y, color));
+    }
+
+    pub fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
+                  bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
+                  action: AppAction) {
+        self.buttons.push(ContentButton {
+            x, y, w, h, bg, hover_bg,
+            label: label.to_string(), label_size: 12.0, label_color,
+            action,
+        });
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..3d0af1e
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod app;
+pub mod pages;
+pub mod widgets;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..0512950
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,686 @@
+use std::sync::Arc;
+
+use winit::application::ApplicationHandler;
+use winit::event::{ElementState, MouseButton, WindowEvent};
+use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
+use winit::window::{Window, WindowAttributes};
+
+use clear_ui::color;
+use glyphon::{
+    Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
+    TextBounds, TextRenderer, Viewport,
+};
+
+use clear_system_interface::app::{AppAction, AppState, ContentButton, PageContent};
+use clear_system_interface::pages::{self, Page};
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+struct Vertex {
+    position: [f32; 2],
+    color: [f32; 4],
+}
+
+impl Vertex {
+    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+        0 => Float32x2,
+        1 => Float32x4,
+    ];
+
+    fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
+            step_mode: wgpu::VertexStepMode::Vertex,
+            attributes: &Self::ATTRIBS,
+        }
+    }
+}
+
+fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
+    let x0 = (x / sw) * 2.0 - 1.0;
+    let y0 = 1.0 - (y / sh) * 2.0;
+    let x1 = ((x + w) / sw) * 2.0 - 1.0;
+    let y1 = 1.0 - ((y + h) / sh) * 2.0;
+    [
+        Vertex { position: [x0, y0], color: c },
+        Vertex { position: [x1, y0], color: c },
+        Vertex { position: [x0, y1], color: c },
+        Vertex { position: [x1, y0], color: c },
+        Vertex { position: [x1, y1], color: c },
+        Vertex { position: [x0, y1], color: c },
+    ]
+}
+
+fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
+    let metrics = Metrics::new(size, size * 1.4);
+    let mut buf = Buffer::new(fs, metrics);
+    buf.set_text(fs, text, Attrs::new(), glyphon::Shaping::Advanced);
+    buf.shape_until_scroll(fs, true);
+    buf
+}
+
+struct AppWidget {
+    x: f32, y: f32, w: f32, h: f32,
+    color: [f32; 4],
+    hover_color: [f32; 4],
+    hovering: bool,
+    kind: WidgetKind,
+}
+
+#[derive(Clone)]
+enum WidgetKind {
+    PageButton(Page),
+    ActionButton(AppAction),
+    Static,
+}
+
+struct TextItem {
+    buffer: Buffer,
+    x: f32, y: f32,
+    color: glyphon::Color,
+}
+
+struct SystemInterface {
+    window: Arc<Window>,
+    surface: wgpu::Surface<'static>,
+    device: wgpu::Device,
+    queue: wgpu::Queue,
+    config: wgpu::SurfaceConfiguration,
+    render_pipeline: wgpu::RenderPipeline,
+    vertex_buffer: wgpu::Buffer,
+    vertex_count: u32,
+
+    app: AppState,
+
+    font_system: FontSystem,
+    swash_cache: SwashCache,
+    text_atlas: TextAtlas,
+    text_renderer: TextRenderer,
+    text_viewport: Viewport,
+
+    widgets: Vec<AppWidget>,
+    text_items: Vec<TextItem>,
+    page_buttons: Vec<ContentButton>,
+
+    sidebar_width: f32,
+    header_height: f32,
+    status_height: f32,
+
+    cursor_x: f32,
+    cursor_y: f32,
+
+    rx_power: std::sync::mpsc::Receiver<pages::power::PowerState>,
+    rx_audio: std::sync::mpsc::Receiver<pages::audio::AudioState>,
+    rx_display: std::sync::mpsc::Receiver<pages::display::DisplayState>,
+    rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
+    rx_layout: std::sync::mpsc::Receiver<pages::layout::LayoutState>,
+    rx_input: std::sync::mpsc::Receiver<pages::input::InputState>,
+    rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
+    rx_status: std::sync::mpsc::Receiver<pages::status::StatusState>,
+    rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageState>,
+
+    scale_factor: f64,
+    width: u32,
+    height: u32,
+    needs_rebuild: bool,
+}
+
+impl SystemInterface {
+    async fn new(window: Arc<Window>) -> Self {
+        let size = window.inner_size();
+        let sw = size.width as f32;
+        let sh = size.height as f32;
+
+        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
+            backends: wgpu::Backends::VULKAN,
+            ..Default::default()
+        });
+        let surface = instance.create_surface(window.clone()).expect("surface");
+        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
+            power_preference: wgpu::PowerPreference::HighPerformance,
+            compatible_surface: Some(&surface),
+            force_fallback_adapter: false,
+        }).await.expect("adapter");
+        let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
+            label: Some("GPU Device"),
+            required_features: wgpu::Features::empty(),
+            required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
+            memory_hints: wgpu::MemoryHints::MemoryUsage,
+        }, None).await.expect("device");
+        let config = surface.get_default_config(&adapter, size.width.max(1), size.height.max(1)).expect("config");
+        surface.configure(&device, &config);
+
+        let shader_code = clear_ui::SHADER;
+        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Shader"),
+            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
+        });
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("Pipeline Layout"),
+            bind_group_layouts: &[],
+            push_constant_ranges: &[],
+        });
+        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Render Pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                buffers: &[Vertex::desc()],
+                compilation_options: Default::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                targets: &[Some(wgpu::ColorTargetState {
+                    format: config.format,
+                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+                    write_mask: wgpu::ColorWrites::ALL,
+                })],
+                compilation_options: Default::default(),
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                front_face: wgpu::FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                unclipped_depth: false,
+                conservative: false,
+                strip_index_format: None,
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
+            multiview: None,
+            cache: None,
+        });
+
+        let font_system = FontSystem::new();
+        let swash_cache = SwashCache::new();
+        let cache = Cache::new(&device);
+        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+        let mut text_viewport = Viewport::new(&device, &cache);
+        text_viewport.update(&queue, Resolution { width: size.width, height: size.height });
+
+        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some("Vertex Buffer"),
+            size: 1,
+            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            mapped_at_creation: false,
+        });
+
+        // ── Initial state ──
+        let app = AppState {
+            power: pages::power::fetch_power_state().await,
+            audio: pages::audio::fetch_audio_state().await,
+            display: pages::display::fetch_display_state().await,
+            network: pages::network::fetch_network_state().await,
+            layout: pages::layout::read_layout_config(),
+            input: pages::input::read_input_config(),
+            system_info: pages::system_info::fetch_system_state().await,
+            status: pages::status::fetch_status_state().await,
+            storage: pages::storage::fetch_storage_state().await,
+            current_page: Page::ALL[0],
+        };
+
+        // ── Background refresh channels ──
+        fn spawn_bg<T, F>(period_secs: u64, f: fn() -> F) -> std::sync::mpsc::Receiver<T>
+        where
+            T: Send + 'static,
+            F: std::future::Future<Output = T> + Send + 'static,
+        {
+            let (tx, rx) = std::sync::mpsc::channel::<T>();
+            tokio::spawn(async move {
+                loop {
+                    tokio::time::sleep(std::time::Duration::from_secs(period_secs)).await;
+                    let val = f().await;
+                    if tx.send(val).is_err() { break; }
+                }
+            });
+            rx
+        }
+
+        let rx_power = spawn_bg(5, || pages::power::fetch_power_state());
+        let rx_audio = spawn_bg(3, || pages::audio::fetch_audio_state());
+        let rx_display = spawn_bg(10, || pages::display::fetch_display_state());
+        let rx_network = spawn_bg(5, || pages::network::fetch_network_state());
+        let rx_layout = {
+            let (tx, rx) = std::sync::mpsc::channel::<pages::layout::LayoutState>();
+            tokio::spawn(async move {
+                loop {
+                    tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+                    let val = tokio::task::spawn_blocking(|| pages::layout::read_layout_config()).await;
+                    if let Ok(val) = val { if tx.send(val).is_err() { break; } }
+                }
+            });
+            rx
+        };
+        let rx_input = {
+            let (tx, rx) = std::sync::mpsc::channel::<pages::input::InputState>();
+            tokio::spawn(async move {
+                loop {
+                    tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+                    let val = tokio::task::spawn_blocking(|| pages::input::read_input_config()).await;
+                    if let Ok(val) = val { if tx.send(val).is_err() { break; } }
+                }
+            });
+            rx
+        };
+        let rx_system = spawn_bg(5, || pages::system_info::fetch_system_state());
+        let rx_status = spawn_bg(10, || pages::status::fetch_status_state());
+        let rx_storage = spawn_bg(10, || pages::storage::fetch_storage_state());
+
+        let scale_factor = (window.scale_factor() as f32).max(2.0) as f64;
+        let mut this = Self {
+            window, surface, device, queue, config, render_pipeline,
+            vertex_buffer, vertex_count: 0,
+            app,
+            font_system, swash_cache, text_atlas, text_renderer, text_viewport,
+            widgets: Vec::new(), text_items: Vec::new(), page_buttons: Vec::new(),
+            sidebar_width: 140.0, header_height: 40.0, status_height: 28.0,
+            cursor_x: 0.0, cursor_y: 0.0,
+            scale_factor,
+            rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input,
+            rx_system, rx_status, rx_storage,
+            width: size.width, height: size.height,
+            needs_rebuild: true,
+        };
+        this.rebuild_layout(sw, sh);
+        this
+    }
+
+    fn rebuild_layout(&mut self, sw: f32, sh: f32) {
+        let s = self.scale_factor as f32;
+        let mut widgets = Vec::new();
+        let mut text_items = Vec::new();
+        let mut page_buttons = Vec::new();
+
+        let sb_w = self.sidebar_width * s;
+        let hdr_h = self.header_height * s;
+        let st_h = self.status_height * s;
+
+        // Header
+        widgets.push(AppWidget {
+            x: 0.0, y: 0.0, w: sw, h: hdr_h,
+            color: color::HEADER_BG, hover_color: color::HEADER_BG,
+            hovering: false, kind: WidgetKind::Static,
+        });
+        text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "Clear System Interface", 14.0 * s),
+            x: 12.0 * s, y: 12.0 * s,
+            color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
+        });
+
+        // Sidebar bg
+        widgets.push(AppWidget {
+            x: 0.0, y: hdr_h, w: sb_w, h: sh - hdr_h - st_h,
+            color: [0.16, 0.16, 0.26, 1.0],
+            hover_color: [0.16, 0.16, 0.26, 1.0],
+            hovering: false, kind: WidgetKind::Static,
+        });
+
+        // Sidebar page buttons
+        let btn_h = 32.0 * s;
+        let btn_margin = 4.0 * s;
+        let total = Page::ALL.len() as f32;
+        let sb_h = sh - hdr_h - st_h;
+        let start_y = hdr_h + (sb_h - total * (btn_h + btn_margin)) / 2.0;
+        let start_y = start_y.max(hdr_h + 8.0 * s);
+
+        for (i, page) in Page::ALL.iter().enumerate() {
+            let active = *page == self.app.current_page;
+            let y = start_y + i as f32 * (btn_h + btn_margin);
+            let bg = if active { color::BUTTON_PRESS } else { [0.18, 0.18, 0.28, 1.0] };
+            let hov = if active { color::BUTTON_IDLE } else { [0.22, 0.22, 0.34, 1.0] };
+            widgets.push(AppWidget {
+                x: 6.0 * s, y, w: sb_w - 12.0 * s, h: btn_h,
+                color: bg, hover_color: hov,
+                hovering: false,
+                kind: WidgetKind::PageButton(*page),
+            });
+            text_items.push(TextItem {
+                buffer: make_text_buffer(&mut self.font_system, page.label(), 12.0 * s),
+                x: 16.0 * s, y: y + 9.0 * s,
+                color: glyphon::Color::rgb(0x99, 0x99, 0xaa),
+            });
+        }
+
+        // Content area (logical coords, scaled to physical later)
+        let lcx = self.sidebar_width;
+        let lcy = self.header_height;
+        let lcw = sw / s - self.sidebar_width;
+        let lch = sh / s - self.header_height - self.status_height;
+        let p_cx = lcx * s;
+        let p_cy = lcy * s;
+        let p_cw = lcw * s;
+        let p_ch = lch * s;
+        widgets.push(AppWidget {
+            x: p_cx, y: p_cy, w: p_cw, h: p_ch,
+            color: color::CONTENT_BG, hover_color: color::CONTENT_BG,
+            hovering: false, kind: WidgetKind::Static,
+        });
+
+        // Page content in LOGICAL coordinates, then scale to physical
+        let pc = self.render_page_content(lcx, lcy, lcw, lch);
+
+        for (c, x, y, w, h) in &pc.rects {
+            widgets.push(AppWidget {
+                x: *x * s, y: *y * s, w: *w * s, h: *h * s,
+                color: *c, hover_color: *c,
+                hovering: false, kind: WidgetKind::Static,
+            });
+        }
+        for (t, size, x, y, tc) in &pc.texts {
+            text_items.push(TextItem {
+                buffer: make_text_buffer(&mut self.font_system, t, *size * s),
+                x: *x * s, y: *y * s,
+                color: glyphon::Color::rgb(
+                    (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
+                ),
+            });
+        }
+        for btn in &pc.buttons {
+            widgets.push(AppWidget {
+                x: btn.x * s, y: btn.y * s, w: btn.w * s, h: btn.h * s,
+                color: btn.bg, hover_color: btn.hover_bg,
+                hovering: false,
+                kind: WidgetKind::ActionButton(btn.action.clone()),
+            });
+            text_items.push(TextItem {
+                buffer: make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s),
+                x: btn.x * s + 10.0 * s, y: btn.y * s + 8.0 * s,
+                color: glyphon::Color::rgb(
+                    (btn.label_color[0] * 255.0) as u8,
+                    (btn.label_color[1] * 255.0) as u8,
+                    (btn.label_color[2] * 255.0) as u8,
+                ),
+            });
+            let mut cb = btn.clone();
+            cb.x *= s; cb.y *= s; cb.w *= s; cb.h *= s;
+            page_buttons.push(cb);
+        }
+
+        // Status bar
+        widgets.push(AppWidget {
+            x: 0.0, y: sh - st_h, w: sw, h: st_h,
+            color: color::STATUS_BG, hover_color: color::STATUS_BG,
+            hovering: false, kind: WidgetKind::Static,
+        });
+
+        self.widgets = widgets;
+        self.text_items = text_items;
+        self.page_buttons = page_buttons;
+        self.needs_rebuild = false;
+    }
+
+    fn render_page_content(&self, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
+        use pages::*;
+        match self.app.current_page {
+            Page::Power => power::view(&self.app.power, cx, cy, cw, ch),
+            Page::Audio => audio::view(&self.app.audio, cx, cy, cw, ch),
+            Page::Display => display::view(&self.app.display, cx, cy, cw, ch),
+            Page::Radios => network::view(&self.app.network, cx, cy, cw, ch),
+            Page::Layout => layout::view(&self.app.layout, cx, cy, cw, ch),
+            Page::Input => input::view(&self.app.input, cx, cy, cw, ch),
+            Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch),
+            Page::Status => status::view(&self.app.status, cx, cy, cw, ch),
+            Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch),
+        }
+    }
+
+    fn collect_vertices(&self) -> Vec<Vertex> {
+        let sw = self.width as f32;
+        let sh = self.height as f32;
+        let mut verts = Vec::new();
+        for w in &self.widgets {
+            verts.extend(quad_vertices(w.x, w.y, w.w, w.h, sw, sh, w.color));
+        }
+        verts
+    }
+
+    fn upload_vertices(&mut self) {
+        let verts = self.collect_vertices();
+        self.vertex_count = verts.len() as u32;
+        let data = bytemuck::cast_slice(&verts);
+        let needed = data.len() as wgpu::BufferAddress;
+        if needed > self.vertex_buffer.size() {
+            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("Vertex Buffer"),
+                size: needed,
+                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            });
+        }
+        self.queue.write_buffer(&self.vertex_buffer, 0, data);
+    }
+
+    fn prepare_text(&mut self) {
+        let Self {
+            ref mut text_renderer, ref device, ref queue,
+            ref mut font_system, ref mut text_atlas,
+            ref mut text_viewport, ref mut swash_cache,
+            ref text_items, width, height, ..
+        } = self;
+
+        let w = *width as f32;
+        let h = *height as f32;
+        let viewport = Resolution { width: w as u32, height: h as u32 };
+        text_viewport.update(queue, viewport);
+        let bounds = TextBounds { left: 0, top: 0, right: w as i32, bottom: h as i32 };
+        let areas: Vec<TextArea> = text_items.iter().map(|ti| TextArea {
+            buffer: &ti.buffer,
+            left: ti.x, top: ti.y, scale: 1.0, bounds,
+            default_color: ti.color,
+            custom_glyphs: &[],
+        }).collect();
+        text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
+    }
+
+    fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
+        if size.width > 0 && size.height > 0 {
+            self.width = size.width; self.height = size.height;
+            self.config.width = size.width; self.config.height = size.height;
+            self.surface.configure(&self.device, &self.config);
+            self.needs_rebuild = true;
+        }
+    }
+
+    fn poll_background_updates(&mut self) {
+        use pages::*;
+        while let Ok(s) = self.rx_power.try_recv() {
+            power::update(&mut self.app.power, power::PowerMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_audio.try_recv() {
+            audio::update(&mut self.app.audio, audio::AudioMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_display.try_recv() {
+            display::update(&mut self.app.display, display::DisplayMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_network.try_recv() {
+            network::update(&mut self.app.network, network::NetworkMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_layout.try_recv() {
+            layout::update(&mut self.app.layout, layout::LayoutMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_input.try_recv() {
+            input::update(&mut self.app.input, input::InputMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_system.try_recv() {
+            system_info::update(&mut self.app.system_info, system_info::SystemMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_status.try_recv() {
+            status::update(&mut self.app.status, status::StatusMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+        while let Ok(s) = self.rx_storage.try_recv() {
+            storage::update(&mut self.app.storage, storage::StorageMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
+    }
+
+    fn handle_action(&mut self, action: &AppAction) {
+        use pages::*;
+        match action {
+            AppAction::Power(m) => power::update(&mut self.app.power, m.clone()),
+            AppAction::Audio(m) => audio::update(&mut self.app.audio, m.clone()),
+            AppAction::Display(m) => display::update(&mut self.app.display, m.clone()),
+            AppAction::Radios(m) => network::update(&mut self.app.network, m.clone()),
+            AppAction::Layout(m) => layout::update(&mut self.app.layout, m.clone()),
+            AppAction::Input(m) => input::update(&mut self.app.input, m.clone()),
+            AppAction::SystemInfo(m) => system_info::update(&mut self.app.system_info, m.clone()),
+            AppAction::Status(m) => status::update(&mut self.app.status, m.clone()),
+            AppAction::Storage(m) => storage::update(&mut self.app.storage, m.clone()),
+        }
+    }
+
+    fn handle_event(&mut self, event: &WindowEvent) -> bool {
+        match event {
+            WindowEvent::CursorMoved { position, .. } => {
+                self.cursor_x = position.x as f32;
+                self.cursor_y = position.y as f32;
+                let mut changed = false;
+                for w in &mut self.widgets {
+                    let was = w.hovering;
+                    w.hovering = self.cursor_x >= w.x && self.cursor_x <= w.x + w.w
+                        && self.cursor_y >= w.y && self.cursor_y <= w.y + w.h;
+                    if w.hovering != was {
+                        w.color = if w.hovering { w.hover_color } else { w.color };
+                        changed = true;
+                    }
+                }
+                if changed { self.needs_rebuild = true; }
+                changed
+            }
+            WindowEvent::MouseInput { state, button, .. } => {
+                if *button != MouseButton::Left { return false; }
+                if *state == ElementState::Released {
+                    let (px, py) = (self.cursor_x, self.cursor_y);
+                    for btn in &self.page_buttons.clone() {
+                        if px >= btn.x && px <= btn.x + btn.w && py >= btn.y && py <= btn.y + btn.h {
+                            self.handle_action(&btn.action);
+                            self.needs_rebuild = true;
+                            return true;
+                        }
+                    }
+                    for w in &self.widgets {
+                        if px >= w.x && px <= w.x + w.w && py >= w.y && py <= w.y + w.h {
+                            if let WidgetKind::PageButton(p) = &w.kind {
+                                if self.app.current_page != *p {
+                                    self.app.current_page = *p;
+                                    self.needs_rebuild = true;
+                                    return true;
+                                }
+                            }
+                        }
+                    }
+                }
+                self.needs_rebuild = true;
+                true
+            }
+            _ => false,
+        }
+    }
+
+    fn render(&mut self) {
+        self.poll_background_updates();
+
+        let sw = self.width as f32;
+        let sh = self.height as f32;
+
+        if self.needs_rebuild {
+            self.rebuild_layout(sw, sh);
+            self.upload_vertices();
+        }
+
+        self.prepare_text();
+
+        let output = match self.surface.get_current_texture() {
+            Ok(t) => t,
+            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+                self.surface.configure(&self.device, &self.config);
+                return;
+            }
+            Err(wgpu::SurfaceError::Timeout) => return,
+            Err(e) => { eprintln!("Surface error: {e:?}"); return; }
+        };
+
+        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("Encoder"),
+        });
+
+        {
+            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("Render Pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: &view,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.06, g: 0.06, b: 0.08, a: 1.0 }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            pass.set_pipeline(&self.render_pipeline);
+            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
+            pass.draw(0..self.vertex_count, 0..1);
+
+            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
+        }
+
+        self.queue.submit(std::iter::once(encoder.finish()));
+        self.window.pre_present_notify();
+        output.present();
+    }
+}
+
+struct App { state: Option<SystemInterface> }
+impl App {
+    fn new() -> Self { Self { state: None } }
+}
+
+impl ApplicationHandler for App {
+    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+        if self.state.is_some() { return; }
+        let window = Arc::new(event_loop.create_window(
+            WindowAttributes::default()
+                .with_title("Clear System Interface")
+                .with_inner_size(winit::dpi::LogicalSize::new(820, 680)),
+        ).unwrap());
+        let state = pollster::block_on(SystemInterface::new(window));
+        self.state = Some(state);
+        self.state.as_ref().unwrap().window.request_redraw();
+    }
+
+    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: winit::window::WindowId, event: WindowEvent) {
+        let redraw = match &event {
+            WindowEvent::CloseRequested => { event_loop.exit(); true }
+            WindowEvent::Resized(s) => { if let Some(st) = &mut self.state { st.resize(*s); } true }
+            WindowEvent::RedrawRequested => {
+                if let Some(st) = &mut self.state { st.render(); st.window.request_redraw(); }
+                true
+            }
+            _ => self.state.as_mut().map(|st| st.handle_event(&event)).unwrap_or(false)
+        };
+        if redraw { if let Some(st) = &mut self.state { st.window.request_redraw(); } }
+    }
+}
+
+fn main() {
+    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let _guard = rt.enter();
+    let event_loop = EventLoop::new().unwrap();
+    event_loop.set_control_flow(ControlFlow::Poll);
+    event_loop.run_app(&mut App::new()).unwrap();
+}
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
new file mode 100644
index 0000000..6608676
--- /dev/null
+++ b/src/pages/audio.rs
@@ -0,0 +1,355 @@
+use crate::app::{AppAction, PageContent};
+
+#[derive(Debug, Clone)]
+pub struct AudioSink {
+    pub id: u32,
+    pub name: String,
+    pub volume: f32,
+    pub muted: bool,
+    pub active: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct AudioSource {
+    pub id: u32,
+    pub name: String,
+    pub volume: f32,
+    pub muted: bool,
+    pub active: bool,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AudioState {
+    pub sinks: Vec<AudioSink>,
+    pub sources: Vec<AudioSource>,
+}
+
+#[derive(Debug, Clone)]
+pub enum AudioMessage {
+    Refreshed(AudioState),
+    SinkVolume(u32, f32),
+    SinkMute(u32),
+    SourceVolume(u32, f32),
+    SourceMute(u32),
+}
+
+fn drm_connected_ports() -> Vec<String> {
+    let mut connected = Vec::new();
+    let Ok(entries) = std::fs::read_dir("/sys/class/drm") else {
+        return connected;
+    };
+    for entry in entries.flatten() {
+        let name = entry.file_name().to_string_lossy().to_string();
+        let status_path = entry.path().join("status");
+        if status_path.exists() {
+            if let Ok(s) = std::fs::read_to_string(&status_path) {
+                if s.trim() == "connected" {
+                    connected.push(name);
+                }
+            }
+        }
+    }
+    connected
+}
+
+fn is_hdmi_active(name: &str, connected_ports: &[String]) -> bool {
+    if !name.contains("HDMI") {
+        return true;
+    }
+    for port in connected_ports {
+        if !port.contains("HDMI") {
+            continue;
+        }
+        if let Some(num) = name.split("HDMI").nth(1).and_then(|s| s.chars().next()) {
+            if port.contains(&format!("HDMI-A-{}", num)) {
+                return true;
+            }
+        }
+    }
+    false
+}
+
+fn short_name(name: &str) -> String {
+    if let Some(hifi_part) = name.split("HiFi__").nth(1) {
+        let label = hifi_part.replace("__sink", "").replace("_", " ");
+        return label;
+    }
+    if let Some(alsa) = name.strip_prefix("alsa_output.") {
+        return alsa.split('.').next_back().unwrap_or(alsa).replace("_", " ");
+    }
+    if let Some(alsa) = name.strip_prefix("alsa_input.") {
+        return alsa.split('.').next_back().unwrap_or(alsa).replace("_", " ");
+    }
+    name.to_string()
+}
+
+fn set_sink_volume(id: u32, vol: f32) {
+    let pct = (vol * 100.0).round() as u32;
+    let _ = tokio::process::Command::new("pactl")
+        .args(["set-sink-volume", &id.to_string(), &format!("{}%", pct)])
+        .spawn();
+}
+
+fn set_sink_mute(id: u32, mute: bool) {
+    let _ = tokio::process::Command::new("pactl")
+        .args(["set-sink-mute", &id.to_string(), if mute { "1" } else { "0" }])
+        .spawn();
+}
+
+fn set_source_volume(id: u32, vol: f32) {
+    let pct = (vol * 100.0).round() as u32;
+    let _ = tokio::process::Command::new("pactl")
+        .args(["set-source-volume", &id.to_string(), &format!("{}%", pct)])
+        .spawn();
+}
+
+fn set_source_mute(id: u32, mute: bool) {
+    let _ = tokio::process::Command::new("pactl")
+        .args(["set-source-mute", &id.to_string(), if mute { "1" } else { "0" }])
+        .spawn();
+}
+
+pub async fn fetch_audio_state() -> AudioState {
+    let connected = drm_connected_ports();
+    let sinks = fetch_sinks(&connected).await;
+    let sources = fetch_sources(&connected).await;
+    AudioState { sinks, sources }
+}
+
+async fn fetch_sinks(connected_ports: &[String]) -> Vec<AudioSink> {
+    let short_out = match tokio::process::Command::new("pactl")
+        .args(["list", "sinks", "short"]).output().await
+    {
+        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
+        Err(_) => return Vec::new(),
+    };
+
+    let mut sinks = Vec::new();
+    for line in short_out.lines() {
+        let parts: Vec<&str> = line.split('\t').collect();
+        if parts.len() < 2 { continue; }
+        let id = parts[0].parse::<u32>().unwrap_or(0);
+        let name = parts[1].to_string();
+
+        let vol = tokio::process::Command::new("pactl")
+            .args(["get-sink-volume", &id.to_string()]).output().await.ok()
+            .and_then(|o| {
+                let s = String::from_utf8_lossy(&o.stdout);
+                s.split('/').nth(1)
+                    .and_then(|v| v.trim().trim_end_matches('%').parse::<f32>().ok())
+            }).unwrap_or(50.0);
+
+        let muted = tokio::process::Command::new("pactl")
+            .args(["get-sink-mute", &id.to_string()]).output().await.ok()
+            .map(|o| String::from_utf8_lossy(&o.stdout).contains("yes"))
+            .unwrap_or(false);
+
+        sinks.push(AudioSink {
+            id, name: short_name(&name), volume: vol / 100.0, muted,
+            active: is_hdmi_active(&name, connected_ports),
+        });
+    }
+    sinks.sort_by(|a, b| b.id.cmp(&a.id));
+    sinks
+}
+
+async fn fetch_sources(connected_ports: &[String]) -> Vec<AudioSource> {
+    let short_out = match tokio::process::Command::new("pactl")
+        .args(["list", "sources", "short"]).output().await
+    {
+        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
+        Err(_) => return Vec::new(),
+    };
+
+    let mut sources = Vec::new();
+    for line in short_out.lines() {
+        let parts: Vec<&str> = line.split('\t').collect();
+        if parts.len() < 2 { continue; }
+        let id = parts[0].parse::<u32>().unwrap_or(0);
+        let name = parts[1].to_string();
+
+        if name.contains(".monitor") { continue; }
+
+        let vol = tokio::process::Command::new("pactl")
+            .args(["get-source-volume", &id.to_string()]).output().await.ok()
+            .and_then(|o| {
+                let s = String::from_utf8_lossy(&o.stdout);
+                s.split('/').nth(1)
+                    .and_then(|v| v.trim().trim_end_matches('%').parse::<f32>().ok())
+            }).unwrap_or(50.0);
+
+        let muted = tokio::process::Command::new("pactl")
+            .args(["get-source-mute", &id.to_string()]).output().await.ok()
+            .map(|o| String::from_utf8_lossy(&o.stdout).contains("yes"))
+            .unwrap_or(false);
+
+        sources.push(AudioSource {
+            id, name: short_name(&name), volume: vol / 100.0, muted,
+            active: is_hdmi_active(&name, connected_ports),
+        });
+    }
+    sources.sort_by(|a, b| b.id.cmp(&a.id));
+    sources
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const MUTED_BG: [f32; 4] = [0.33, 0.20, 0.20, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
+const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
+
+pub fn view(state: &AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── Output section ──
+    pc.text("Output", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    if state.sinks.is_empty() {
+        pc.text("No output devices found", cx + 12.0, y, 12.0, TEXT_DIM);
+        y += 18.0;
+    }
+
+    for sink in &state.sinks {
+        let label = if !sink.active {
+            format!("{}  (inactive)", sink.name)
+        } else if sink.muted {
+            format!("{}  {:.0}%  (muted)", sink.name, sink.volume * 100.0)
+        } else {
+            format!("{}  {:.0}%", sink.name, sink.volume * 100.0)
+        };
+        let lc = if sink.muted { RED } else { TEXT_FG };
+        pc.text(&label, cx + 14.0, y, 13.0, lc);
+        y += 18.0;
+
+        if sink.active {
+            // Volume bar (visual only — we use +/- buttons)
+            let bar_w = cw - 100.0;
+            let bar_x = cx + 14.0;
+            pc.rect(BLANK_BAR, bar_x, y, bar_w, 8.0);
+            pc.rect(FILL_BAR, bar_x, y, bar_w * sink.volume, 8.0);
+            pc.text(&format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, y - 2.0, 11.0, TEXT_DIM);
+
+            // Buttons
+            let btn_h = 28.0;
+            let btn_y = y + 12.0;
+            let mut bx = cx + 14.0;
+
+            pc.button("-10", bx, btn_y, 36.0, btn_h,
+                BTN_INACTIVE, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SinkVolume(sink.id, (sink.volume - 0.10).max(0.0))));
+            bx += 40.0;
+
+            let mute_label = if sink.muted { "Unmute" } else { "Mute" };
+            let mute_bg = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
+            pc.button(mute_label, bx, btn_y, 52.0, btn_h,
+                mute_bg, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SinkMute(sink.id)));
+            bx += 56.0;
+
+            pc.button("+10", bx, btn_y, 36.0, btn_h,
+                BTN_ACTIVE, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SinkVolume(sink.id, (sink.volume + 0.10).min(1.0))));
+
+            y = btn_y + btn_h + 6.0;
+        } else {
+            y += 6.0;
+        }
+    }
+
+    // ── Input section ──
+    y += 8.0;
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Input", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    if state.sources.is_empty() {
+        pc.text("No input devices found", cx + 12.0, y, 12.0, TEXT_DIM);
+        y += 18.0;
+    }
+
+    for src in &state.sources {
+        let label = if !src.active {
+            format!("{}  (inactive)", src.name)
+        } else if src.muted {
+            format!("{}  {:.0}%  (muted)", src.name, src.volume * 100.0)
+        } else {
+            format!("{}  {:.0}%", src.name, src.volume * 100.0)
+        };
+        let lc = if src.muted { RED } else { TEXT_FG };
+        pc.text(&label, cx + 14.0, y, 13.0, lc);
+        y += 18.0;
+
+        if src.active {
+            let bar_w = cw - 100.0;
+            let bar_x = cx + 14.0;
+            pc.rect(BLANK_BAR, bar_x, y, bar_w, 8.0);
+            pc.rect(FILL_BAR, bar_x, y, bar_w * src.volume, 8.0);
+            pc.text(&format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, y - 2.0, 11.0, TEXT_DIM);
+
+            let btn_h = 28.0;
+            let btn_y = y + 12.0;
+            let mut bx = cx + 14.0;
+
+            pc.button("-10", bx, btn_y, 36.0, btn_h,
+                BTN_INACTIVE, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SourceVolume(src.id, (src.volume - 0.10).max(0.0))));
+            bx += 40.0;
+
+            let mute_label = if src.muted { "Unmute" } else { "Mute" };
+            let mute_bg = if src.muted { MUTED_BG } else { BTN_INACTIVE };
+            pc.button(mute_label, bx, btn_y, 52.0, btn_h,
+                mute_bg, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SourceMute(src.id)));
+            bx += 56.0;
+
+            pc.button("+10", bx, btn_y, 36.0, btn_h,
+                BTN_ACTIVE, BTN_HOVER, WHITE,
+                AppAction::Audio(AudioMessage::SourceVolume(src.id, (src.volume + 0.10).min(1.0))));
+
+            y = btn_y + btn_h + 6.0;
+        } else {
+            y += 6.0;
+        }
+    }
+
+    pc
+}
+
+pub fn update(state: &mut AudioState, msg: AudioMessage) {
+    match msg {
+        AudioMessage::Refreshed(new) => { *state = new; }
+        AudioMessage::SinkVolume(id, vol) => {
+            if let Some(sink) = state.sinks.iter_mut().find(|s| s.id == id) {
+                sink.volume = vol;
+                set_sink_volume(id, vol);
+            }
+        }
+        AudioMessage::SinkMute(id) => {
+            if let Some(sink) = state.sinks.iter_mut().find(|s| s.id == id) {
+                sink.muted = !sink.muted;
+                set_sink_mute(id, sink.muted);
+            }
+        }
+        AudioMessage::SourceVolume(id, vol) => {
+            if let Some(src) = state.sources.iter_mut().find(|s| s.id == id) {
+                src.volume = vol;
+                set_source_volume(id, vol);
+            }
+        }
+        AudioMessage::SourceMute(id) => {
+            if let Some(src) = state.sources.iter_mut().find(|s| s.id == id) {
+                src.muted = !src.muted;
+                set_source_mute(id, src.muted);
+            }
+        }
+    }
+}
diff --git a/src/pages/display.rs b/src/pages/display.rs
new file mode 100644
index 0000000..a23cb0f
--- /dev/null
+++ b/src/pages/display.rs
@@ -0,0 +1,196 @@
+use crate::app::{AppAction, PageContent};
+
+#[derive(Debug, Clone)]
+pub struct DisplayOutput {
+    pub name: String,
+    pub resolution: String,
+    pub refresh: String,
+    pub scale: f32,
+    pub connected: bool,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct DisplayState {
+    pub brightness: f32,
+    pub max_brightness: f32,
+    pub outputs: Vec<DisplayOutput>,
+    pub night_light: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum DisplayMessage {
+    Refreshed(DisplayState),
+    BrightnessDecrement,
+    BrightnessIncrement,
+}
+
+pub async fn fetch_display_state() -> DisplayState {
+    let (brightness, max_brightness) = fetch_brightness().await;
+    let outputs = fetch_outputs().await;
+    let night_light = is_night_light_on().await;
+    DisplayState { brightness, max_brightness, outputs, night_light }
+}
+
+async fn fetch_brightness() -> (f32, f32) {
+    let cur = tokio::process::Command::new("brightnessctl")
+        .arg("get").output().await.ok()
+        .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<f32>().ok())
+        .unwrap_or(50.0);
+    let max = tokio::process::Command::new("brightnessctl")
+        .arg("max").output().await.ok()
+        .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<f32>().ok())
+        .unwrap_or(100.0);
+    (cur, max)
+}
+
+async fn fetch_outputs() -> Vec<DisplayOutput> {
+    let output = tokio::process::Command::new("wlr-randr")
+        .output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+        .unwrap_or_default();
+
+    let mut displays = Vec::new();
+    let mut current: Option<DisplayOutput> = None;
+
+    for line in output.lines() {
+        let trimmed = line.trim();
+        if !trimmed.starts_with(' ') && trimmed.contains('"') {
+            if let Some(prev) = current.take() { displays.push(prev); }
+            let name = trimmed.split_whitespace().next().unwrap_or("").to_string();
+            current = Some(DisplayOutput {
+                name, resolution: String::new(), refresh: String::new(),
+                scale: 1.0, connected: true,
+            });
+            continue;
+        }
+        if let Some(ref mut out) = current {
+            if trimmed.contains("px,") && trimmed.contains("Hz") && trimmed.contains("current") {
+                let parts: Vec<&str> = trimmed.split_whitespace().collect();
+                if let Some(mode) = parts.first() { out.resolution = mode.to_string(); }
+                if let Some(hz_idx) = parts.iter().position(|p| *p == "Hz") {
+                    if hz_idx > 0 { out.refresh = parts[hz_idx - 1].to_string(); }
+                }
+            } else if let Some(rest) = trimmed.strip_prefix("Scale:") {
+                out.scale = rest.trim().parse::<f32>().unwrap_or(1.0);
+            } else if trimmed.starts_with("Enabled:") && trimmed.contains("no") {
+                out.connected = false;
+            }
+        }
+    }
+    if let Some(prev) = current.take() { displays.push(prev); }
+    displays
+}
+
+async fn is_night_light_on() -> bool {
+    tokio::process::Command::new("gdbus")
+        .args(["call", "--session", "--dest", "org.gnome.SettingsDaemon.Color",
+               "--object-path", "/org/gnome/SettingsDaemon/Color",
+               "--method", "org.gnome.SettingsDaemon.Color.Get", "night-light-enabled"])
+        .output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).contains("true"))
+        .unwrap_or(false)
+}
+
+fn spawn_brightness(pct: u32) {
+    let _ = tokio::process::Command::new("brightnessctl")
+        .args(["set", &format!("{}%", pct)]).spawn();
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
+const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── Brightness ──
+    pc.text("Brightness", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    let bright_pct = if state.max_brightness > 0.0 {
+        (state.brightness / state.max_brightness * 100.0) as u32
+    } else { 0 };
+
+    let bar_w = cw - 100.0;
+    pc.rect(BLANK_BAR, cx + 12.0, y, bar_w, 8.0);
+    pc.rect(FILL_BAR, cx + 12.0, y, bar_w * bright_pct as f32 / 100.0, 8.0);
+    pc.text(&format!("{}%", bright_pct), cx + 16.0 + bar_w, y - 2.0, 11.0, TEXT_DIM);
+    y += 14.0;
+
+    let btn_h = 28.0;
+    let btn_y = y;
+    pc.button("-10", cx + 12.0, btn_y, 36.0, btn_h,
+        BTN_INACTIVE, BTN_HOVER, WHITE,
+        AppAction::Display(DisplayMessage::BrightnessDecrement));
+    pc.text("brightnessctl set", cx + 56.0, btn_y + 8.0, 10.0, TEXT_DIM);
+    pc.button("+10", cx + 12.0 + bar_w - 36.0, btn_y, 36.0, btn_h,
+        BTN_ACTIVE, BTN_HOVER, WHITE,
+        AppAction::Display(DisplayMessage::BrightnessIncrement));
+
+    y = btn_y + btn_h + 12.0;
+
+    // ── Night Light ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
+    pc.text(nl_label, cx + 12.0, y, 13.0, TEXT_FG);
+    y += 24.0;
+
+    // ── Outputs ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Outputs", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    for out in &state.outputs {
+        if out.connected {
+            let scale_info = if out.scale > 1.0 {
+                if let Some((w_str, h_str)) = out.resolution.rsplit_once('x') {
+                    if let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>()) {
+                        format!("  logical {:.0}x{:.0} | scale {:.0}x", w as f32 / out.scale, h as f32 / out.scale, out.scale)
+                    } else { format!("  scale {:.0}x", out.scale) }
+                } else { String::new() }
+            } else { String::new() };
+            pc.text(&format!("{}  {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
+                cx + 14.0, y, 12.0, TEXT_FG);
+        } else {
+            pc.text(&format!("{}  (disconnected)", out.name), cx + 14.0, y, 12.0, TEXT_DIM);
+        }
+        y += 18.0;
+    }
+
+    pc
+}
+
+pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
+    match msg {
+        DisplayMessage::Refreshed(new) => { *state = new; }
+        DisplayMessage::BrightnessDecrement => {
+            let pct = if state.max_brightness > 0.0 {
+                (state.brightness / state.max_brightness * 100.0) as u32
+            } else { 0 };
+            if pct > 0 {
+                let new_pct = pct.saturating_sub(10).max(0);
+                state.brightness = new_pct as f32 / 100.0 * state.max_brightness;
+                spawn_brightness(new_pct);
+            }
+        }
+        DisplayMessage::BrightnessIncrement => {
+            let pct = if state.max_brightness > 0.0 {
+                (state.brightness / state.max_brightness * 100.0) as u32
+            } else { 0 };
+            if pct < 100 {
+                let new_pct = (pct + 10).min(100);
+                state.brightness = new_pct as f32 / 100.0 * state.max_brightness;
+                spawn_brightness(new_pct);
+            }
+        }
+    }
+}
diff --git a/src/pages/input.rs b/src/pages/input.rs
new file mode 100644
index 0000000..dafef86
--- /dev/null
+++ b/src/pages/input.rs
@@ -0,0 +1,251 @@
+use std::fs;
+use std::io::Write;
+
+use crate::app::{AppAction, PageContent};
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
+const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
+
+#[derive(Debug, Clone)]
+pub struct Keybind {
+    pub mods: String,
+    pub key: String,
+    pub action: String,
+    pub command: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct InputState {
+    pub tap_to_click: bool,
+    pub repeat_rate: u16,
+    pub repeat_delay: u16,
+    pub keybinds: Vec<Keybind>,
+}
+
+impl Default for InputState {
+    fn default() -> Self {
+        Self { tap_to_click: false, repeat_rate: 50, repeat_delay: 300, keybinds: Vec::new() }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum InputMessage {
+    ToggleTapToClick,
+    RepeatRateDown,
+    RepeatRateUp,
+    RepeatDelayDown,
+    RepeatDelayUp,
+    Refreshed(InputState),
+}
+
+pub fn read_input_config() -> InputState {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    InputState {
+        tap_to_click: parse_bool_from(&content, "tap_to_click"),
+        repeat_rate: parse_u16_key(&content, "rate", 50),
+        repeat_delay: parse_u16_key(&content, "delay", 300),
+        keybinds: parse_keybinds(&content),
+    }
+}
+
+fn parse_bool_from(content: &str, key: &str) -> bool {
+    content.lines().find(|l| l.trim().starts_with(key))
+        .and_then(|l| l.split('=').nth(1))
+        .map(|v| v.trim() == "true")
+        .unwrap_or(false)
+}
+
+fn parse_u16_key(content: &str, key: &str, default: u16) -> u16 {
+    content.lines().find(|l| l.trim().starts_with(key))
+        .and_then(|l| l.split('=').nth(1))
+        .and_then(|v| v.trim().parse::<u16>().ok())
+        .unwrap_or(default)
+}
+
+fn parse_keybinds(content: &str) -> Vec<Keybind> {
+    let mut keybinds = Vec::new();
+    let mut current: Option<Keybind> = None;
+
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if trimmed == "[[keybind]]" {
+            if let Some(kb) = current.take() { keybinds.push(kb); }
+            current = Some(Keybind {
+                mods: String::new(), key: String::new(),
+                action: String::new(), command: String::new(),
+            });
+            continue;
+        }
+        if let Some(ref mut kb) = current {
+            let set = |rest: &str| rest.trim_start_matches(|c: char| c == ' ' || c == '=').trim_matches('"').to_string();
+            if let Some(rest) = trimmed.strip_prefix("mods") { kb.mods = set(rest); }
+            else if let Some(rest) = trimmed.strip_prefix("key") { kb.key = set(rest); }
+            else if let Some(rest) = trimmed.strip_prefix("action") { kb.action = set(rest); }
+            else if let Some(rest) = trimmed.strip_prefix("command") { kb.command = set(rest); }
+        }
+    }
+    if let Some(kb) = current.take() { keybinds.push(kb); }
+    keybinds
+}
+
+fn send_ipc_command(cmd: &str) {
+    if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(CLEARWM_SOCK) {
+        let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
+    }
+}
+
+fn write_config_value(key: &str, value: &str) {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let new_line = format!("{} = {}", key, value);
+
+    let mut found = false;
+    let updated: String = content.lines()
+        .map(|line| {
+            if line.trim().starts_with(key) { found = true; new_line.clone() }
+            else { line.to_string() }
+        })
+        .collect::<Vec<_>>()
+        .join("\n");
+
+    if !found {
+        let section = if key == "tap_to_click" { "[input]" } else { "[repeat]" };
+        let mut result = String::new();
+        let mut in_section = false;
+        let mut inserted = false;
+        for line in updated.lines() {
+            if line.trim() == section { in_section = true; }
+            else if line.trim().starts_with('[') && in_section {
+                if !inserted { result.push_str(&new_line); result.push('\n'); inserted = true; }
+                in_section = false;
+            }
+            result.push_str(line);
+            result.push('\n');
+        }
+        if !inserted {
+            if !in_section { result.push('\n'); result.push_str(section); result.push('\n'); }
+            result.push_str(&new_line);
+            result.push('\n');
+        }
+        let _ = fs::write(CONFIG_PATH, result);
+    } else {
+        let _ = fs::write(CONFIG_PATH, updated);
+    }
+}
+
+fn write_tap_to_click(enabled: bool) {
+    write_config_value("tap_to_click", &enabled.to_string());
+    send_ipc_command(&format!("input tap-to-click {}", enabled));
+}
+
+fn apply_repeat_config(rate: u16, delay: u16) {
+    write_config_value("rate", &rate.to_string());
+    write_config_value("delay", &delay.to_string());
+    send_ipc_command(&format!("repeat rate {}", rate));
+    send_ipc_command(&format!("repeat delay {}", delay));
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const TOGGLE_ON: [f32; 4] = [0.16, 0.41, 0.18, 1.0];
+const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── Touchpad ──
+    pc.text("Touchpad", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    let tap_bg = if state.tap_to_click { TOGGLE_ON } else { TOGGLE_OFF };
+    let tap_label = if state.tap_to_click { "Tap to Click: ON" } else { "Tap to Click: OFF" };
+    pc.text(tap_label, cx + 14.0, y + 6.0, 13.0, if state.tap_to_click { ACCENT } else { TEXT_DIM });
+    let btn_w = (cw - 32.0).min(100.0);
+    pc.button(if state.tap_to_click { "ON" } else { "OFF" }, cx + cw - btn_w - 14.0, y, btn_w, 28.0,
+        tap_bg, BTN_HOVER, WHITE,
+        AppAction::Input(InputMessage::ToggleTapToClick));
+    y += 36.0;
+
+    // ── Keyboard ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Keyboard", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    // Repeat Rate
+    pc.text(&format!("Repeat Rate: {} /sec", state.repeat_rate), cx + 14.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+    pc.button("-1", cx + 14.0, y, 36.0, 28.0, BTN_INACTIVE, BTN_HOVER, WHITE,
+        AppAction::Input(InputMessage::RepeatRateDown));
+    pc.text(&format!(" {} ", state.repeat_rate), cx + 58.0, y + 7.0, 13.0, TEXT_FG);
+    pc.button("+1", cx + 14.0 + 36.0 + 8.0, y, 36.0, 28.0, BTN_ACTIVE, BTN_HOVER, WHITE,
+        AppAction::Input(InputMessage::RepeatRateUp));
+    y += 34.0;
+
+    // Repeat Delay
+    pc.text(&format!("Repeat Delay: {}ms", state.repeat_delay), cx + 14.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+    pc.button("-10", cx + 14.0, y, 36.0, 28.0, BTN_INACTIVE, BTN_HOVER, WHITE,
+        AppAction::Input(InputMessage::RepeatDelayDown));
+    pc.text(&format!(" {}ms ", state.repeat_delay), cx + 58.0, y + 7.0, 13.0, TEXT_FG);
+    pc.button("+10", cx + 14.0 + 36.0 + 8.0, y, 36.0, 28.0, BTN_ACTIVE, BTN_HOVER, WHITE,
+        AppAction::Input(InputMessage::RepeatDelayUp));
+    y += 40.0;
+
+    // ── Keybindings ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Keyboard Bindings", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    for kb in &state.keybinds {
+        let binding = if kb.mods.is_empty() {
+            kb.key.clone()
+        } else {
+            format!("{}+{}", kb.mods, kb.key)
+        };
+        let action_label = if kb.command.is_empty() {
+            kb.action.clone()
+        } else {
+            format!("{}: {}", kb.action, kb.command)
+        };
+        pc.text(&binding, cx + 14.0, y, 12.0, TEXT_FG);
+        let label_w = cw - 200.0;
+        pc.text(&action_label, cx + 14.0 + label_w.min(180.0), y, 12.0, TEXT_DIM);
+        y += 18.0;
+    }
+
+    pc
+}
+
+pub fn update(state: &mut InputState, msg: InputMessage) {
+    match msg {
+        InputMessage::ToggleTapToClick => {
+            state.tap_to_click = !state.tap_to_click;
+            write_tap_to_click(state.tap_to_click);
+        }
+        InputMessage::RepeatRateDown => {
+            if state.repeat_rate > 1 { state.repeat_rate -= 1; }
+            apply_repeat_config(state.repeat_rate, state.repeat_delay);
+        }
+        InputMessage::RepeatRateUp => {
+            if state.repeat_rate < 100 { state.repeat_rate += 1; }
+            apply_repeat_config(state.repeat_rate, state.repeat_delay);
+        }
+        InputMessage::RepeatDelayDown => {
+            if state.repeat_delay > 100 { state.repeat_delay -= 10; }
+            apply_repeat_config(state.repeat_rate, state.repeat_delay);
+        }
+        InputMessage::RepeatDelayUp => {
+            if state.repeat_delay < 2000 { state.repeat_delay += 10; }
+            apply_repeat_config(state.repeat_rate, state.repeat_delay);
+        }
+        InputMessage::Refreshed(new) => { *state = new; }
+    }
+}
diff --git a/src/pages/keybindings.rs b/src/pages/keybindings.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/pages/keybindings.rs
@@ -0,0 +1 @@
+
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
new file mode 100644
index 0000000..560e6a5
--- /dev/null
+++ b/src/pages/layout.rs
@@ -0,0 +1,312 @@
+use std::fs;
+use std::io::Write;
+
+use crate::app::{AppAction, PageContent};
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
+const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
+
+#[derive(Debug, Clone)]
+pub struct LayoutState {
+    pub background_color: [u8; 3],
+    pub border_color: [u8; 3],
+    pub fullscreen_border_width: u16,
+    pub cascade_border_width: u16,
+    pub grid_border_width: u16,
+    pub vsplit_border_width: u16,
+    pub hsplit_border_width: u16,
+    pub floating_border_width: u16,
+    pub color_options: Vec<(&'static str, [u8; 3])>,
+}
+
+impl Default for LayoutState {
+    fn default() -> Self {
+        Self {
+            background_color: [0x0a, 0x1a, 0x0e],
+            border_color: [0x3e, 0x3e, 0x3e],
+            fullscreen_border_width: 0,
+            cascade_border_width: 6,
+            grid_border_width: 6,
+            vsplit_border_width: 6,
+            hsplit_border_width: 6,
+            floating_border_width: 6,
+            color_options: preset_colors(),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum LayoutMessage {
+    SetBackground([u8; 3]),
+    SetBorderColor([u8; 3]),
+    FullscreenDown, FullscreenUp,
+    CascadeDown, CascadeUp,
+    GridDown, GridUp,
+    VsplitDown, VsplitUp,
+    HsplitDown, HsplitUp,
+    FloatingDown, FloatingUp,
+    Refreshed(LayoutState),
+}
+
+fn preset_colors() -> Vec<(&'static str, [u8; 3])> {
+    vec![
+        ("Black",       [0x00, 0x00, 0x00]),
+        ("Dark Gray",   [0x1a, 0x1a, 0x2e]),
+        ("Slate",       [0x2d, 0x2d, 0x3d]),
+        ("Dark Forest", [0x0a, 0x1a, 0x0e]),
+        ("Forest",      [0x1a, 0x2a, 0x1c]),
+        ("Dark Teal",   [0x0a, 0x1a, 0x1e]),
+        ("Navy",        [0x0a, 0x0f, 0x2e]),
+        ("Dark Wine",   [0x1e, 0x0a, 0x14]),
+        ("Dark Brown",  [0x1e, 0x16, 0x0e]),
+        ("Charcoal",    [0x22, 0x22, 0x22]),
+        ("Midnight",    [0x10, 0x10, 0x20]),
+        ("Deep Sea",    [0x06, 0x14, 0x1e]),
+    ]
+}
+
+pub fn read_layout_config() -> LayoutState {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    LayoutState {
+        background_color: parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]),
+        border_color: parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]),
+        fullscreen_border_width: parse_u16_from(&content, "fullscreen_border_width", 0),
+        cascade_border_width: parse_u16_from(&content, "cascade_border_width", 6),
+        grid_border_width: parse_u16_from(&content, "grid_border_width", 6),
+        vsplit_border_width: parse_u16_from(&content, "vsplit_border_width", 6),
+        hsplit_border_width: parse_u16_from(&content, "hsplit_border_width", 6),
+        floating_border_width: parse_u16_from(&content, "floating_border_width", 6),
+        color_options: preset_colors(),
+    }
+}
+
+fn parse_color_from_key(content: &str, key: &str, default: [u8; 3]) -> [u8; 3] {
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if let Some(rest) = trimmed.strip_prefix(key) {
+            let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+            let hex = rest.trim_end_matches('"').trim();
+            return parse_hex(hex);
+        }
+    }
+    default
+}
+
+fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if let Some(rest) = trimmed.strip_prefix(key) {
+            let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+            return rest.trim_end_matches('"').trim().parse::<u16>().unwrap_or(default);
+        }
+    }
+    default
+}
+
+fn parse_hex(s: &str) -> [u8; 3] {
+    let s = s.trim_start_matches('#');
+    if s.len() >= 6 {
+        let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0x0a);
+        let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0x1a);
+        let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0x0e);
+        [r, g, b]
+    } else { [0x0a, 0x1a, 0x0e] }
+}
+
+fn write_config_value(key: &str, value: &str) -> bool {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let new_line = format!("{} = {}", key, value);
+    let mut found = false;
+    let updated: String = content.lines()
+        .map(|line| {
+            if line.trim().starts_with(key) { found = true; new_line.clone() }
+            else { line.to_string() }
+        }).collect::<Vec<_>>().join("\n");
+    if !found {
+        let mut result = String::new();
+        let mut in_layout = false;
+        let mut inserted = false;
+        for line in updated.lines() {
+            if line.trim() == "[layout]" { in_layout = true; }
+            else if line.trim().starts_with('[') && in_layout {
+                if !inserted { result.push_str(&new_line); result.push('\n'); inserted = true; }
+                in_layout = false;
+            }
+            result.push_str(line); result.push('\n');
+        }
+        if in_layout && !inserted { result.push_str(&new_line); result.push('\n'); }
+        fs::write(CONFIG_PATH, result).is_ok()
+    } else { fs::write(CONFIG_PATH, updated).is_ok() }
+}
+
+fn send_ipc_command(cmd: &str) {
+    if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(CLEARWM_SOCK) {
+        let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
+    }
+}
+
+fn apply_background(rgb: [u8; 3]) {
+    let _ = std::process::Command::new("pkill").args(["-x", "swaybg"]).status();
+    std::thread::sleep(std::time::Duration::from_millis(100));
+    let hex = format!("{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]);
+    let _ = std::process::Command::new("swaybg").arg("-c").arg(&hex).spawn();
+    write_config_value("background_color", &format!("\"#{}\"", hex));
+    send_ipc_command(&format!("layout background_color #{}", hex));
+}
+
+fn apply_border_color(rgb: [u8; 3]) {
+    let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+    write_config_value("border_color", &hex);
+    send_ipc_command(&format!("layout border_color #{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
+}
+
+fn apply_all_widths(s: &LayoutState) {
+    let w = |k: &str, v: u16| { write_config_value(k, &v.to_string()); send_ipc_command(&format!("layout {} {}", k, v)); };
+    w("fullscreen_border_width", s.fullscreen_border_width);
+    w("cascade_border_width", s.cascade_border_width);
+    w("grid_border_width", s.grid_border_width);
+    w("vsplit_border_width", s.vsplit_border_width);
+    w("hsplit_border_width", s.hsplit_border_width);
+    w("floating_border_width", s.floating_border_width);
+}
+
+fn rgb_float(c: [u8; 3]) -> [f32; 4] {
+    [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0, 1.0]
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── Background Color ──
+    pc.text("Desktop Background", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    let hex = format!("#{:02x}{:02x}{:02x}", state.background_color[0], state.background_color[1], state.background_color[2]);
+    pc.text(&format!("Current: {}", hex), cx + 14.0, y, 12.0, ACCENT);
+    y += 18.0;
+
+    // Color swatch grid
+    let swatch_w = 48.0;
+    let swatch_h = 24.0;
+    let gap = 6.0;
+    let cols = 4usize;
+    let total_w = cols as f32 * (swatch_w + gap);
+    let start_x = cx + (cw - total_w) / 2.0;
+
+    for (i, (name, rgb)) in state.color_options.iter().enumerate() {
+        let col = i % cols;
+        let row = i / cols;
+        let sx = start_x + col as f32 * (swatch_w + gap);
+        let sy = y + row as f32 * (swatch_h + 16.0);
+        let is_selected = *rgb == state.background_color;
+        let border = if is_selected { ACCENT } else { [0.24, 0.24, 0.24, 1.0] };
+
+        pc.rect(rgb_float(*rgb), sx, sy, swatch_w, swatch_h);
+        if is_selected {
+            pc.rect(border, sx - 1.0, sy - 1.0, swatch_w + 2.0, swatch_h + 2.0);
+        }
+        pc.text(name, sx + swatch_w / 2.0 - name.len() as f32 * 2.5, sy + swatch_h + 2.0, 8.0, TEXT_DIM);
+        // Clickable over the swatch
+        let action = AppAction::Layout(LayoutMessage::SetBackground(*rgb));
+        pc.button("", sx, sy, swatch_w, swatch_h,
+            [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], action);
+    }
+    let swatch_rows = (state.color_options.len() + cols - 1) / cols;
+    y += swatch_rows as f32 * (swatch_h + 16.0) + 8.0;
+
+    // ── Border Color ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Border Color", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    for (i, (name, rgb)) in state.color_options.iter().enumerate() {
+        let col = i % cols;
+        let row = i / cols;
+        let sx = start_x + col as f32 * (swatch_w + gap);
+        let sy = y + row as f32 * (swatch_h + 16.0);
+        let is_selected = *rgb == state.border_color;
+        let border = if is_selected { ACCENT } else { [0.24, 0.24, 0.24, 1.0] };
+
+        pc.rect(rgb_float(*rgb), sx, sy, swatch_w, swatch_h);
+        if is_selected {
+            pc.rect(border, sx - 1.0, sy - 1.0, swatch_w + 2.0, swatch_h + 2.0);
+        }
+        pc.text(name, sx + swatch_w / 2.0 - name.len() as f32 * 2.5, sy + swatch_h + 2.0, 8.0, TEXT_DIM);
+        let action = AppAction::Layout(LayoutMessage::SetBorderColor(*rgb));
+        pc.button("", sx, sy, swatch_w, swatch_h,
+            [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], action);
+    }
+    y += swatch_rows as f32 * (swatch_h + 16.0) + 8.0;
+
+    // ── Border Widths ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Border Width", cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    let widths: [(&str, u16, AppAction, AppAction); 6] = [
+        ("Fullscreen", state.fullscreen_border_width,
+         AppAction::Layout(LayoutMessage::FullscreenDown), AppAction::Layout(LayoutMessage::FullscreenUp)),
+        ("Cascade", state.cascade_border_width,
+         AppAction::Layout(LayoutMessage::CascadeDown), AppAction::Layout(LayoutMessage::CascadeUp)),
+        ("Grid", state.grid_border_width,
+         AppAction::Layout(LayoutMessage::GridDown), AppAction::Layout(LayoutMessage::GridUp)),
+        ("Vsplit", state.vsplit_border_width,
+         AppAction::Layout(LayoutMessage::VsplitDown), AppAction::Layout(LayoutMessage::VsplitUp)),
+        ("Hsplit", state.hsplit_border_width,
+         AppAction::Layout(LayoutMessage::HsplitDown), AppAction::Layout(LayoutMessage::HsplitUp)),
+        ("Floating", state.floating_border_width,
+         AppAction::Layout(LayoutMessage::FloatingDown), AppAction::Layout(LayoutMessage::FloatingUp)),
+    ];
+
+    for (name, val, down, up) in &widths {
+        pc.text(name, cx + 14.0, y + 6.0, 12.0, TEXT_DIM);
+        pc.button("-1", cx + cw - 100.0, y, 30.0, 26.0,
+            BTN_INACTIVE, BTN_HOVER, WHITE, down.clone());
+        pc.text(&format!(" {}px ", val), cx + cw - 66.0, y + 6.0, 13.0, TEXT_FG);
+        pc.button("+1", cx + cw - 38.0, y, 30.0, 26.0,
+            BTN_ACTIVE, BTN_HOVER, WHITE, up.clone());
+        y += 30.0;
+    }
+
+    pc
+}
+
+pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
+    let dec = |v: &mut u16| { if *v > 0 { *v -= 1; } };
+    let inc = |v: &mut u16| { if *v < 100 { *v += 1; } };
+    match msg {
+        LayoutMessage::SetBackground(rgb) => {
+            state.background_color = rgb;
+            apply_background(rgb);
+        }
+        LayoutMessage::SetBorderColor(rgb) => {
+            state.border_color = rgb;
+            apply_border_color(rgb);
+        }
+        LayoutMessage::FullscreenDown => { dec(&mut state.fullscreen_border_width); apply_all_widths(state); }
+        LayoutMessage::FullscreenUp => { inc(&mut state.fullscreen_border_width); apply_all_widths(state); }
+        LayoutMessage::CascadeDown => { dec(&mut state.cascade_border_width); apply_all_widths(state); }
+        LayoutMessage::CascadeUp => { inc(&mut state.cascade_border_width); apply_all_widths(state); }
+        LayoutMessage::GridDown => { dec(&mut state.grid_border_width); apply_all_widths(state); }
+        LayoutMessage::GridUp => { inc(&mut state.grid_border_width); apply_all_widths(state); }
+        LayoutMessage::VsplitDown => { dec(&mut state.vsplit_border_width); apply_all_widths(state); }
+        LayoutMessage::VsplitUp => { inc(&mut state.vsplit_border_width); apply_all_widths(state); }
+        LayoutMessage::HsplitDown => { dec(&mut state.hsplit_border_width); apply_all_widths(state); }
+        LayoutMessage::HsplitUp => { inc(&mut state.hsplit_border_width); apply_all_widths(state); }
+        LayoutMessage::FloatingDown => { dec(&mut state.floating_border_width); apply_all_widths(state); }
+        LayoutMessage::FloatingUp => { inc(&mut state.floating_border_width); apply_all_widths(state); }
+        LayoutMessage::Refreshed(new) => { *state = new; }
+    }
+}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
new file mode 100644
index 0000000..74d0cfd
--- /dev/null
+++ b/src/pages/mod.rs
@@ -0,0 +1,55 @@
+pub mod power;
+pub mod audio;
+pub mod network;
+pub mod display;
+pub mod layout;
+pub mod storage;
+pub mod system_info;
+pub mod keybindings;
+pub mod input;
+pub mod status;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Page {
+    Power,
+    Audio,
+    Radios,
+    Storage,
+    Display,
+    Layout,
+    System,
+    Input,
+    Status,
+}
+
+impl Page {
+    pub const ALL: [Page; 9] = [
+        Page::Audio,
+        Page::Display,
+        Page::Input,
+        Page::Layout,
+        Page::Power,
+        Page::Radios,
+        Page::Status,
+        Page::Storage,
+        Page::System,
+    ];
+
+    pub fn label(self) -> &'static str {
+        match self {
+            Page::Power => "Power",
+            Page::Audio => "Audio",
+            Page::Radios => "Radios",
+            Page::Storage => "Storage",
+            Page::Display => "Display",
+            Page::Layout => "Layout",
+            Page::System => "System",
+            Page::Input => "Input",
+            Page::Status => "Status",
+        }
+    }
+
+    pub fn icon(self) -> &'static str {
+        ""
+    }
+}
diff --git a/src/pages/network.rs b/src/pages/network.rs
new file mode 100644
index 0000000..54df5e3
--- /dev/null
+++ b/src/pages/network.rs
@@ -0,0 +1,300 @@
+use crate::app::{AppAction, PageContent};
+
+#[derive(Debug, Clone)]
+pub struct WifiNetwork {
+    pub ssid: String,
+    pub signal: u8,
+    pub secured: bool,
+    pub in_use: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct BluetoothDevice {
+    pub mac: String,
+    pub name: String,
+    pub icon: String,
+    pub connected: bool,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct NetworkState {
+    pub wifi_enabled: bool,
+    pub connected_ssid: String,
+    pub signal_strength: u8,
+    pub ip_address: String,
+    pub device: String,
+    pub available: Vec<WifiNetwork>,
+    pub bt_enabled: bool,
+    pub bt_devices: Vec<BluetoothDevice>,
+    pub bt_scanning: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum NetworkMessage {
+    Refreshed(NetworkState),
+    ToggleWifi,
+    ConnectWifi(String),
+    ToggleBluetooth,
+    BtConnect(String),
+    BtDisconnect(String),
+    BtScan,
+}
+
+pub async fn fetch_network_state() -> NetworkState {
+    let wifi_enabled = tokio::process::Command::new("nmcli")
+        .args(["radio", "wifi"]).output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).trim().starts_with("enabled"))
+        .unwrap_or(false);
+
+    let active = tokio::process::Command::new("nmcli")
+        .args(["-t", "-f", "NAME,DEVICE,SIGNAL", "con", "show", "--active"])
+        .output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+        .unwrap_or_default();
+
+    let (connected_ssid, device) = active.lines()
+        .filter(|l| !l.contains(":lo:"))
+        .filter_map(|l| {
+            let parts: Vec<&str> = l.splitn(3, ':').collect();
+            if parts.len() >= 2 && !parts[0].is_empty() {
+                Some((parts[0].to_string(), parts[1].to_string()))
+            } else { None }
+        }).next().unwrap_or_default();
+
+    let signal = tokio::process::Command::new("nmcli")
+        .args(["-t", "-f", "ACTIVE,SIGNAL", "dev", "wifi", "list"])
+        .output().await.ok()
+        .and_then(|o| {
+            String::from_utf8_lossy(&o.stdout).lines()
+                .find(|l| l.starts_with("yes:"))
+                .and_then(|l| l.split(':').nth(1))
+                .and_then(|v| v.parse::<u8>().ok())
+        }).unwrap_or(0);
+
+    let ip_address = tokio::process::Command::new("nmcli")
+        .args(["-t", "-f", "IP4.ADDRESS", "dev", "show", &device])
+        .output().await.ok()
+        .and_then(|o| {
+            String::from_utf8_lossy(&o.stdout).lines()
+                .find(|l| !l.is_empty())
+                .map(|l| l.split('/').next().unwrap_or(l).to_string())
+        }).unwrap_or_default();
+
+    let available = if wifi_enabled { fetch_wifi_list().await } else { Vec::new() };
+    let (bt_enabled, bt_devices) = fetch_bluetooth_state().await;
+
+    NetworkState {
+        wifi_enabled, connected_ssid, signal_strength: signal,
+        ip_address, device, available,
+        bt_enabled, bt_devices, bt_scanning: false,
+    }
+}
+
+async fn fetch_wifi_list() -> Vec<WifiNetwork> {
+    let out = match tokio::process::Command::new("nmcli")
+        .args(["-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi", "list"])
+        .output().await
+    {
+        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
+        Err(_) => return Vec::new(),
+    };
+
+    let mut networks = Vec::new();
+    let mut seen = std::collections::HashSet::new();
+    for line in out.lines() {
+        let parts: Vec<&str> = line.splitn(4, ':').collect();
+        if parts.len() >= 3 {
+            let ssid = parts[0].to_string();
+            if ssid.is_empty() || ssid == "--" || seen.contains(&ssid) { continue; }
+            seen.insert(ssid.clone());
+            networks.push(WifiNetwork {
+                ssid, signal: parts[1].parse::<u8>().unwrap_or(0),
+                secured: !parts[2].is_empty(),
+                in_use: parts.len() > 3 && parts[3] == "*",
+            });
+        }
+    }
+    networks.sort_by(|a, b| b.signal.cmp(&a.signal));
+    networks
+}
+
+async fn fetch_bluetooth_state() -> (bool, Vec<BluetoothDevice>) {
+    let bt_enabled = tokio::process::Command::new("bluetoothctl")
+        .args(["show"]).output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).lines().any(|l| l.contains("Powered: yes")))
+        .unwrap_or(false);
+
+    let devices = if bt_enabled { fetch_bt_devices().await } else { Vec::new() };
+    (bt_enabled, devices)
+}
+
+async fn fetch_bt_devices() -> Vec<BluetoothDevice> {
+    let out = match tokio::process::Command::new("bluetoothctl")
+        .args(["devices"]).output().await
+    {
+        Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
+        Err(_) => return Vec::new(),
+    };
+
+    let mut devices = Vec::new();
+    for line in out.lines() {
+        let rest = line.strip_prefix("Device ").unwrap_or("");
+        let parts: Vec<&str> = rest.splitn(2, ' ').collect();
+        if parts.len() < 2 || parts[0].is_empty() || parts[1].is_empty() { continue; }
+        let mac = parts[0].to_string();
+        let name = parts[1].to_string();
+
+        let info = tokio::process::Command::new("bluetoothctl")
+            .args(["info", &mac]).output().await.ok()
+            .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+            .unwrap_or_default();
+
+        let connected = info.lines().any(|l| l.contains("Connected: yes"));
+        let icon = info.lines()
+            .find(|l| l.contains("Icon:"))
+            .and_then(|l| l.split(':').nth(1).map(|s| s.trim().to_string()))
+            .unwrap_or_else(|| "audio-card".into());
+
+        devices.push(BluetoothDevice { mac, name, icon, connected });
+    }
+    devices
+}
+
+fn wifi_connect(ssid: &str) {
+    let _ = tokio::process::Command::new("nmcli")
+        .args(["dev", "wifi", "connect", ssid]).spawn();
+}
+
+fn wifi_toggle(enable: bool) {
+    let _ = tokio::process::Command::new("nmcli")
+        .args(["radio", "wifi", if enable { "on" } else { "off" }]).spawn();
+}
+
+fn bt_toggle(enable: bool) {
+    let _ = tokio::process::Command::new("bluetoothctl")
+        .args([if enable { "power" } else { "power" }, if enable { "on" } else { "off" }]).spawn();
+}
+
+fn bt_connect(mac: &str) {
+    let _ = tokio::process::Command::new("bluetoothctl")
+        .args(["connect", mac]).spawn();
+}
+
+fn bt_disconnect(mac: &str) {
+    let _ = tokio::process::Command::new("bluetoothctl")
+        .args(["disconnect", mac]).spawn();
+}
+
+fn bt_scan() {
+    let _ = tokio::process::Command::new("bluetoothctl")
+        .args(["scan", "on"]).spawn();
+    let _ = tokio::process::Command::new("sh")
+        .args(["-c", "sleep 5 && bluetoothctl scan off"]).spawn();
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const TOGGLE_ON: [f32; 4] = [0.16, 0.41, 0.18, 1.0];
+const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const NET_BTN: [f32; 4] = [0.13, 0.20, 0.27, 1.0];
+const ACT_BTN: [f32; 4] = [0.16, 0.29, 0.18, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── WiFi ──
+    pc.text("WiFi", cx + 12.0, y, 14.0, TEXT_FG);
+    pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
+        cx + cw - 80.0, y, 60.0, 28.0,
+        if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+        AppAction::Radios(NetworkMessage::ToggleWifi));
+    y += 34.0;
+
+    if !state.connected_ssid.is_empty() {
+        pc.text(&format!("Connected: {}", state.connected_ssid), cx + 14.0, y, 13.0, ACCENT);
+        y += 18.0;
+        pc.text(&format!("Signal: {}%  IP: {}", state.signal_strength, state.ip_address),
+            cx + 14.0, y, 12.0, TEXT_DIM);
+        y += 16.0;
+    } else if state.wifi_enabled {
+        pc.text("Not connected", cx + 14.0, y, 12.0, TEXT_DIM);
+        y += 16.0;
+    }
+
+    if state.wifi_enabled && !state.available.is_empty() {
+        for net in &state.available {
+            let prefix = if net.in_use { ">" } else { " " };
+            let label = format!("{}  {}  ({}%)", prefix, net.ssid, net.signal);
+            let active = net.in_use;
+            pc.button(&label, cx + 14.0, y, cw - 28.0, 26.0,
+                if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
+                if active { ACCENT } else { TEXT_FG },
+                AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
+            y += 30.0;
+        }
+    }
+
+    // ── Bluetooth ──
+    y += 4.0;
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("Bluetooth", cx + 12.0, y, 14.0, TEXT_FG);
+
+    // BT toggle
+    pc.button(if state.bt_enabled { "ON" } else { "OFF" },
+        cx + cw - 140.0, y, 60.0, 28.0,
+        if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+        AppAction::Radios(NetworkMessage::ToggleBluetooth));
+
+    // Scan button
+    pc.button("Scan", cx + cw - 72.0, y, 52.0, 28.0,
+        TOGGLE_OFF, BTN_HOVER, WHITE,
+        AppAction::Radios(NetworkMessage::BtScan));
+    y += 34.0;
+
+    if state.bt_devices.is_empty() {
+        if state.bt_enabled {
+            pc.text("No paired devices found", cx + 14.0, y, 12.0, TEXT_DIM);
+        }
+    } else {
+        for dev in &state.bt_devices {
+            let status = if dev.connected { ">" } else { " " };
+            let label = format!("{} {} ({})", status, dev.name, dev.mac);
+            let action_label = if dev.connected { "Disconnect" } else { "Connect" };
+            pc.text(&label, cx + 14.0, y, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
+            pc.button(action_label, cx + cw - 90.0, y - 2.0, 70.0, 22.0,
+                if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
+                if dev.connected {
+                    AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
+                } else {
+                    AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
+                });
+            y += 24.0;
+        }
+    }
+
+    pc
+}
+
+pub fn update(state: &mut NetworkState, msg: NetworkMessage) {
+    match msg {
+        NetworkMessage::Refreshed(new) => { *state = new; }
+        NetworkMessage::ToggleWifi => {
+            state.wifi_enabled = !state.wifi_enabled;
+            wifi_toggle(state.wifi_enabled);
+        }
+        NetworkMessage::ConnectWifi(ssid) => { wifi_connect(&ssid); }
+        NetworkMessage::ToggleBluetooth => {
+            state.bt_enabled = !state.bt_enabled;
+            bt_toggle(state.bt_enabled);
+        }
+        NetworkMessage::BtConnect(mac) => { bt_connect(&mac); }
+        NetworkMessage::BtDisconnect(mac) => { bt_disconnect(&mac); }
+        NetworkMessage::BtScan => { bt_scan(); }
+    }
+}
diff --git a/src/pages/power.rs b/src/pages/power.rs
new file mode 100644
index 0000000..bf16277
--- /dev/null
+++ b/src/pages/power.rs
@@ -0,0 +1,321 @@
+use crate::app::{AppAction, PageContent};
+
+#[derive(Debug, Clone, Default)]
+pub struct BatteryInfo {
+    pub percentage: f32,
+    pub state: String,
+    pub energy: f64,
+    pub energy_full: f64,
+    pub energy_rate: f64,
+    pub time_to_empty: i64,
+    pub time_to_full: i64,
+    pub vendor: String,
+    pub model: String,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct PowerState {
+    pub battery: BatteryInfo,
+    pub on_ac: bool,
+    pub cpu_powersave: bool,
+    pub gpu_powersave: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum PowerMessage {
+    Refreshed(PowerState),
+    SetCpuPerformance,
+    SetCpuPowersave,
+    SetGpuDefault,
+    SetGpuPowersave,
+    Suspend,
+    Hibernate,
+    Reboot,
+    PowerOff,
+    Tick,
+}
+
+// ── zbus proxies ────────────────────────────────────────────────────
+
+#[zbus::proxy(
+    interface = "org.freedesktop.UPower.Device",
+    default_service = "org.freedesktop.UPower",
+    default_path = "/org/freedesktop/UPower/devices/battery_BAT0"
+)]
+trait UpowerBattery {
+    #[zbus(property)]
+    fn percentage(&self) -> zbus::Result<f64>;
+    #[zbus(property)]
+    fn state(&self) -> zbus::Result<u32>;
+    #[zbus(property)]
+    fn energy(&self) -> zbus::Result<f64>;
+    #[zbus(property)]
+    fn energy_full(&self) -> zbus::Result<f64>;
+    #[zbus(property)]
+    fn energy_rate(&self) -> zbus::Result<f64>;
+    #[zbus(property)]
+    fn time_to_empty(&self) -> zbus::Result<i64>;
+    #[zbus(property)]
+    fn time_to_full(&self) -> zbus::Result<i64>;
+    #[zbus(property)]
+    fn vendor(&self) -> zbus::Result<String>;
+    #[zbus(property)]
+    fn model(&self) -> zbus::Result<String>;
+}
+
+#[zbus::proxy(
+    interface = "org.freedesktop.UPower",
+    default_service = "org.freedesktop.UPower",
+    default_path = "/org/freedesktop/UPower"
+)]
+trait UpowerDaemon {
+    #[zbus(property, name = "OnBattery")]
+    fn on_battery(&self) -> zbus::Result<bool>;
+}
+
+// ── Helpers ─────────────────────────────────────────────────────────
+
+fn format_duration(secs: i64) -> String {
+    let h = secs / 3600;
+    let m = (secs % 3600) / 60;
+    if h > 0 { format!("{}h {}m", h, m) } else { format!("{}m", m) }
+}
+
+fn spawn_systemctl(action: &str) {
+    let _ = tokio::process::Command::new("systemctl").arg(action).spawn();
+}
+
+fn spawn_cpu_power(powersave: bool) {
+    let script = if powersave { "cpu-powersave-on" } else { "cpu-powersave-off" };
+    let _ = tokio::process::Command::new("pkexec")
+        .arg(format!("/home/lsgalante/.local/share/clear-system-interface/helpers/{}", script))
+        .spawn();
+}
+
+fn spawn_gpu_power(powersave: bool) {
+    let script = if powersave { "gpu-powersave-on" } else { "gpu-powersave-off" };
+    let _ = tokio::process::Command::new("pkexec")
+        .arg(format!("/home/lsgalante/.local/share/clear-system-interface/helpers/{}", script))
+        .spawn();
+}
+
+fn current_cpu_governor() -> String {
+    std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
+        .unwrap_or_default().trim().to_string()
+}
+
+async fn current_gpu_power_cap() -> bool {
+    tokio::process::Command::new("nvidia-smi")
+        .args(["--query-gpu=power.limit", "--format=csv,noheader,nounits"])
+        .output().await.ok()
+        .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<f32>().ok())
+        .map(|w| w <= 10.0).unwrap_or(false)
+}
+
+pub async fn fetch_power_state() -> PowerState {
+    let (battery, on_ac) = fetch_upower().await;
+    let cpu_powersave = current_cpu_governor() == "powersave";
+    let gpu_powersave = current_gpu_power_cap().await;
+    PowerState { battery, on_ac, cpu_powersave, gpu_powersave }
+}
+
+async fn fetch_upower() -> (BatteryInfo, bool) {
+    let conn = match zbus::Connection::system().await {
+        Ok(c) => c,
+        Err(_) => return (BatteryInfo::default(), true),
+    };
+
+    let battery = match UpowerBatteryProxy::new(&conn).await {
+        Ok(proxy) => BatteryInfo {
+            percentage: proxy.percentage().await.unwrap_or(0.0) as f32,
+            state: {
+                let s = proxy.state().await.unwrap_or(0);
+                match s { 1 => "charging", 2 => "discharging", 4 => "fully-charged", _ => "unknown" }.into()
+            },
+            energy: proxy.energy().await.unwrap_or(0.0),
+            energy_full: proxy.energy_full().await.unwrap_or(0.0),
+            energy_rate: proxy.energy_rate().await.unwrap_or(0.0),
+            time_to_empty: proxy.time_to_empty().await.unwrap_or(0),
+            time_to_full: proxy.time_to_full().await.unwrap_or(0),
+            vendor: proxy.vendor().await.unwrap_or_default(),
+            model: proxy.model().await.unwrap_or_default(),
+        },
+        Err(_) => BatteryInfo::default(),
+    };
+
+    let on_ac = match UpowerDaemonProxy::new(&conn).await {
+        Ok(proxy) => !proxy.on_battery().await.unwrap_or(false),
+        Err(_) => true,
+    };
+
+    (battery, on_ac)
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const DANGER_BG: [f32; 4] = [0.67, 0.20, 0.20, 1.0];
+const SAFE_BG: [f32; 4] = [0.20, 0.33, 0.22, 1.0];
+const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
+const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
+
+pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // ── Battery section ──
+    let bat = &state.battery;
+    let bat_icon = match bat.state.as_str() {
+        "charging" => "+",
+        "fully-charged" => "=",
+        _ => "",
+    };
+
+    let pct_color = if bat.percentage < 20.0 { RED }
+        else if bat.percentage < 50.0 { ORANGE }
+        else { ACCENT };
+
+    let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
+    pc.text(&pct_str, cx + 12.0, y, 24.0, pct_color);
+    y += 30.0;
+
+    let state_str = format!("{}  •  {:.1}W  •  {:.1}/{:.1} Wh",
+        bat.state, bat.energy_rate, bat.energy, bat.energy_full);
+    pc.text(&state_str, cx + 12.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+
+    let time_str = if bat.time_to_empty > 0 {
+        format!("Time remaining: {}", format_duration(bat.time_to_empty))
+    } else if bat.time_to_full > 0 {
+        format!("Time to full: {}", format_duration(bat.time_to_full))
+    } else { String::new() };
+    if !time_str.is_empty() {
+        pc.text(&time_str, cx + 12.0, y, 12.0, TEXT_DIM);
+        y += 18.0;
+    }
+
+    let detail_str = format!("{}  {}", bat.vendor, bat.model);
+    pc.text(&detail_str, cx + 12.0, y, 11.0, TEXT_DIM);
+    y += 20.0;
+
+    let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
+    pc.text(ac_str, cx + 12.0, y, 14.0, TEXT_FG);
+    y += 24.0;
+
+    // ── CPU Governor section ──
+    y += 8.0;
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("CPU Governor", cx + 12.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+
+    let btn_w = (cw - 40.0) / 2.0;
+    let btn_h = 44.0;
+
+    let perf_active = !state.cpu_powersave;
+    let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
+        (BTN_ACTIVE, "Governor set to performance", ACCENT)
+    } else {
+        (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
+    };
+
+    pc.button("Performance", cx + 12.0, y, btn_w, btn_h,
+        perf_bg, BTN_HOVER, WHITE,
+        AppAction::Power(PowerMessage::SetCpuPerformance));
+    pc.text(perf_desc, cx + 16.0, y + 26.0, 10.0, perf_desc_color);
+
+    let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
+        (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
+    } else {
+        (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
+    };
+
+    let save_x = cx + 16.0 + btn_w;
+    pc.button("Powersave", save_x, y, btn_w, btn_h,
+        save_bg, BTN_HOVER, WHITE,
+        AppAction::Power(PowerMessage::SetCpuPowersave));
+    pc.text(save_desc, save_x + 4.0, y + 26.0, 10.0, save_desc_color);
+    y += btn_h + 12.0;
+
+    // ── GPU Power section ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("GPU Power", cx + 12.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+
+    let gpu_def_active = !state.gpu_powersave;
+    let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
+        (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
+    } else {
+        (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
+    };
+
+    pc.button("80W Default", cx + 12.0, y, btn_w, btn_h,
+        gpu_def_bg, BTN_HOVER, WHITE,
+        AppAction::Power(PowerMessage::SetGpuDefault));
+    pc.text(gpu_def_desc, cx + 16.0, y + 26.0, 10.0, gpu_def_desc_c);
+
+    let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
+        (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
+    } else {
+        (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
+    };
+
+    pc.button("5W Cap", save_x, y, btn_w, btn_h,
+        gpu_cap_bg, BTN_HOVER, WHITE,
+        AppAction::Power(PowerMessage::SetGpuPowersave));
+    pc.text(gpu_cap_desc, save_x + 4.0, y + 26.0, 10.0, gpu_cap_desc_c);
+    y += btn_h + 12.0;
+
+    // ── System Actions section ──
+    pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
+    y += 8.0;
+    pc.text("System Actions", cx + 12.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+
+    let act_btn_w = (cw - 48.0) / 4.0;
+    let act_btn_h = 32.0;
+
+    pc.button("Suspend", cx + 12.0, y, act_btn_w, act_btn_h,
+        SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Suspend));
+    pc.button("Hibernate", cx + 16.0 + act_btn_w, y, act_btn_w, act_btn_h,
+        SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Hibernate));
+    pc.button("Reboot", cx + 20.0 + 2.0 * act_btn_w, y, act_btn_w, act_btn_h,
+        DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Reboot));
+    pc.button("Power Off", cx + 24.0 + 3.0 * act_btn_w, y, act_btn_w, act_btn_h,
+        DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::PowerOff));
+
+    pc
+}
+
+pub fn update(state: &mut PowerState, msg: PowerMessage) {
+    match msg {
+        PowerMessage::Refreshed(new) => { *state = new; }
+        PowerMessage::SetCpuPerformance => {
+            state.cpu_powersave = false;
+            spawn_cpu_power(false);
+        }
+        PowerMessage::SetCpuPowersave => {
+            state.cpu_powersave = true;
+            spawn_cpu_power(true);
+        }
+        PowerMessage::SetGpuDefault => {
+            state.gpu_powersave = false;
+            spawn_gpu_power(false);
+        }
+        PowerMessage::SetGpuPowersave => {
+            state.gpu_powersave = true;
+            spawn_gpu_power(true);
+        }
+        PowerMessage::Suspend => spawn_systemctl("suspend"),
+        PowerMessage::Hibernate => spawn_systemctl("hibernate"),
+        PowerMessage::Reboot => spawn_systemctl("reboot"),
+        PowerMessage::PowerOff => spawn_systemctl("poweroff"),
+        PowerMessage::Tick => {} // handled externally
+    }
+}
diff --git a/src/pages/status.rs b/src/pages/status.rs
new file mode 100644
index 0000000..f3d247c
--- /dev/null
+++ b/src/pages/status.rs
@@ -0,0 +1,123 @@
+use crate::app::{AppAction, PageContent};
+
+#[derive(Debug, Clone, Default)]
+pub struct StatusState {
+    pub font_size: u16,
+    pub running: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum StatusMessage {
+    Refreshed(StatusState),
+    FontSizeUp,
+    FontSizeDown,
+    ReloadWaybar,
+}
+
+pub async fn fetch_status_state() -> StatusState {
+    let running = tokio::process::Command::new("pgrep")
+        .args(["-x", "waybar"]).output().await.ok()
+        .map(|o| !o.stdout.is_empty())
+        .unwrap_or(false);
+
+    let font_size = read_waybar_font_size().unwrap_or(13);
+    StatusState { font_size, running }
+}
+
+fn read_waybar_font_size() -> Option<u16> {
+    let css = std::fs::read_to_string(
+        std::path::Path::new(&std::env::var("HOME").unwrap_or_default()).join(".config/waybar/style.css")
+    ).ok()?;
+    for line in css.lines() {
+        if line.contains("font-size") {
+            let val = line.split(':').nth(1)?.trim().trim_end_matches(';').trim();
+            if let Some(px) = val.strip_suffix("px") {
+                return px.trim().parse::<u16>().ok();
+            }
+            return val.parse::<u16>().ok();
+        }
+    }
+    None
+}
+
+fn write_waybar_font_size(size: u16) {
+    let home = std::env::var("HOME").unwrap_or_default();
+    let path = std::path::Path::new(&home).join(".config/waybar/style.css");
+    let css = std::fs::read_to_string(&path).unwrap_or_default();
+    let mut new_lines = Vec::new();
+    for line in css.lines() {
+        if line.contains("font-size") {
+            new_lines.push(format!("    font-size: {}px;", size));
+        } else {
+            new_lines.push(line.to_string());
+        }
+    }
+    let _ = std::fs::write(&path, new_lines.join("\n"));
+}
+
+fn waybar_reload() {
+    let _ = tokio::process::Command::new("pkill")
+        .args(["-x", "waybar", "-SIGUSR2"]).spawn();
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // Waybar status header
+    let status_color = if state.running { ACCENT } else { [0.67, 0.20, 0.20, 1.0] };
+    let status_text = if state.running { "Running" } else { "Stopped" };
+    pc.text("Waybar", cx + 12.0, y, 14.0, TEXT_FG);
+    pc.text(status_text, cx + 80.0, y, 14.0, status_color);
+    y += 22.0;
+
+    // Font size
+    pc.text(&format!("Font size: {}px", state.font_size), cx + 12.0, y, 13.0, TEXT_FG);
+    y += 20.0;
+
+    let btn_h = 28.0;
+    pc.button("-1", cx + 12.0, y, 36.0, btn_h,
+        BTN_INACTIVE, BTN_HOVER, WHITE,
+        AppAction::Status(StatusMessage::FontSizeDown));
+    pc.text(&format!(" {}px ", state.font_size), cx + 56.0, y + 7.0, 13.0, TEXT_FG);
+    pc.button("+1", cx + 12.0 + 36.0 + 8.0, y, 36.0, btn_h,
+        BTN_ACTIVE, BTN_HOVER, WHITE,
+        AppAction::Status(StatusMessage::FontSizeUp));
+    y += btn_h + 12.0;
+
+    // Reload button
+    let btn_w = (cw - 24.0).min(200.0);
+    pc.button("Reload Waybar", cx + cw / 2.0 - btn_w / 2.0, y, btn_w, 32.0,
+        BTN_INACTIVE, BTN_HOVER, WHITE,
+        AppAction::Status(StatusMessage::ReloadWaybar));
+
+    pc
+}
+
+pub fn update(state: &mut StatusState, msg: StatusMessage) {
+    match msg {
+        StatusMessage::Refreshed(new) => { *state = new; }
+        StatusMessage::FontSizeUp => {
+            if state.font_size < 28 {
+                state.font_size += 1;
+                write_waybar_font_size(state.font_size);
+            }
+        }
+        StatusMessage::FontSizeDown => {
+            if state.font_size > 8 {
+                state.font_size -= 1;
+                write_waybar_font_size(state.font_size);
+            }
+        }
+        StatusMessage::ReloadWaybar => {
+            waybar_reload();
+        }
+    }
+}
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
new file mode 100644
index 0000000..74df39f
--- /dev/null
+++ b/src/pages/storage.rs
@@ -0,0 +1,111 @@
+use crate::app::PageContent;
+
+#[derive(Debug, Clone, Default)]
+pub struct StorageState {
+    pub disk_total: f64,
+    pub disk_used: f64,
+    pub ram_total: f64,
+    pub ram_used: f64,
+}
+
+#[derive(Debug, Clone)]
+pub enum StorageMessage {
+    Refreshed(StorageState),
+}
+
+pub async fn fetch_storage_state() -> StorageState {
+    let disk_output = tokio::process::Command::new("df")
+        .args(["-BG", "/"])
+        .output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+        .unwrap_or_default();
+    let (disk_total, disk_used) = parse_disk(&disk_output);
+
+    let mem_output = tokio::process::Command::new("free")
+        .args(["-b"])
+        .output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+        .unwrap_or_default();
+    let (ram_total, ram_used) = parse_mem(&mem_output);
+
+    StorageState { disk_total, disk_used, ram_total, ram_used }
+}
+
+fn parse_disk(info: &str) -> (f64, f64) {
+    for line in info.lines().skip(1) {
+        let parts: Vec<&str> = line.split_whitespace().collect();
+        if parts.len() >= 4 {
+            let total = parts[1].trim_end_matches('G').parse::<f64>().unwrap_or(0.0);
+            let used = parts[2].trim_end_matches('G').parse::<f64>().unwrap_or(0.0);
+            return (total, used);
+        }
+    }
+    (0.0, 0.0)
+}
+
+fn parse_mem(info: &str) -> (f64, f64) {
+    for line in info.lines() {
+        if line.starts_with("Mem:") {
+            let parts: Vec<&str> = line.split_whitespace().collect();
+            if parts.len() >= 3 {
+                let total = parts[1].parse::<f64>().unwrap_or(0.0) / 1_073_741_824.0;
+                let used = parts[2].parse::<f64>().unwrap_or(0.0) / 1_073_741_824.0;
+                return (total, used);
+            }
+        }
+    }
+    (0.0, 0.0)
+}
+
+const LABEL_FG: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+
+pub fn view(state: &StorageState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    pc.text("Local Storage", cx + 12.0, y, 16.0, LABEL_FG);
+    y += 26.0;
+
+    let disk_pct = if state.disk_total > 0.0 {
+        state.disk_used / state.disk_total * 100.0
+    } else {
+        0.0
+    };
+
+    pc.text("Disk", cx + 12.0, y, 12.0, LABEL_FG);
+    pc.text(
+        &format!("{:.0} / {:.0} GiB  ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
+        cx + 100.0, y, 12.0, TEXT_FG,
+    );
+    y += 18.0;
+
+    let bar_w = _cw - 24.0;
+    pc.rect([0.15, 0.15, 0.25, 1.0], cx + 12.0, y, bar_w, 8.0);
+    if disk_pct > 0.0 {
+        pc.rect([0.36, 0.60, 0.36, 1.0], cx + 12.0, y, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
+    }
+    y += 20.0;
+
+    let ram_pct = if state.ram_total > 0.0 {
+        state.ram_used / state.ram_total * 100.0
+    } else {
+        0.0
+    };
+
+    pc.text("RAM", cx + 12.0, y, 12.0, LABEL_FG);
+    pc.text(
+        &format!("{:.1} / {:.1} GiB  ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
+        cx + 100.0, y, 12.0, TEXT_FG,
+    );
+    y += 18.0;
+
+    pc.rect([0.15, 0.15, 0.25, 1.0], cx + 12.0, y, bar_w, 8.0);
+    if ram_pct > 0.0 {
+        pc.rect([0.50, 0.50, 0.65, 1.0], cx + 12.0, y, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+    }
+
+    pc
+}
+
+pub fn update(_state: &mut StorageState, _msg: StorageMessage) {}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
new file mode 100644
index 0000000..e18a4dd
--- /dev/null
+++ b/src/pages/system_info.rs
@@ -0,0 +1,114 @@
+use crate::app::PageContent;
+
+#[derive(Debug, Clone, Default)]
+pub struct SystemState {
+    pub hostname: String,
+    pub kernel: String,
+    pub cpu_model: String,
+    pub cpu_usage: f32,
+    pub cpu_cores: u32,
+    pub uptime: String,
+    pub gpu: String,
+}
+
+#[derive(Debug, Clone)]
+pub enum SystemMessage {
+    Refreshed(SystemState),
+}
+
+pub async fn fetch_system_state() -> SystemState {
+    let hostname = tokio::process::Command::new("hostname")
+        .output().await.ok()
+        .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().split('.').next().map(|s| s.to_string()))
+        .unwrap_or_default();
+
+    let kernel = tokio::process::Command::new("uname")
+        .args(["-r"]).output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
+        .unwrap_or_default();
+
+    let (cpu_model, cpu_cores) = {
+        let lscpu = tokio::process::Command::new("lscpu")
+            .output().await.ok()
+            .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+            .unwrap_or_default();
+        let model = lscpu.lines()
+            .find(|l| l.contains("Model name"))
+            .and_then(|l| l.split(':').nth(1))
+            .map(|s| s.trim().to_string())
+            .unwrap_or_default();
+        let cores = lscpu.lines()
+            .find(|l| l.contains("CPU(s)"))
+            .and_then(|l| {
+                let rest = l.split(':').nth(1).unwrap_or("").trim();
+                rest.split_whitespace().next().and_then(|n| n.parse::<u32>().ok())
+            })
+            .unwrap_or(0);
+        (model, cores)
+    };
+
+    let cpu_usage = {
+        let read_stat = || -> Option<(u64, u64)> {
+            let stat = std::fs::read_to_string("/proc/stat").ok()?;
+            let first = stat.lines().next()?;
+            let vals: Vec<u64> = first.split_whitespace().skip(1).filter_map(|v| v.parse().ok()).collect();
+            if vals.len() < 3 { return None; }
+            let total: u64 = vals.iter().sum();
+            let idle = vals.get(3).copied().unwrap_or(0);
+            Some((idle, total))
+        };
+        let (idle1, total1) = read_stat().unwrap_or((0, 1));
+        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+        let (idle2, total2) = read_stat().unwrap_or((0, 1));
+        let d_idle = idle2.saturating_sub(idle1);
+        let d_total = total2.saturating_sub(total1);
+        if d_total > 0 {
+            (1.0 - d_idle as f64 / d_total as f64) * 100.0
+        } else { 0.0 }
+    } as f32;
+
+    let uptime = tokio::process::Command::new("uptime")
+        .args(["-p"]).output().await.ok()
+        .map(|o| String::from_utf8_lossy(&o.stdout).trim().trim_start_matches("up ").to_string())
+        .unwrap_or_default();
+
+    let gpu = tokio::process::Command::new("lspci")
+        .output().await.ok()
+        .and_then(|o| {
+            String::from_utf8_lossy(&o.stdout).lines()
+                .find(|l| l.contains("VGA") || l.contains("3D"))
+                .and_then(|l| l.split(':').nth(2))
+                .map(|s| s.trim().to_string())
+        })
+        .unwrap_or_default();
+
+    SystemState { hostname, kernel, cpu_model, cpu_usage, cpu_cores, uptime, gpu }
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+
+pub fn view(state: &SystemState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let mut y = cy + 12.0;
+
+    // Hostname / kernel
+    pc.text(&format!("{}  —  Linux {}", state.hostname, state.kernel), cx + 12.0, y, 14.0, TEXT_FG);
+    y += 22.0;
+
+    // Uptime
+    pc.text(&format!("Uptime: {}", state.uptime), cx + 12.0, y, 12.0, TEXT_DIM);
+    y += 18.0;
+
+    // CPU
+    pc.text(&format!("CPU  {}  ({} cores)  —  {:.0}%", state.cpu_model, state.cpu_cores, state.cpu_usage),
+        cx + 12.0, y, 12.0, TEXT_FG);
+    y += 18.0;
+
+    // GPU
+    pc.text(&format!("GPU  {}", state.gpu), cx + 12.0, y, 12.0, TEXT_FG);
+
+    pc
+}
+
+pub fn update(_state: &mut SystemState, _msg: SystemMessage) {}
diff --git a/src/widgets.rs b/src/widgets.rs
new file mode 100644
index 0000000..c0dd794
--- /dev/null
+++ b/src/widgets.rs
@@ -0,0 +1,30 @@
+use clear_ui::color;
+
+pub struct SectionStyle {
+    pub label: String,
+    pub x: f32,
+    pub y: f32,
+    pub w: f32,
+    pub h: f32,
+}
+
+pub fn section_rects(label: &str, x: f32, y: f32, w: f32, h: f32) -> Vec<([f32; 4], f32, f32, f32, f32)> {
+    let mut rects = Vec::new();
+    // Section background
+    rects.push((color::CONTENT_BG, x, y, w, h));
+    // Section border top
+    rects.push(([0.20, 0.33, 0.22, 1.0], x, y, w, 1.0));
+    rects
+}
+
+pub fn label_color() -> [f32; 4] {
+    color::TEXT_ACCENT
+}
+
+pub fn text_dim_color() -> [f32; 4] {
+    color::TEXT_DIM
+}
+
+pub fn text_fg_color() -> [f32; 4] {
+    color::TEXT_FG
+}