diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..d3873f3
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,19 @@
+#!/bin/sh
+# Mirrors the `fmt` job in .github/workflows/ci.yml so formatting
+# failures surface here instead of after a push.
+#
+# Enable with: git config core.hooksPath .githooks
+# Bypass once with: git commit --no-verify
+
+if ! command -v cargo >/dev/null 2>&1; then
+ echo "pre-commit: cargo not found, skipping fmt check" >&2
+ exit 0
+fi
+
+if ! cargo fmt --check; then
+ echo >&2
+ echo "pre-commit: formatting check failed (this is what CI's fmt job runs)." >&2
+ echo " Fix with: cargo fmt" >&2
+ echo " Bypass with: git commit --no-verify" >&2
+ exit 1
+fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 505f3b2..e544e8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
+## [1.0.0] - 2026-08-22
+
+### Added
+- Redesigned desktop UI: new design tokens, component and screen stylesheets,
+ and a modular frontend split into `store`, `actions`, `api` and per-screen modules
+- Clean history: every run is recorded per item with its outcome and reclaimed
+ size, and is browsable from a new History screen
+- Disk usage reporting, so reclaimable space is shown as a share of the volume
+ the artifacts actually live on
+- Template-based tray icons for correct rendering in light and dark menu bars
+
+### Changed
+- Refreshed app icons and website assets
+- Reworked scanners across all ecosystems for more accurate sizing and staleness
+- Expanded configuration options for scan roots, ecosystems and thresholds
+
+## [0.1.2] - 2026-08-22
+
+### Fixed
+- GUI-launched apps no longer fail to spawn toolchains with
+ "IO error: No such file or directory (os error 2)". macOS `launchd` and Linux
+ desktop launchers hand the app a bare `PATH`; the real `PATH` is now restored
+ at startup from the user's login shell, so `cargo`, `go`, `npm`, `brew` and
+ `docker` are found
+
+## [0.1.1] - 2026-08-22
+
+### Fixed
+- Sentinel files such as `.env` in a project root no longer block commands like
+ `cargo clean` from running there. Working directories are now checked
+ separately from deletion targets — the command never deletes that directory,
+ while deleting it directly is still refused
+
## [0.1.0] - 2026-03-12
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2746cf3..7497474 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,6 +62,18 @@ crates/
3. Add the ecosystem variant to `Ecosystem` enum in `model.rs`
4. Register it in `scanner/mod.rs` and `commands.rs`
+### Git Hooks
+
+Enable the repo's hooks once per clone:
+
+```bash
+git config core.hooksPath .githooks
+```
+
+The `pre-commit` hook runs `cargo fmt --check` — the same check as CI's `fmt`
+job — so formatting problems surface before you push. Bypass it for a single
+commit with `git commit --no-verify`.
+
## Code Style
- Run `cargo fmt` before committing
diff --git a/Cargo.lock b/Cargo.lock
index 6efc22c..900b3e5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -202,6 +202,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.11.1"
@@ -576,21 +582,24 @@ dependencies = [
[[package]]
name = "deepclean-app"
-version = "0.1.0"
+version = "1.0.0"
dependencies = [
+ "chrono",
"deepclean-core",
+ "dirs",
"serde",
"serde_json",
"tauri",
"tauri-build",
- "tauri-plugin-shell",
+ "tauri-plugin-dialog",
+ "tempfile",
"tokio",
"uuid",
]
[[package]]
name = "deepclean-core"
-version = "0.1.0"
+version = "1.0.0"
dependencies = [
"async-trait",
"bytesize",
@@ -814,15 +823,6 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
-[[package]]
-name = "encoding_rs"
-version = "0.8.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
-
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -1660,6 +1660,19 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+]
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -1708,25 +1721,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "is-docker"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
-dependencies = [
- "once_cell",
-]
-
-[[package]]
-name = "is-wsl"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
-dependencies = [
- "is-docker",
- "once_cell",
-]
-
[[package]]
name = "itoa"
version = "1.0.17"
@@ -2014,6 +2008,16 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
[[package]]
name = "muda"
version = "0.19.3"
@@ -2240,6 +2244,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.11.0",
"block2",
+ "libc",
"objc2",
"objc2-core-foundation",
]
@@ -2339,34 +2344,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-[[package]]
-name = "open"
-version = "5.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
-dependencies = [
- "dunce",
- "is-wsl",
- "libc",
- "pathdiff",
-]
-
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-[[package]]
-name = "os_pipe"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "pango"
version = "0.18.3"
@@ -2415,12 +2398,6 @@ dependencies = [
"windows-link 0.2.1",
]
-[[package]]
-name = "pathdiff"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
-
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -2765,6 +2742,12 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pxfm"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -2985,6 +2968,30 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "rfd"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
+dependencies = [
+ "block2",
+ "dispatch2",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -3310,44 +3317,12 @@ dependencies = [
"digest",
]
-[[package]]
-name = "shared_child"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
-dependencies = [
- "libc",
- "sigchld",
- "windows-sys 0.60.2",
-]
-
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-[[package]]
-name = "sigchld"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
-dependencies = [
- "libc",
- "os_pipe",
- "signal-hook",
-]
-
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -3673,6 +3648,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "image",
"jni",
"libc",
"log",
@@ -3787,24 +3763,45 @@ dependencies = [
]
[[package]]
-name = "tauri-plugin-shell"
-version = "2.3.5"
+name = "tauri-plugin-dialog"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
+checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
dependencies = [
- "encoding_rs",
"log",
- "open",
- "os_pipe",
- "regex",
+ "raw-window-handle",
+ "rfd",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-plugin-fs",
+ "thiserror 2.0.19",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-fs"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "glob",
+ "log",
+ "objc2-foundation",
+ "percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
- "shared_child",
+ "serde_repr",
"tauri",
"tauri-plugin",
+ "tauri-utils",
"thiserror 2.0.19",
- "tokio",
+ "toml 1.0.6+spec-1.1.0",
+ "url",
]
[[package]]
@@ -4092,6 +4089,21 @@ dependencies = [
"winnow 0.7.15",
]
+[[package]]
+name = "toml"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
+dependencies = [
+ "indexmap 2.13.0",
+ "serde_core",
+ "serde_spanned 1.0.4",
+ "toml_datetime 1.0.0+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
[[package]]
name = "toml_datetime"
version = "0.6.3"
diff --git a/README.md b/README.md
index e4d0017..a88ef0f 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,11 @@ One scan, gigabytes back.
[Download](https://github.com/eladbash/void/releases) · [Website](https://eladbash.github.io/void/) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
+
+
+
+
+
---
@@ -36,6 +41,9 @@ a risk level before you click it. Nothing is deleted without your say-so.
- **Safety first** — blocked-path lists, sentinel-file detection, and Safe / Caution / Danger classification on every action
- **Fast** — multi-threaded filesystem walker with concurrent analysis, built on Tokio and the `ignore` crate
- **Staleness aware** — surfaces artifacts untouched for longer than your threshold (30 days by default), so you clean what you're not using
+- **Nothing runs unseen** — every item shows the exact command or deletion it will perform, and you
+ pick which one when an artifact offers several
+- **Keeps a record** — each clean is stored locally with the paths, commands and bytes recovered
- **Lives in your menu bar** — quick-scan and open the dashboard without leaving your workflow
- **Yours to configure** — scan roots, enabled ecosystems, staleness thresholds, and paths Void must never touch
@@ -93,18 +101,38 @@ cargo tauri build # produce a release bundle
Deleting files is easy to get wrong, so Void is deliberately conservative:
-- **Never-touch paths** — `~/Documents`, `~/Desktop`, `~/Pictures`, `~/Library/Keychains`,
+- **Never-touch paths** — `~/Documents`, `~/Desktop`, `~/Pictures`, `~/Library/Keychains` and
`~/Library/Preferences`, plus anything named `.ssh`, `.gnupg`, `.aws`, `.config`, or `.env`
- anywhere in the path.
+ anywhere in the path. `~/Downloads` is protected as a folder, though individual files inside it
+ can be cleaned — that is what the System scanner is for.
+- **Commands are checked too** — an action that shells out to `rm`, `osascript` or PowerShell has
+ its arguments scanned against the same protected paths, and discovered filenames are escaped
+ before they are ever embedded in a script.
- **Sentinel detection** — a directory containing `.env`, `credentials`, `secrets.y(a)ml`,
`id_rsa`, or `id_ed25519` is excluded from deletion outright.
- **Risk escalation** — paths directly under `$HOME` and symlinks are automatically promoted to
a higher risk level.
+- **Risk follows the action** — an artifact can offer several cleanups at different risk levels;
+ Void shows the risk of the one that will actually run, and asks you to type `delete` before
+ anything permanent.
- **Your own blocklist** — add any path in settings and Void will refuse to touch it.
- **Nothing is automatic** — Void never deletes anything you haven't explicitly selected.
Found a safety gap? Please report it privately — see [SECURITY.md](SECURITY.md).
+## Configuration
+
+Settings and clean history are plain JSON, written atomically next to the bundle identifier:
+
+| Platform | Location |
+|----------|----------|
+| macOS | `~/Library/Application Support/com.void.app/` |
+| Linux | `~/.config/com.void.app/` |
+| Windows | `%APPDATA%\com.void.app\` |
+
+Both files are safe to edit or delete by hand — a missing or unparseable file falls back to
+defaults rather than blocking launch.
+
## Project layout
```
@@ -112,6 +140,7 @@ crates/
deepclean-core/ scanning engine, safety checker, action executor
deepclean-app/ Tauri 2 desktop app, system tray, frontend
docs/ project website (GitHub Pages)
+design/ design system and screen specifications
```
## Contributing
diff --git a/crates/deepclean-app/Cargo.toml b/crates/deepclean-app/Cargo.toml
index 3a0eddb..ac55a0b 100644
--- a/crates/deepclean-app/Cargo.toml
+++ b/crates/deepclean-app/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deepclean-app"
-version = "0.1.0"
+version = "1.0.0"
edition = "2021"
rust-version.workspace = true
@@ -10,8 +10,13 @@ serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }
-tauri = { version = "2", features = ["tray-icon"] }
-tauri-plugin-shell = "2"
+tauri = { version = "2", features = ["tray-icon", "image-png"] }
+tauri-plugin-dialog = "2"
+chrono = { workspace = true }
+dirs = { workspace = true }
+
+[dev-dependencies]
+tempfile = "3"
[build-dependencies]
tauri-build = { version = "2", features = [] }
diff --git a/crates/deepclean-app/capabilities/default.json b/crates/deepclean-app/capabilities/default.json
index 93d4c26..01fa8bf 100644
--- a/crates/deepclean-app/capabilities/default.json
+++ b/crates/deepclean-app/capabilities/default.json
@@ -2,10 +2,12 @@
"$schema": "https://raw.githubusercontent.com/nickkadutskyi/jb-tauri-plugin/main/crates/tauri-utils/schema.json",
"identifier": "default",
"description": "Capability for the main window",
- "windows": ["main"],
+ "windows": [
+ "main"
+ ],
"permissions": [
"core:default",
"core:event:default",
- "shell:allow-open"
+ "dialog:allow-open"
]
}
diff --git a/crates/deepclean-app/frontend/css/base.css b/crates/deepclean-app/frontend/css/base.css
new file mode 100644
index 0000000..426c9cb
--- /dev/null
+++ b/crates/deepclean-app/frontend/css/base.css
@@ -0,0 +1,106 @@
+/* ============================================================================
+ BASE — reset, native feel, type utilities.
+ ============================================================================ */
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html {
+ -webkit-text-size-adjust: 100%;
+ font-family: var(--font-sans);
+ font-optical-sizing: auto;
+ font-synthesis: none;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font: 450 var(--t-body) var(--font-sans);
+ letter-spacing: -0.003em;
+ background: var(--surface-base);
+ color: var(--text-primary);
+ height: 100vh;
+ overflow: hidden;
+ user-select: none;
+ /* Rubber-banding on a desktop window reveals the void behind it. */
+ overscroll-behavior: none;
+ cursor: default;
+}
+
+/* Native apps do not put a hand cursor on buttons. It is the single loudest
+ "this is a web page" tell in a webview app. */
+button, .btn, .chip, [role="button"], .rail-btn, .seg button { cursor: default; }
+
+/* ...and anything genuinely selectable has to say so. */
+.path, .size, .error-plate, .cmdline, .pathplate, input, textarea {
+ user-select: text;
+ cursor: text;
+}
+
+* { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
+
+:focus { outline: none; }
+:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 1px;
+ border-radius: inherit;
+}
+
+img, svg { display: block; }
+input, button, select, textarea {
+ font: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+}
+ul, ol { list-style: none; }
+
+/* Chrome must never be drag-selected. */
+.rail, .cmdbar, .toolbar, .statusbar, .summary, .tablehead { user-select: none; }
+
+/* ── Type utilities ── */
+.t-metric-lg { font: 600 var(--t-metric-lg) var(--font-sans); letter-spacing: -0.022em; }
+.t-metric-md { font: 600 var(--t-metric-md) var(--font-sans); letter-spacing: -0.020em; }
+.t-metric-sm { font: 600 var(--t-metric-sm) var(--font-sans); letter-spacing: -0.015em; }
+.t-page-title { font: 600 var(--t-page-title) var(--font-sans); letter-spacing: -0.014em; }
+.t-section-title { font: 600 var(--t-section-title) var(--font-sans); letter-spacing: -0.003em; }
+.t-body { font: 450 var(--t-body) var(--font-sans); letter-spacing: -0.003em; }
+.t-body-strong { font: 560 var(--t-body) var(--font-sans); letter-spacing: -0.003em; }
+.t-body-sm { font: 450 var(--t-body-sm) var(--font-sans); }
+.t-label { font: 520 var(--t-label) var(--font-sans); letter-spacing: -0.002em; }
+.t-caption { font: 450 var(--t-caption) var(--font-sans); letter-spacing: 0.004em; }
+.t-overline {
+ font: 620 var(--t-micro) var(--font-sans);
+ letter-spacing: 0.075em;
+ text-transform: uppercase;
+}
+.t-mono-sm { font: 430 var(--t-mono-sm) var(--font-mono); letter-spacing: -0.006em; }
+.t-mono-xs { font: 430 var(--t-mono-xs) var(--font-mono); letter-spacing: -0.004em; }
+
+.mono, code, .path, .cmdline, .error-plate, .pathplate {
+ font-family: var(--font-mono);
+ font-variant-ligatures: none;
+ font-feature-settings: "calt" 0, "liga" 0, "zero" 1;
+}
+
+/* The `font:` shorthand resets numeric variants, so .tnum comes after. */
+.tnum { font-variant-numeric: tabular-nums lining-nums; }
+
+.dim { color: var(--text-tertiary); }
+.spacer { flex: 1; }
+.hidden { display: none !important; }
+.sr-only {
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
+}
+
+/* Windows and Linux get styled scrollbars; macOS overlay scrollbars are
+ already correct and should be left alone. */
+:root[data-platform="windows"] ::-webkit-scrollbar,
+:root[data-platform="linux"] ::-webkit-scrollbar { width: 10px; height: 10px; }
+:root[data-platform="windows"] ::-webkit-scrollbar-track,
+:root[data-platform="linux"] ::-webkit-scrollbar-track { background: transparent; }
+:root[data-platform="windows"] ::-webkit-scrollbar-thumb,
+:root[data-platform="linux"] ::-webkit-scrollbar-thumb {
+ background: var(--border-default);
+ border: 2px solid transparent;
+ background-clip: padding-box;
+ border-radius: var(--radius-full);
+}
diff --git a/crates/deepclean-app/frontend/css/components.css b/crates/deepclean-app/frontend/css/components.css
new file mode 100644
index 0000000..92052b7
--- /dev/null
+++ b/crates/deepclean-app/frontend/css/components.css
@@ -0,0 +1,362 @@
+/* ============================================================================
+ COMPONENTS
+ ============================================================================ */
+
+/* ── Icons ── */
+.icon {
+ width: 16px; height: 16px; flex: none; display: block;
+ fill: none; stroke: currentColor; stroke-width: 1.5;
+ stroke-linecap: round; stroke-linejoin: round;
+}
+.icon-sm { width: 14px; height: 14px; stroke-width: 1.6; }
+.icon-lg { width: 20px; height: 20px; stroke-width: 1.4; }
+.icon-xl { width: 24px; height: 24px; stroke-width: 1.35; }
+.icon [data-solid] { fill: currentColor; stroke: none; }
+
+/* ── Buttons ── */
+.btn {
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
+ height: var(--ctl-h-md); padding: 0 11px;
+ border: 1px solid transparent; border-radius: var(--radius-sm);
+ font: 540 13px/1 var(--font-sans); letter-spacing: -0.004em;
+ white-space: nowrap; background: transparent; color: inherit;
+ transition: background-color var(--dur-2) var(--ease-out),
+ border-color var(--dur-2) var(--ease-out),
+ color var(--dur-2) var(--ease-out);
+}
+.btn:hover:not(:disabled) { transition-duration: var(--dur-1); }
+.btn:active:not(:disabled) { transform: scale(0.985); transition-duration: 60ms; }
+.btn .icon { width: 14px; height: 14px; }
+.btn:disabled { opacity: 0.45; pointer-events: none; }
+
+.btn-sm { height: var(--ctl-h-sm); padding: 0 8px; font-size: 12px; }
+.btn-lg { height: var(--ctl-h-lg); padding: 0 14px; }
+
+.btn-primary { background: var(--accent-bg); color: var(--accent-on-solid); box-shadow: var(--edge-highlight); }
+.btn-primary:hover:not(:disabled) { background: var(--accent-bg-hover); }
+.btn-primary:active:not(:disabled) { background: var(--accent-bg-active); }
+
+.btn-secondary { background: var(--surface-raised); color: var(--text-primary); border-color: var(--border-strong); }
+.btn-secondary:hover:not(:disabled) { background: var(--state-hover); }
+.btn-secondary:active:not(:disabled) { background: var(--state-active); }
+
+.btn-ghost { color: var(--text-secondary); }
+.btn-ghost:hover:not(:disabled) { background: var(--state-hover); color: var(--text-primary); }
+
+.btn-danger { background: var(--danger-solid); color: var(--danger-on-solid); box-shadow: var(--edge-highlight); }
+.btn-danger:hover:not(:disabled) { background: var(--danger-solid-hover); }
+
+.iconbtn {
+ width: var(--iconbtn); height: var(--iconbtn); padding: 0;
+ display: inline-flex; align-items: center; justify-content: center;
+ border: 1px solid transparent; border-radius: var(--radius-sm);
+ background: transparent; color: var(--text-tertiary);
+ transition: background-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
+}
+.iconbtn:hover { background: var(--state-hover); color: var(--text-primary); }
+.iconbtn.is-active { background: var(--accent-subtle); color: var(--accent-text); }
+.iconbtn:disabled { opacity: 0.45; pointer-events: none; }
+
+.kbd {
+ display: inline-flex; align-items: center; height: 16px; padding: 0 4px; margin-left: 2px;
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-xs);
+ background: var(--surface-sunken); color: var(--text-tertiary);
+ font: 500 10px/1 var(--font-sans);
+}
+
+/* ── Checkbox ── */
+.cb {
+ width: var(--checkbox); height: var(--checkbox); flex: none;
+ display: flex; align-items: center; justify-content: center;
+ border: 1px solid var(--border-strong); border-radius: var(--radius-xs);
+ background: var(--surface-raised);
+ transition: background-color 90ms var(--ease-out), border-color 90ms var(--ease-out);
+}
+.cb .icon { width: 11px; height: 11px; stroke-width: 2.2; color: var(--accent-on-solid); opacity: 0; }
+.cb[aria-checked="true"], .cb[aria-checked="mixed"] {
+ background: var(--accent-bg); border-color: var(--accent-bg);
+}
+.cb[aria-checked="true"] .icon-check,
+.cb[aria-checked="mixed"] .icon-mixed { opacity: 1; }
+.cb[aria-checked="true"] .icon-mixed,
+.cb[aria-checked="false"] .icon-mixed,
+.cb[aria-checked="mixed"] .icon-check { display: none; }
+.cb.is-disabled { opacity: 0.4; pointer-events: none; }
+
+/* ── Radio ── */
+.radio {
+ width: 14px; height: 14px; flex: none; margin-top: 2px; border-radius: 50%;
+ border: 1px solid var(--border-strong); background: var(--surface-raised);
+}
+.radio.is-on { border: 4px solid var(--accent-bg); background: var(--surface-raised); }
+
+/* ── Toggle ── */
+.toggle {
+ width: 32px; height: 18px; flex: none; position: relative;
+ border-radius: var(--radius-full); background: var(--chart-track);
+ transition: background-color var(--dur-2) var(--ease-out);
+}
+.toggle i {
+ position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
+ border-radius: 50%; background: var(--surface-raised); box-shadow: var(--shadow-1);
+ transition: transform var(--dur-2) var(--ease-out);
+}
+.toggle.is-on { background: var(--accent-bg); }
+.toggle.is-on i { transform: translateX(14px); }
+
+/* ── Badges ──
+ Risk is never carried by color alone: each badge pairs a glyph, a shape and
+ the word. Safe is a full pill; caution and danger are squarer, and danger
+ adds a left rule. */
+.badge {
+ display: inline-flex; align-items: center; gap: 3px;
+ height: var(--badge-h); padding: 1px 5px 0;
+ border-radius: var(--radius-xs);
+ font: 600 10px/1 var(--font-sans); letter-spacing: 0.045em;
+ text-transform: uppercase; white-space: nowrap;
+}
+.badge .icon { width: 10px; height: 10px; stroke-width: 2; }
+.badge-safe {
+ border-radius: var(--radius-full); padding: 1px 7px 0;
+ background: var(--safe-bg-subtle); color: var(--safe-text);
+ box-shadow: inset 0 0 0 1px var(--safe-border);
+}
+.badge-caution {
+ background: var(--caution-bg-subtle); color: var(--caution-text);
+ box-shadow: inset 0 0 0 1px var(--caution-border);
+}
+.badge-danger {
+ background: var(--danger-bg-subtle); color: var(--danger-text);
+ box-shadow: inset 0 0 0 1px var(--danger-border);
+ border-left: 2px solid var(--danger-border-strong);
+}
+.badge-neutral {
+ background: var(--surface-sunken); color: var(--text-tertiary);
+ box-shadow: inset 0 0 0 1px var(--border-subtle);
+}
+
+.countpill {
+ display: inline-flex; align-items: center; justify-content: center;
+ height: 16px; min-width: 16px; padding: 0 5px;
+ border-radius: var(--radius-xs); background: var(--surface-sunken);
+ color: var(--text-tertiary); font: 520 10px/1 var(--font-sans);
+ font-variant-numeric: tabular-nums;
+}
+
+/* ── Chips ── */
+.chip {
+ display: inline-flex; align-items: center; gap: 6px;
+ height: var(--ctl-h-sm); padding: 0 9px;
+ border: 1px solid var(--border-default); border-radius: var(--radius-sm);
+ background: var(--surface-raised); color: var(--text-secondary);
+ font: 500 12px/1 var(--font-sans); white-space: nowrap;
+ transition: background-color var(--dur-2) var(--ease-out),
+ border-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
+}
+.chip:hover { border-color: var(--border-strong); color: var(--text-primary); }
+.chip.is-active { background: var(--accent-subtle); border-color: var(--accent-border-strong); color: var(--accent-text); }
+.chip .icon { width: 13px; height: 13px; }
+.chip-quiet {
+ height: 18px; padding: 0 6px; font-size: 11px;
+ background: transparent; border-color: var(--border-subtle); color: var(--text-tertiary);
+}
+
+/* ── Inputs ── */
+.input {
+ height: var(--ctl-h-md); padding: 0 9px;
+ border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
+ background: var(--surface-sunken); color: var(--text-primary);
+ font: 450 13px/1 var(--font-sans);
+}
+.input::placeholder { color: var(--text-disabled); }
+.input:focus-visible { border-color: var(--accent-border-strong); }
+
+.search {
+ display: flex; align-items: center; gap: 6px;
+ height: var(--ctl-h-sm); flex: 1; max-width: 320px; min-width: 120px;
+ padding: 0 9px; border: 1px solid var(--border-default);
+ border-radius: var(--radius-sm); background: var(--surface-sunken);
+ color: var(--text-tertiary);
+}
+.search input {
+ flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
+ color: var(--text-primary); font: 450 12px/1 var(--font-sans);
+}
+.search input::placeholder { color: var(--text-disabled); }
+
+/* ── Segmented control ── */
+.seg {
+ display: inline-flex; height: var(--ctl-h-md); padding: 2px; gap: 2px;
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-sm);
+ background: var(--surface-sunken);
+}
+.seg button {
+ height: 100%; padding: 0 11px; border: 0; border-radius: 3px;
+ background: transparent; color: var(--text-tertiary);
+ font: 520 12px/1 var(--font-sans);
+}
+.seg button.is-on { background: var(--surface-raised); color: var(--text-primary); box-shadow: var(--shadow-1); }
+
+/* ── Size cell — four typographic tiers plus unknown ── */
+.size {
+ display: inline-flex; align-items: baseline; gap: 3px; justify-content: flex-end;
+ font-variant-numeric: tabular-nums lining-nums;
+}
+.size-num { font: 560 13px/18px var(--font-sans); letter-spacing: -0.003em; color: var(--text-primary); }
+.size-unit {
+ font: 500 10px/18px var(--font-sans); letter-spacing: 0.02em;
+ color: var(--text-tertiary); min-width: 16px; text-align: left;
+}
+.size[data-mag="xl"] .size-num { font-size: 15px; font-weight: 620; }
+.size[data-mag="lg"] .size-num { font-size: 14px; font-weight: 620; }
+.size[data-mag="sm"] .size-num { color: var(--text-secondary); font-weight: 450; }
+.size[data-mag="unknown"] .size-num {
+ font: italic 450 12px/18px var(--font-sans); color: var(--text-disabled);
+}
+
+/* ── Path with emphasis roles ── */
+.path {
+ font: 430 var(--t-mono-xs) var(--font-mono); letter-spacing: -0.004em;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
+}
+.path .s-root, .path .s-dim { color: var(--text-tertiary); }
+.path .s-sep, .path .s-gap { color: var(--text-disabled); }
+.path .s-project { color: var(--text-secondary); font-weight: 560; }
+.path .s-leaf { color: var(--text-secondary); font-weight: 500; }
+
+/* ── Bars ── */
+.magbar { display: block; height: 4px; border-radius: 2px; background: var(--chart-track); overflow: hidden; }
+.magbar i { display: block; height: 100%; border-radius: 2px; background: var(--eco-accent); opacity: 0.85; }
+.sharebar { display: block; width: 100px; height: 6px; flex: none; border-radius: var(--radius-xs); background: var(--chart-track); overflow: hidden; }
+.sharebar i { display: block; height: 100%; border-radius: var(--radius-xs); background: var(--eco-accent); }
+.bigbar { flex: none; height: 8px; border-radius: var(--radius-xs); background: var(--chart-track); overflow: hidden; }
+.bigbar i { display: block; height: 100%; border-radius: var(--radius-xs); background: var(--accent-bg); transition: width var(--dur-4) linear; }
+
+.diskmeter { display: flex; height: 8px; border-radius: var(--radius-xs); background: var(--chart-track); overflow: hidden; }
+.diskmeter i { display: block; height: 100%; }
+.diskmeter .seg-used { background: var(--text-disabled); }
+.diskmeter .seg-recl { background: var(--accent-bg); }
+
+/* ── Progress ── */
+.progress { height: 2px; flex: none; background: var(--chart-track); overflow: hidden; position: relative; }
+.progress.is-indeterminate::after {
+ content: ''; position: absolute; inset-block: 0; left: 0; width: 32%;
+ background: var(--accent-bg); border-radius: 1px;
+ animation: prog-slide 1150ms var(--ease-standard) infinite;
+}
+@keyframes prog-slide {
+ 0% { transform: translateX(-105%) scaleX(0.55); }
+ 55% { transform: translateX(120%) scaleX(1); }
+ 100% { transform: translateX(320%) scaleX(0.55); }
+}
+
+.spinring {
+ width: 12px; height: 12px; flex: none; border-radius: 50%;
+ border: 1.5px solid currentColor; border-right-color: transparent;
+ animation: void-spin 900ms linear infinite;
+}
+@keyframes void-spin { to { transform: rotate(360deg); } }
+
+/* ── Plates ── */
+.cmdline {
+ display: block; padding: 4px 6px;
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-xs);
+ background: var(--surface-inset); color: var(--text-code);
+ font: 430 var(--t-mono-xs) var(--font-mono); word-break: break-all;
+}
+.pathplate {
+ padding: 8px 10px; border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm); background: var(--surface-inset);
+ color: var(--text-code); font: 430 var(--t-mono-xs) var(--font-mono);
+ word-break: break-all;
+}
+/* Machine text is always monospace, always selectable, never folded into a
+ sentence — it is the difference between a paraphrase and the real message. */
+.error-plate {
+ padding: 8px 10px; text-align: left;
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-sm);
+ background: var(--surface-inset); color: var(--text-secondary);
+ font: 430 var(--t-mono-xs) var(--font-mono);
+ max-width: 52ch; max-height: 96px; overflow: auto;
+ white-space: pre-wrap; overflow-wrap: anywhere;
+}
+
+.card {
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
+ background: var(--surface-raised); padding: 12px;
+}
+
+/* ── Empty / error states ── */
+.state {
+ display: flex; flex-direction: column; align-items: center; text-align: center;
+ padding: 56px 24px;
+}
+.state .glyph { width: 24px; height: 24px; color: var(--text-disabled); stroke-width: 1.35; }
+.state .title { margin-top: 12px; font: 600 14px/20px var(--font-sans); letter-spacing: -0.008em; color: var(--text-primary); }
+.state .body {
+ margin-top: 4px; font: 450 13px/20px var(--font-sans);
+ color: var(--text-secondary); max-width: 44ch; text-wrap: pretty;
+}
+.state .actions { margin-top: 16px; display: flex; gap: 8px; }
+.state .hint { margin-top: 14px; font: 450 11px/14px var(--font-sans); color: var(--text-disabled); }
+.state[data-size="sm"] { padding: 28px 16px; }
+
+/* ── Banner ── */
+.banner {
+ height: 44px; flex: none; display: flex; align-items: center; gap: 10px;
+ padding: 0 var(--pad-window-x); border-bottom: 1px solid var(--border-subtle);
+ background: var(--caution-bg-subtle); color: var(--caution-text);
+}
+.banner .txt { flex: 1; color: var(--text-primary); font: 450 var(--t-body) var(--font-sans); }
+
+/* ── Skeleton ── */
+@keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.42; } }
+.skeleton {
+ background: var(--skeleton-base); border-radius: var(--radius-xs);
+ animation: skeleton-pulse 1400ms cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+/* ── Modal ── */
+.scrim {
+ position: absolute; inset: 0; z-index: 40;
+ display: flex; align-items: center; justify-content: center;
+ background: var(--surface-scrim);
+}
+.modal {
+ width: 560px; max-width: calc(100% - 48px); max-height: calc(100% - 48px);
+ display: flex; flex-direction: column;
+ background: var(--surface-overlay); border: 1px solid var(--border-default);
+ border-radius: var(--radius-lg); box-shadow: var(--shadow-3); overflow: hidden;
+}
+.modal-head { padding: 16px 20px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: flex-start; gap: 12px; }
+.modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
+.modal-foot {
+ flex: none; padding: 14px 20px; border-top: 1px solid var(--border-subtle);
+ display: flex; align-items: center; gap: 10px; background: var(--surface-raised);
+}
+
+/* ── Popover ── */
+.popover {
+ position: absolute; z-index: 30; min-width: 260px;
+ background: var(--surface-overlay); border: 1px solid var(--border-default);
+ border-radius: var(--radius-md); box-shadow: var(--shadow-2); overflow: hidden;
+}
+.popover-row {
+ display: flex; align-items: center; gap: 8px; width: 100%;
+ padding: 7px 10px; border: 0; background: transparent;
+ color: var(--text-primary); font: 450 var(--t-body) var(--font-sans); text-align: left;
+}
+.popover-row:hover { background: var(--state-hover); }
+.popover-sect { padding: 8px 10px 4px; }
+.popover-foot { border-top: 1px solid var(--border-subtle); padding: 6px; }
+
+/* ── Toast — the only genuinely undoable action gets the only toast ── */
+.toast {
+ position: absolute; left: 16px; bottom: 40px; z-index: 50;
+ display: flex; align-items: center; gap: 12px;
+ min-width: 280px; padding: 10px 12px;
+ background: var(--surface-overlay); border: 1px solid var(--border-default);
+ border-radius: var(--radius-md); box-shadow: var(--shadow-2);
+ animation: toast-in var(--dur-4) var(--ease-out);
+}
+@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }
diff --git a/crates/deepclean-app/frontend/css/screens.css b/crates/deepclean-app/frontend/css/screens.css
new file mode 100644
index 0000000..0f16326
--- /dev/null
+++ b/crates/deepclean-app/frontend/css/screens.css
@@ -0,0 +1,338 @@
+/* ============================================================================
+ SHELL & SCREENS
+ ============================================================================ */
+
+.app { display: flex; height: 100vh; overflow: hidden; }
+
+/* ── Rail ──
+ 56px of horizontal space is 6% at the 900px default; a top nav would spend
+ 52px of vertical space, which is the scarce dimension in a 700px window. */
+.rail {
+ width: var(--rail-w); flex: none;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 4px; padding: 12px 0 16px;
+ background: var(--surface-sunken); border-right: 1px solid var(--border-subtle);
+}
+.rail-mark { width: 24px; height: 24px; color: var(--accent-bg); margin-bottom: 20px; }
+.rail-mark.is-scanning { animation: void-spin 900ms linear infinite; }
+.rail-btn {
+ position: relative; width: 40px; height: 40px;
+ display: flex; align-items: center; justify-content: center;
+ border: 0; border-radius: var(--radius-sm);
+ background: transparent; color: var(--text-tertiary);
+ transition: background-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
+}
+.rail-btn:hover { background: var(--state-hover); color: var(--text-secondary); }
+.rail-btn[aria-current="page"] {
+ background: var(--accent-subtle); color: var(--accent-text);
+ box-shadow: inset 2px 0 0 var(--accent-bg);
+}
+.rail-dot {
+ position: absolute; top: 7px; right: 7px; width: 6px; height: 6px;
+ border-radius: 50%; background: var(--accent-bg);
+ box-shadow: 0 0 0 2px var(--surface-sunken);
+}
+.rail-disk { width: 40px; display: flex; flex-direction: column; align-items: center; gap: 5px; color: var(--text-tertiary); }
+.rail-disk-bar {
+ width: 4px; height: 36px; border-radius: 2px; overflow: hidden;
+ background: var(--chart-track);
+ display: flex; flex-direction: column; justify-content: flex-end;
+}
+.rail-disk-bar i { display: block; width: 100%; flex: none; }
+.rail-disk-bar .recl { background: var(--accent-bg); }
+.rail-disk-bar .used { background: var(--text-disabled); }
+
+/* ── Screen frame ── */
+.screen { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; }
+
+.cmdbar {
+ height: var(--header-h); flex: none;
+ display: flex; align-items: center; gap: 12px;
+ padding: 0 var(--pad-window-x);
+ border-bottom: 1px solid var(--border-subtle); background: var(--surface-base);
+}
+.cmdbar-titles { flex: 1; min-width: 0; }
+.cmdbar-titles .sub { color: var(--text-tertiary); margin-top: 1px; font-variant-numeric: tabular-nums; }
+.cmdbar-actions { display: flex; align-items: center; gap: 8px; flex: none; }
+
+/* ── Summary strip ── */
+.summary {
+ height: var(--summary-h); flex: none;
+ display: flex; align-items: center; gap: 20px;
+ padding: 0 var(--pad-window-x);
+ border-bottom: 1px solid var(--border-subtle); background: var(--surface-base);
+}
+.summary-headline { width: 250px; flex: none; }
+.summary-headline .metric { display: flex; align-items: baseline; gap: 4px; }
+.summary-headline .sub { color: var(--text-tertiary); margin-top: 2px; font-variant-numeric: tabular-nums; }
+.summary-disk { flex: 1; min-width: 0; }
+.summary-disk .legend {
+ margin-top: 6px; display: flex; gap: 12px; white-space: nowrap; overflow: hidden;
+ color: var(--text-tertiary); font-variant-numeric: tabular-nums;
+}
+.summary-disk .legend b { color: var(--text-secondary); font-weight: 560; }
+/* Reserved so selecting never shifts the layout under the cursor. */
+.summary-action { width: 240px; flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
+.summary-action .selinfo { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
+.summary-action .selinfo b { color: var(--text-primary); font-weight: 600; }
+
+/* Below the default window width the headline and the action button leave the
+ disk meter too little room to say anything useful, and it wraps. The rail
+ keeps showing disk usage, so the strip drops it rather than cramming it. */
+@media (max-width: 860px) {
+ .summary-headline { width: auto; flex: 1; }
+ .summary-disk { display: none; }
+}
+
+/* Clean-in-progress variant of the same strip. */
+.summary.is-progress { height: 78px; align-items: stretch; padding-block: 10px; }
+.summary.is-progress .prog { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 7px; }
+.summary.is-progress .prog-row { display: flex; align-items: baseline; gap: 12px; }
+
+/* Completion card. */
+.resultcard {
+ height: 96px; flex: none; display: flex; align-items: center; gap: 14px;
+ padding: 0 var(--pad-window-x);
+ border-bottom: 1px solid var(--border-subtle); background: var(--safe-bg-subtle);
+}
+.resultcard.has-failures { background: var(--caution-bg-subtle); }
+.resultcard .glyph { color: var(--safe-text); }
+.resultcard.has-failures .glyph { color: var(--caution-text); }
+.resultcard .link { color: var(--danger-text); text-decoration: underline; }
+
+/* ── Scanner ticker ── */
+.ticker {
+ height: 28px; flex: none; display: flex; align-items: center; gap: 14px;
+ padding: 0 var(--pad-window-x); overflow-x: auto; overflow-y: hidden;
+ border-bottom: 1px solid var(--border-subtle); background: var(--surface-sunken);
+ scrollbar-width: none;
+}
+.ticker::-webkit-scrollbar { display: none; }
+.ticker-item {
+ display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
+ color: var(--text-disabled); font: 500 11px/1 var(--font-sans);
+}
+.ticker-item.is-done { color: var(--text-secondary); }
+.ticker-item.is-running { color: var(--accent-text); }
+.ticker-item .icon { width: 12px; height: 12px; }
+
+/* ── Toolbar ── */
+.toolbar {
+ height: var(--toolbar-h); flex: none; display: flex; align-items: center; gap: 8px;
+ padding: 0 var(--pad-window-x);
+ border-bottom: 1px solid var(--border-subtle); background: var(--surface-base);
+}
+.toolbar-sep { width: 1px; height: 16px; background: var(--border-subtle); margin: 0 8px; }
+
+/* ── Table ── */
+.tablehead {
+ height: var(--colhead-h); flex: none; display: flex; align-items: center;
+ padding: 0 var(--pad-window-x); color: var(--text-tertiary);
+ border-bottom: 1px solid var(--border-subtle); background: var(--surface-base);
+}
+.col-stale { width: 56px; text-align: right; padding-right: 12px; flex: none; }
+.col-mag { width: 56px; padding-right: 12px; flex: none; }
+.col-size { width: 84px; flex: none; display: flex; justify-content: flex-end; padding-right: 8px; }
+.col-chev { width: 24px; flex: none; display: flex; align-items: center; justify-content: center; }
+
+.listwrap { flex: 1; min-height: 0; position: relative; background: var(--surface-base); }
+.list { height: 100%; overflow-y: auto; padding-bottom: 8px; }
+
+.group {
+ position: sticky; top: 0; z-index: 2;
+ height: var(--group-h); display: flex; align-items: center; gap: 10px;
+ padding: 0 var(--pad-window-x); margin-top: -1px;
+ background: var(--surface-sunken);
+ border-block: 1px solid var(--border-subtle);
+}
+.group .gname { color: var(--text-primary); }
+.group .gsize { width: 84px; flex: none; text-align: right; }
+.group-toggle {
+ display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
+ border: 0; background: transparent; color: inherit; text-align: left;
+}
+
+.item {
+ position: relative; height: var(--row-h); overflow: hidden;
+ display: flex; align-items: center;
+ padding: 0 var(--pad-window-x);
+ background: var(--surface-base); border-bottom: 1px solid var(--border-subtle);
+ transition: background-color var(--dur-2) var(--ease-out);
+}
+.item:hover { background: var(--state-hover); }
+.item.is-selected { background: var(--state-selected); box-shadow: inset 2px 0 0 var(--state-selected-border); }
+.item.is-focused { box-shadow: inset 0 0 0 2px var(--focus-ring); }
+/* Per-row magnitude, drawn under the row rather than in a column. */
+.item::after {
+ content: ''; position: absolute; left: 0; bottom: 0; height: 2px;
+ width: calc(var(--size-ratio, 0) * 100%);
+ background: var(--eco-accent); opacity: 0.22;
+ border-radius: 0 1px 1px 0; pointer-events: none;
+}
+.item .cell-check { width: 22px; flex: none; display: flex; align-items: center; }
+/* Risk edge: zero-cost, always-present, reflects the *selected action*. */
+.riskedge { width: 4px; align-self: stretch; flex: none; margin-right: 8px; border-radius: 1px; }
+.riskedge.safe { background: transparent; }
+.riskedge.caution { background: var(--caution-solid); }
+.riskedge.danger { background: var(--danger-solid); }
+.item .cell-main { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 1px; padding-right: 12px; }
+.item .line1, .item .line2 {
+ display: flex; align-items: center; gap: 8px;
+ min-width: 0; flex-wrap: nowrap; overflow: hidden;
+}
+/* Badges and chips keep their size; the text around them gives way. */
+.item .line1 > .badge, .item .line2 > .chip { flex: none; }
+.item .kindname {
+ color: var(--text-primary); font: 560 var(--t-body) var(--font-sans);
+ letter-spacing: -0.003em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.item .projname {
+ color: var(--text-tertiary); font: 450 var(--t-body) var(--font-sans);
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ flex: 0 1 auto; min-width: 0;
+}
+/* Shrink order is expressed as differential flex-shrink rather than source
+ order: the short label that says *what* this is must survive long after the
+ 64-character hash beside it has been ellipsed away. */
+.item .kindname { flex: 0 1 auto; flex-shrink: 1; }
+.item .projname { flex-shrink: 400; }
+
+/* The path is the item's identity, so it takes the leftover space and gives it
+ back last. */
+.item .line2 > .path { flex: 1 1 auto; min-width: 0; flex-shrink: 1; }
+/* "via cargo clean" is a hint, not the subject. Capped so it can never crowd
+ the path, and first to yield when space runs out. */
+.item .via {
+ color: var(--text-tertiary); white-space: nowrap;
+ overflow: hidden; text-overflow: ellipsis;
+ flex: 0 1 auto; min-width: 0; max-width: 45%; flex-shrink: 400;
+}
+.item .cell-stale { color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
+.item .cell-stale.is-stale { color: var(--caution-text); }
+.item .cell-chev { color: var(--text-disabled); }
+.item:hover .cell-chev { color: var(--text-secondary); }
+
+/* Row states during a clean. */
+.item.is-queued { opacity: 0.55; }
+.item.is-active { background: var(--state-hover); box-shadow: inset 2px 0 0 var(--accent-bg); }
+.item.is-failed { background: var(--danger-bg-subtle); }
+
+[data-density="compact"] .item { height: var(--row-h-compact); }
+[data-density="compact"] .item .line1 .kindname { font-size: 12px; }
+
+/* ── Status bar ── */
+.statusbar {
+ height: var(--statusbar-h); flex: none; display: flex; align-items: center; gap: 12px;
+ padding: 0 var(--pad-window-x); white-space: nowrap; overflow: hidden;
+ border-top: 1px solid var(--border-subtle); background: var(--surface-sunken);
+ color: var(--text-tertiary); font-variant-numeric: tabular-nums;
+}
+.statusbar > * { flex: none; }
+.statusbar .issues { display: inline-flex; align-items: center; gap: 5px; color: var(--caution-text); border: 0; background: transparent; }
+
+/* ── Drawer ──
+ Below 1280 it overlays with a scrim so the table keeps the layout the user
+ already knows; at 1280+ it becomes an inline column. */
+.drawer {
+ width: var(--drawer-w); flex: none; display: flex; flex-direction: column;
+ background: var(--surface-raised); border-left: 1px solid var(--border-subtle);
+ overflow: hidden;
+ position: absolute; top: 0; right: 0; bottom: 0; z-index: 20;
+ box-shadow: var(--shadow-3);
+}
+.drawer-scrim { position: absolute; inset: 0; z-index: 19; background: var(--surface-scrim); }
+@media (min-width: 1280px) {
+ .drawer { position: static; box-shadow: none; }
+ .drawer-scrim { display: none; }
+ .app.has-drawer .screen { flex: 1; }
+}
+.app.has-drawer { position: relative; }
+.drawer-head { height: 44px; flex: none; display: flex; align-items: center; gap: 6px; padding: 0 12px 0 16px; border-bottom: 1px solid var(--border-subtle); }
+.drawer-body { flex: 1; overflow-y: auto; }
+.drawer-sect { padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); }
+.drawer-sect:last-child { border-bottom: 0; }
+.drawer-sect .t-overline { color: var(--text-disabled); margin-bottom: 10px; }
+.drawer-foot { flex: none; padding: 12px 16px; border-top: 1px solid var(--border-subtle); display: flex; gap: 8px; }
+.facts { display: grid; grid-template-columns: 92px 1fr; row-gap: 6px; column-gap: 12px; align-items: baseline; }
+.facts dt { color: var(--text-tertiary); }
+.facts dd { color: var(--text-primary); font-variant-numeric: tabular-nums; word-break: break-all; }
+
+.action-opt { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--border-subtle); text-align: left; width: 100%; background: transparent; border-left: 0; border-right: 0; border-top: 0; }
+.action-opt:last-child { border-bottom: 0; }
+.action-opt .body { flex: 1; min-width: 0; }
+.action-opt .row1 { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; flex-wrap: wrap; }
+.action-opt .lbl { color: var(--text-primary); font: 540 var(--t-body) var(--font-sans); }
+.action-opt .desc { color: var(--text-tertiary); font: 450 var(--t-body-sm) var(--font-sans); margin-bottom: 4px; }
+.action-opt .outcome { margin-top: 4px; color: var(--text-disabled); font: 450 var(--t-caption) var(--font-sans); }
+.action-opt .outcome .recoverable { color: var(--safe-text); }
+
+.safety-line { display: flex; gap: 7px; align-items: flex-start; margin-bottom: 6px; color: var(--text-secondary); font: 450 var(--t-body-sm) var(--font-sans); }
+.safety-line .icon { width: 13px; height: 13px; margin-top: 1.5px; flex: none; }
+.safety-line.ok .icon { color: var(--safe-text); }
+.safety-line.warn, .safety-line.warn .icon { color: var(--caution-text); }
+
+/* ── Confirm modal ── */
+.effect-row { display: flex; align-items: center; gap: 10px; height: 32px; color: var(--text-primary); }
+.effect-row .icon { color: var(--text-tertiary); flex: none; }
+.effect-row .txt { flex: 1; }
+.effect-row .amt { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
+/* The commit control must never sit below the fold, so the itemised list is
+ what scrolls. */
+.attn-card {
+ border: 1px solid var(--border-default); border-radius: var(--radius-sm);
+ background: var(--surface-inset); max-height: 200px; overflow-y: auto;
+}
+.attn-item { padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); }
+.attn-item:last-child { border-bottom: 0; }
+.attn-item .row1 { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
+.attn-item .row1 .lbl { flex: 1; color: var(--text-primary); font: 540 var(--t-body) var(--font-sans); }
+.attn-item .row1 .amt { color: var(--text-secondary); font-variant-numeric: tabular-nums; font-size: 12px; }
+
+/* ── Settings ── */
+.settings-nav { height: 44px; flex: none; display: flex; align-items: center; padding: 0 var(--pad-window-x); border-bottom: 1px solid var(--border-subtle); }
+.form { padding: 20px var(--pad-window-x); max-width: 560px; }
+.field { margin-bottom: 22px; }
+.field .help { color: var(--text-tertiary); margin-bottom: 8px; }
+.field .foot { color: var(--text-disabled); margin-top: 8px; max-width: 46ch; }
+.pathlist { border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--surface-inset); overflow: hidden; }
+.pathrow { height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; border-bottom: 1px solid var(--border-subtle); color: var(--text-primary); }
+.pathrow:last-child { border-bottom: 0; }
+.pathrow .p { flex: 1; min-width: 0; color: var(--text-secondary); font: 430 var(--t-mono-sm) var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.pathrow.is-locked { color: var(--text-tertiary); opacity: 0.8; }
+.ecorow { height: 48px; display: flex; align-items: center; gap: 10px; padding: 0 10px; border-bottom: 1px solid var(--border-subtle); }
+.ecorow:last-child { border-bottom: 0; }
+.ecorow .nm { flex: 1; color: var(--text-primary); }
+.ecorow .stat { color: var(--text-tertiary); font-variant-numeric: tabular-nums; text-align: right; }
+.slider-row { display: flex; justify-content: space-between; align-items: baseline; }
+.slider { width: 100%; margin: 10px 0; accent-color: var(--accent-bg); }
+
+/* ── History ── */
+.stattiles { display: flex; gap: 10px; padding: 16px var(--pad-window-x) 0; }
+.stattile {
+ flex: 1; height: 72px; padding: 10px 12px;
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
+ background: var(--surface-raised);
+ display: flex; flex-direction: column; justify-content: space-between;
+}
+.stattile .k { color: var(--text-disabled); }
+.spark { display: flex; align-items: flex-end; gap: 3px; height: 64px; }
+/* Capped so a history of three runs does not render three giant slabs. */
+.spark i { flex: 1 1 0; max-width: 28px; min-height: 2px; border-radius: 1.5px 1.5px 0 0; background: var(--accent-bg); opacity: 0.75; }
+.spark i.hi { opacity: 1; }
+.hrow { height: 44px; display: flex; align-items: center; gap: 12px; padding: 0 var(--pad-window-x); border-bottom: 1px solid var(--border-subtle); background: var(--surface-base); }
+.hrow:hover { background: var(--state-hover); }
+.hrow .d { width: 150px; color: var(--text-primary); }
+.hrow .n { width: 60px; text-align: right; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
+.hrow .f { width: 90px; text-align: right; color: var(--text-primary); font-weight: 560; font-variant-numeric: tabular-nums; }
+.hrow .r { flex: 1; padding-left: 16px; color: var(--text-tertiary); }
+.ok-text { color: var(--safe-text); }
+.fail-text { color: var(--danger-text); }
+
+/* ── Wordmark ── */
+.void-lockup { display: inline-flex; align-items: center; gap: 8px; }
+.void-mark { color: var(--accent-bg); flex: none; }
+.void-word {
+ font-family: var(--font-sans); font-size: 16px; font-weight: 600; line-height: 20px;
+ letter-spacing: -0.032em; color: var(--text-primary); margin-top: 1px;
+}
+.void-word .v { letter-spacing: -0.044em; }
diff --git a/crates/deepclean-app/frontend/css/tokens.css b/crates/deepclean-app/frontend/css/tokens.css
new file mode 100644
index 0000000..004c6d9
--- /dev/null
+++ b/crates/deepclean-app/frontend/css/tokens.css
@@ -0,0 +1,542 @@
+/* ============================================================================
+ VOID — COLOR SYSTEM v1
+ Neutral hue 254 / Accent hue 252, sampled from the app icon's glow.
+ Light = :root ; Dark = prefers-color-scheme AND [data-theme="dark"].
+ ============================================================================ */
+
+:root {
+ /* ---------- PRIMITIVE : NEUTRAL (OKLCH hue 254) ---------- */
+ --n-0: #FFFFFF; /* oklch(1.000 0.000 254) */
+ --n-50: #FAFBFD; /* oklch(0.988 0.003 254) */
+ --n-100: #F3F6F9; /* oklch(0.972 0.005 254) */
+ --n-150: #E5E8ED; /* oklch(0.930 0.007 254) */
+ --n-200: #D7DCE1; /* oklch(0.892 0.009 254) */
+ --n-300: #C2C7CE; /* oklch(0.828 0.011 254) */
+ --n-400: #A5ABB3; /* oklch(0.740 0.013 254) */
+ --n-500: #868D95; /* oklch(0.640 0.015 254) */
+ --n-600: #69717A; /* oklch(0.545 0.017 254) */
+ --n-700: #515963; /* oklch(0.462 0.019 254) */
+ --n-800: #363E48; /* oklch(0.360 0.020 254) */
+ --n-850: #283039; /* oklch(0.305 0.020 254) */
+ --n-900: #1C232C; /* oklch(0.252 0.020 254) */
+ --n-950: #0F151D; /* oklch(0.195 0.018 254) */
+ --n-975: #060B11; /* oklch(0.148 0.016 254) */
+ --n-1000: #010306; /* oklch(0.095 0.014 254) */
+
+ /* ---------- PRIMITIVE : GLOW (accent, OKLCH hue 252) ---------- */
+ --glow-50: #EDF5FF; /* oklch(0.968 0.016 252) */
+ --glow-100: #DBECFF; /* oklch(0.935 0.032 252) */
+ --glow-200: #BEDCFF; /* oklch(0.885 0.058 252) */
+ --glow-300: #95C7FF; /* oklch(0.815 0.096 252) */
+ --glow-400: #6BB2FF; /* oklch(0.748 0.133 252) */
+ --glow-500: #389BFF; /* oklch(0.682 0.173 252) */
+ --glow-600: #007FE2; /* oklch(0.592 0.177 252) */
+ --glow-700: #0064B5; /* oklch(0.500 0.150 252) */
+ --glow-800: #004986; /* oklch(0.402 0.121 252) */
+ --glow-900: #00315E; /* oklch(0.312 0.094 252) */
+ --glow-950: #001E3E; /* oklch(0.235 0.071 252) */
+
+ /* ---------- PRIMITIVE : SAFE (OKLCH hue 170) ---------- */
+ --safe-50: #E8FFF6; /* oklch(0.980 0.026 170) */
+ --safe-100: #D0FCEB; /* oklch(0.955 0.050 170) */
+ --safe-200: #77F8CF; /* oklch(0.895 0.130 170) */
+ --safe-300: #6AECC4; /* oklch(0.860 0.130 170) */
+ --safe-400: #54D8B1; /* oklch(0.800 0.130 170) */
+ --safe-500: #39C39D; /* oklch(0.735 0.130 170) */
+ --safe-600: #05AB86; /* oklch(0.660 0.130 170) */
+ --safe-650: #009D7B; /* oklch(0.620 0.123 170) */
+ --safe-700: #007D61; /* oklch(0.525 0.104 170) */
+ --safe-800: #00634C; /* oklch(0.445 0.088 170) */
+ --safe-900: #004332; /* oklch(0.340 0.067 170) */
+ --safe-950: #002A1F; /* oklch(0.255 0.051 170) */
+
+ /* ---------- PRIMITIVE : CAUTION (OKLCH hue 88) ---------- */
+ --caution-50: #FFF8E7; /* oklch(0.980 0.023 88) */
+ --caution-100: #FFEFC8; /* oklch(0.955 0.053 88) */
+ --caution-200: #FFD875; /* oklch(0.895 0.125 88) */
+ --caution-300: #FEC92F; /* oklch(0.860 0.165 88) */
+ --caution-400: #E9B600; /* oklch(0.800 0.164 88) */
+ --caution-500: #D1A200; /* oklch(0.735 0.150 88) */
+ --caution-600: #B58C00; /* oklch(0.660 0.135 88) */
+ --caution-650: #A68000; /* oklch(0.620 0.127 88) */
+ --caution-700: #846600; /* oklch(0.525 0.107 88) */
+ --caution-800: #695000; /* oklch(0.445 0.091 88) */
+ --caution-900: #473500; /* oklch(0.340 0.070 88) */
+ --caution-950: #2D2100; /* oklch(0.255 0.053 88) */
+
+ /* ---------- PRIMITIVE : DANGER (OKLCH hue 15) ---------- */
+ --danger-50: #FFF6F6; /* oklch(0.980 0.010 15) */
+ --danger-100: #FFEAEB; /* oklch(0.955 0.022 15) */
+ --danger-200: #FFCED1; /* oklch(0.895 0.055 15) */
+ --danger-300: #FFBDC1; /* oklch(0.860 0.076 15) */
+ --danger-400: #FF9EA6; /* oklch(0.800 0.116 15) */
+ --danger-500: #FF7887; /* oklch(0.735 0.164 15) */
+ --danger-600: #EF546C; /* oklch(0.660 0.190 15) */
+ --danger-650: #E14660; /* oklch(0.620 0.190 15) */
+ --danger-700: #BF2145; /* oklch(0.525 0.190 15) */
+ --danger-800: #9E0032; /* oklch(0.445 0.178 15) */
+ --danger-900: #6D0020; /* oklch(0.340 0.136 15) */
+ --danger-950: #480012; /* oklch(0.255 0.102 15) */
+
+ /* ============ SEMANTIC — LIGHT THEME ============ */
+ --surface-base: #FAFBFD;
+ --surface-raised: #FFFFFF;
+ --surface-sunken: #F3F6F9;
+ --surface-overlay: #FFFFFF;
+ --surface-inset: #E5E8ED;
+ --surface-scrim: rgba(28, 35, 44, 0.45);
+
+ --border-subtle: #E5E8ED; /* decorative hairline */
+ --border-default: #D7DCE1; /* decorative separator */
+ --border-strong: #868D95; /* >=3:1 — inputs, checkboxes, toggles */
+
+ --text-primary: #1C232C;
+ --text-secondary: #515963;
+ --text-tertiary: #69717A;
+ --text-disabled: #A5ABB3; /* WCAG 1.4.3 inactive-control exception */
+ --text-inverse: #FFFFFF;
+ --text-on-accent: #FFFFFF;
+ --text-code: #363E48;
+ --text-link: #0064B5;
+
+ --accent-bg: #0064B5;
+ --accent-bg-hover: #004986;
+ --accent-bg-active: #00315E;
+ --accent-subtle: #EDF5FF;
+ --accent-subtle-hover: #DBECFF;
+ --accent-border: #BEDCFF; /* decorative */
+ --accent-border-strong: #007FE2; /* >=3:1 */
+ --accent-text: #0064B5;
+ --accent-on-solid: #FFFFFF;
+
+ --safe-text: #007D61;
+ --safe-bg-subtle: #E8FFF6;
+ --safe-bg-subtle-hover: #D0FCEB;
+ --safe-border: #77F8CF; /* decorative */
+ --safe-border-strong: #009D7B; /* >=3:1 */
+ --safe-solid: #007D61;
+ --safe-solid-hover: #00634C;
+ --safe-solid-active: #004332;
+ --safe-on-solid: #FFFFFF;
+
+ --caution-text: #695000;
+ --caution-bg-subtle: #FFF8E7;
+ --caution-bg-subtle-hover: #FFEFC8;
+ --caution-border: #FFD875; /* decorative */
+ --caution-border-strong: #A68000; /* >=3:1 */
+ --caution-solid: #695000;
+ --caution-solid-hover: #473500;
+ --caution-solid-active: #2D2100;
+ --caution-on-solid: #FFFFFF;
+
+ --danger-text: #6D0020;
+ --danger-bg-subtle: #FFF6F6;
+ --danger-bg-subtle-hover: #FFEAEB;
+ --danger-border: #FFCED1; /* decorative */
+ --danger-border-strong: #E14660; /* >=3:1 */
+ --danger-solid: #BF2145;
+ --danger-solid-hover: #9E0032;
+ --danger-solid-active: #6D0020;
+ --danger-on-solid: #FFFFFF;
+
+ --success-text: #007D61; /* alias of safe */
+ --success-bg-subtle: #E8FFF6;
+ --success-solid: #007D61;
+ --error-text: #6D0020; /* alias of danger */
+ --error-bg-subtle: #FFF6F6;
+ --error-solid: #BF2145;
+ --warning-text: #695000; /* alias of caution */
+ --warning-bg-subtle: #FFF8E7;
+ --info-text: #0064B5;
+ --info-bg-subtle: #EDF5FF;
+
+ --state-hover: #F3F3F3;
+ --state-active: #E8E9EA;
+ --state-selected: #EDF5FF;
+ --state-selected-hover: #DBECFF;
+ --state-selected-border: #007FE2;
+ --state-disabled-bg: #F3F6F9;
+ --state-disabled-border: #D7DCE1;
+ --state-disabled-text: #A5ABB3;
+ --focus-ring: #007FE2;
+ --focus-ring-offset: #FAFBFD;
+ --skeleton-base: #E5E8ED;
+ --skeleton-shine: #F3F6F9;
+
+ --eco-rust: #BB5F00;
+ --eco-java: #954D3F;
+ --eco-homebrew: #777B18;
+ --eco-node: #097200;
+ --eco-go: #0095A1;
+ --eco-docker: #007397;
+ --eco-python: #004E85;
+ --eco-apple: #6D74D8;
+ --eco-dotnet: #712DC3;
+ --eco-jetbrains: #811288;
+ --eco-system: #C63E86;
+
+ --eco-rust-soft: #F7ECE0;
+ --eco-java-soft: #F2EAE8;
+ --eco-homebrew-soft: #EFEFE3;
+ --eco-node-soft: #E1EEE0;
+ --eco-go-soft: #E0F2F4;
+ --eco-docker-soft: #E0EEF3;
+ --eco-python-soft: #E0EAF0;
+ --eco-apple-soft: #EDEEFA;
+ --eco-dotnet-soft: #EEE6F8;
+ --eco-jetbrains-soft: #F0E3F1;
+ --eco-system-soft: #F8E8F0;
+
+ --seq-1: #E5F2FF;
+ --seq-2: #C4E0FD;
+ --seq-3: #9AC7F8;
+ --seq-4: #67A9F0;
+ --seq-5: #2B86E4;
+ --seq-6: #0062BA;
+ --seq-7: #004085;
+ --chart-track: #E5E8ED;
+ --chart-grid: #D7DCE1;
+ --chart-axis: #C2C7CE;
+ --chart-label: #69717A;
+
+ --shadow-1: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.10);
+ --shadow-2: 0 4px 10px -3px rgba(16,24,40,.10), 0 2px 4px -2px rgba(16,24,40,.06);
+ --shadow-3: 0 16px 32px -8px rgba(16,24,40,.14), 0 4px 10px -4px rgba(16,24,40,.07);
+ --shadow-glow: 0 0 0 1px rgba(0,127,226,.28), 0 4px 16px -4px rgba(0,127,226,.28);
+ --shadow-focus: 0 0 0 2px #FAFBFD, 0 0 0 4px #007FE2;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --surface-base: #060B11;
+ --surface-raised: #0F151D;
+ --surface-sunken: #010306;
+ --surface-overlay: #1C232C;
+ --surface-inset: #010306;
+ --surface-scrim: rgba(1, 3, 6, 0.78);
+
+ --border-subtle: #1C232C; /* decorative hairline */
+ --border-default: #283039; /* decorative separator */
+ --border-strong: #69717A; /* >=3:1 — inputs, checkboxes, toggles */
+
+ --text-primary: #F3F6F9;
+ --text-secondary: #A5ABB3;
+ --text-tertiary: #868D95;
+ --text-disabled: #515963; /* WCAG 1.4.3 inactive-control exception */
+ --text-inverse: #010306;
+ --text-on-accent: #010306;
+ --text-code: #D7DCE1;
+ --text-link: #6BB2FF;
+
+ --accent-bg: #389BFF;
+ --accent-bg-hover: #6BB2FF;
+ --accent-bg-active: #007FE2;
+ --accent-subtle: #15283D;
+ --accent-subtle-hover: #18324F;
+ --accent-border: #1D436A; /* decorative */
+ --accent-border-strong: #007FE2; /* >=3:1 */
+ --accent-text: #6BB2FF;
+ --accent-on-solid: #010306;
+
+ --safe-text: #6AECC4;
+ --safe-bg-subtle: #182A2E;
+ --safe-bg-subtle-hover: #1C3334;
+ --safe-border: #2C5A52; /* decorative */
+ --safe-border-strong: #05AB86; /* >=3:1 */
+ --safe-solid: #39C39D;
+ --safe-solid-hover: #54D8B1;
+ --safe-solid-active: #05AB86;
+ --safe-on-solid: #010306;
+
+ --caution-text: #E9B600;
+ --caution-bg-subtle: #25251A;
+ --caution-bg-subtle-hover: #2E2C19;
+ --caution-border: #554914; /* decorative */
+ --caution-border-strong: #B58C00; /* >=3:1 */
+ --caution-solid: #E9B600;
+ --caution-solid-hover: #FEC92F;
+ --caution-solid-active: #D1A200;
+ --caution-on-solid: #010306;
+
+ --danger-text: #EF546C;
+ --danger-bg-subtle: #251B25;
+ --danger-bg-subtle-hover: #2E1E28;
+ --danger-border: #572936; /* decorative */
+ --danger-border-strong: #EF546C; /* >=3:1 */
+ --danger-solid: #EF546C;
+ --danger-solid-hover: #FF7887;
+ --danger-solid-active: #E14660;
+ --danger-on-solid: #010306;
+
+ --success-text: #6AECC4; /* alias of safe */
+ --success-bg-subtle: #182A2E;
+ --success-solid: #39C39D;
+ --error-text: #EF546C; /* alias of danger */
+ --error-bg-subtle: #251B25;
+ --error-solid: #EF546C;
+ --warning-text: #E9B600; /* alias of caution */
+ --warning-bg-subtle: #25251A;
+ --info-text: #6BB2FF;
+ --info-bg-subtle: #15283D;
+
+ --state-hover: #1B2128;
+ --state-active: #252A31;
+ --state-selected: #15283D;
+ --state-selected-hover: #18324F;
+ --state-selected-border: #389BFF;
+ --state-disabled-bg: #1C232C;
+ --state-disabled-border: #283039;
+ --state-disabled-text: #515963;
+ --focus-ring: #6BB2FF;
+ --focus-ring-offset: #060B11;
+ --skeleton-base: #1C232C;
+ --skeleton-shine: #283039;
+
+ --eco-rust: #FF8505;
+ --eco-java: #C37D6F;
+ --eco-homebrew: #A0A645;
+ --eco-node: #3AA831;
+ --eco-go: #31C7D5;
+ --eco-docker: #009AC7;
+ --eco-python: #2174B7;
+ --eco-apple: #AAB4FF;
+ --eco-dotnet: #A676F9;
+ --eco-jetbrains: #A34AAA;
+ --eco-system: #FF89C0;
+
+ --eco-rust-soft: #352719;
+ --eco-java-soft: #2C262A;
+ --eco-homebrew-soft: #262C23;
+ --eco-node-soft: #162D20;
+ --eco-go-soft: #14313A;
+ --eco-docker-soft: #0D2A38;
+ --eco-python-soft: #122436;
+ --eco-apple-soft: #282E41;
+ --eco-dotnet-soft: #272540;
+ --eco-jetbrains-soft: #271D34;
+ --eco-system-soft: #352837;
+
+ --seq-1: #17213D;
+ --seq-2: #233A6E;
+ --seq-3: #2959A2;
+ --seq-4: #2579D4;
+ --seq-5: #1C9DFD;
+ --seq-6: #6DC4FF;
+ --seq-7: #B7E6FF;
+ --chart-track: #1C232C;
+ --chart-grid: #283039;
+ --chart-axis: #515963;
+ --chart-label: #868D95;
+
+ --shadow-1: 0 1px 2px rgba(0,0,0,.72), inset 0 1px 0 rgba(255,255,255,.030);
+ --shadow-2: 0 6px 16px -6px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.040);
+ --shadow-3: 0 20px 44px -12px rgba(0,0,0,.92), inset 0 1px 0 rgba(255,255,255,.055);
+ --shadow-glow: 0 0 0 1px rgba(56,155,255,.32), 0 0 28px -6px rgba(56,155,255,.45);
+ --shadow-focus: 0 0 0 2px #060B11, 0 0 0 4px #6BB2FF;
+ }
+}
+
+:root[data-theme="dark"] {
+ --surface-base: #060B11;
+ --surface-raised: #0F151D;
+ --surface-sunken: #010306;
+ --surface-overlay: #1C232C;
+ --surface-inset: #010306;
+ --surface-scrim: rgba(1, 3, 6, 0.78);
+
+ --border-subtle: #1C232C; /* decorative hairline */
+ --border-default: #283039; /* decorative separator */
+ --border-strong: #69717A; /* >=3:1 — inputs, checkboxes, toggles */
+
+ --text-primary: #F3F6F9;
+ --text-secondary: #A5ABB3;
+ --text-tertiary: #868D95;
+ --text-disabled: #515963; /* WCAG 1.4.3 inactive-control exception */
+ --text-inverse: #010306;
+ --text-on-accent: #010306;
+ --text-code: #D7DCE1;
+ --text-link: #6BB2FF;
+
+ --accent-bg: #389BFF;
+ --accent-bg-hover: #6BB2FF;
+ --accent-bg-active: #007FE2;
+ --accent-subtle: #15283D;
+ --accent-subtle-hover: #18324F;
+ --accent-border: #1D436A; /* decorative */
+ --accent-border-strong: #007FE2; /* >=3:1 */
+ --accent-text: #6BB2FF;
+ --accent-on-solid: #010306;
+
+ --safe-text: #6AECC4;
+ --safe-bg-subtle: #182A2E;
+ --safe-bg-subtle-hover: #1C3334;
+ --safe-border: #2C5A52; /* decorative */
+ --safe-border-strong: #05AB86; /* >=3:1 */
+ --safe-solid: #39C39D;
+ --safe-solid-hover: #54D8B1;
+ --safe-solid-active: #05AB86;
+ --safe-on-solid: #010306;
+
+ --caution-text: #E9B600;
+ --caution-bg-subtle: #25251A;
+ --caution-bg-subtle-hover: #2E2C19;
+ --caution-border: #554914; /* decorative */
+ --caution-border-strong: #B58C00; /* >=3:1 */
+ --caution-solid: #E9B600;
+ --caution-solid-hover: #FEC92F;
+ --caution-solid-active: #D1A200;
+ --caution-on-solid: #010306;
+
+ --danger-text: #EF546C;
+ --danger-bg-subtle: #251B25;
+ --danger-bg-subtle-hover: #2E1E28;
+ --danger-border: #572936; /* decorative */
+ --danger-border-strong: #EF546C; /* >=3:1 */
+ --danger-solid: #EF546C;
+ --danger-solid-hover: #FF7887;
+ --danger-solid-active: #E14660;
+ --danger-on-solid: #010306;
+
+ --success-text: #6AECC4; /* alias of safe */
+ --success-bg-subtle: #182A2E;
+ --success-solid: #39C39D;
+ --error-text: #EF546C; /* alias of danger */
+ --error-bg-subtle: #251B25;
+ --error-solid: #EF546C;
+ --warning-text: #E9B600; /* alias of caution */
+ --warning-bg-subtle: #25251A;
+ --info-text: #6BB2FF;
+ --info-bg-subtle: #15283D;
+
+ --state-hover: #1B2128;
+ --state-active: #252A31;
+ --state-selected: #15283D;
+ --state-selected-hover: #18324F;
+ --state-selected-border: #389BFF;
+ --state-disabled-bg: #1C232C;
+ --state-disabled-border: #283039;
+ --state-disabled-text: #515963;
+ --focus-ring: #6BB2FF;
+ --focus-ring-offset: #060B11;
+ --skeleton-base: #1C232C;
+ --skeleton-shine: #283039;
+
+ --eco-rust: #FF8505;
+ --eco-java: #C37D6F;
+ --eco-homebrew: #A0A645;
+ --eco-node: #3AA831;
+ --eco-go: #31C7D5;
+ --eco-docker: #009AC7;
+ --eco-python: #2174B7;
+ --eco-apple: #AAB4FF;
+ --eco-dotnet: #A676F9;
+ --eco-jetbrains: #A34AAA;
+ --eco-system: #FF89C0;
+
+ --eco-rust-soft: #352719;
+ --eco-java-soft: #2C262A;
+ --eco-homebrew-soft: #262C23;
+ --eco-node-soft: #162D20;
+ --eco-go-soft: #14313A;
+ --eco-docker-soft: #0D2A38;
+ --eco-python-soft: #122436;
+ --eco-apple-soft: #282E41;
+ --eco-dotnet-soft: #272540;
+ --eco-jetbrains-soft: #271D34;
+ --eco-system-soft: #352837;
+
+ --seq-1: #17213D;
+ --seq-2: #233A6E;
+ --seq-3: #2959A2;
+ --seq-4: #2579D4;
+ --seq-5: #1C9DFD;
+ --seq-6: #6DC4FF;
+ --seq-7: #B7E6FF;
+ --chart-track: #1C232C;
+ --chart-grid: #283039;
+ --chart-axis: #515963;
+ --chart-label: #868D95;
+
+ --shadow-1: 0 1px 2px rgba(0,0,0,.72), inset 0 1px 0 rgba(255,255,255,.030);
+ --shadow-2: 0 6px 16px -6px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.040);
+ --shadow-3: 0 20px 44px -12px rgba(0,0,0,.92), inset 0 1px 0 rgba(255,255,255,.055);
+ --shadow-glow: 0 0 0 1px rgba(56,155,255,.32), 0 0 28px -6px rgba(56,155,255,.45);
+ --shadow-focus: 0 0 0 2px #060B11, 0 0 0 4px #6BB2FF;
+}
+
+/* ============================================================================
+ NON-COLOR TOKENS — type, space, radius, motion, elevation.
+ ============================================================================ */
+
+:root {
+ /* Shipping app uses system faces only: the CSP forbids remote origins, and
+ a disk tool should not need the network to render. */
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
+ "Segoe UI", "Noto Sans", Cantarell, Ubuntu, system-ui, sans-serif;
+ --font-mono: ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Mono",
+ "Segoe UI Mono", "Roboto Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
+
+ /* Space — base unit 4px */
+ --space-1: 4px; --space-1-5: 6px; --space-2: 8px; --space-2-5: 10px;
+ --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px;
+ --space-8: 32px; --space-10: 40px; --space-12: 48px;
+
+ --pad-window-x: 16px;
+
+ /* Radius */
+ --radius-xs: 3px; --radius-sm: 5px; --radius-md: 7px;
+ --radius-lg: 10px; --radius-full: 999px;
+
+ /* Heights */
+ --row-h: 44px;
+ --row-h-compact: 32px;
+ --group-h: 36px;
+ --ctl-h-sm: 24px; --ctl-h-md: 30px; --ctl-h-lg: 36px;
+ --iconbtn: 28px;
+ --badge-h: 16px;
+ --header-h: 52px;
+ --toolbar-h: 40px;
+ --statusbar-h: 28px;
+ --colhead-h: 28px;
+ --summary-h: 64px;
+ --rail-w: 56px;
+ --drawer-w: 400px;
+ --checkbox: 14px;
+
+ /* Type scale */
+ --t-metric-lg: 32px/34px; --t-metric-md: 28px/30px; --t-metric-sm: 20px/24px;
+ --t-page-title: 18px/24px; --t-section-title: 13px/16px;
+ --t-body: 13px/18px; --t-body-sm: 12px/16px;
+ --t-label: 12px/16px; --t-caption: 11px/14px; --t-micro: 10px/12px;
+ --t-mono-sm: 12px/17px; --t-mono-xs: 11px/15px;
+
+ /* Motion */
+ --dur-1: 80ms; --dur-2: 120ms; --dur-3: 160ms; --dur-4: 220ms; --dur-5: 320ms;
+ --ease-out: cubic-bezier(0.16, 1, 0.30, 1);
+ --ease-in: cubic-bezier(0.40, 0, 1, 1);
+ --ease-standard: cubic-bezier(0.20, 0, 0, 1);
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+
+ --edge-highlight: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ /* On a near-black ground a drop shadow reads as nothing; a lit top edge
+ is what actually says "raised". */
+ --edge-highlight: inset 0 1px 0 rgb(255 255 255 / 0.055);
+ }
+}
+:root[data-theme="dark"] {
+ --edge-highlight: inset 0 1px 0 rgb(255 255 255 / 0.055);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ :root { --dur-1: 1ms; --dur-2: 1ms; --dur-3: 1ms; --dur-4: 1ms; --dur-5: 1ms; }
+ *, *::before, *::after {
+ animation-duration: 1ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 1ms !important;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/crates/deepclean-app/frontend/index.html b/crates/deepclean-app/frontend/index.html
index bba050d..a1e0c4c 100644
--- a/crates/deepclean-app/frontend/index.html
+++ b/crates/deepclean-app/frontend/index.html
@@ -4,773 +4,13 @@
Void
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
No scan results yet
-
Click "Scan Now" to find cleanable artifacts on your system.
-
-
-
-
-
-
-
-
+
+