diff --git a/docs/superpowers/plans/2026-07-09-interstellar-blackhole-phase1.md b/docs/superpowers/plans/2026-07-09-interstellar-blackhole-phase1.md
new file mode 100644
index 0000000..8a8a317
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-09-interstellar-blackhole-phase1.md
@@ -0,0 +1,2109 @@
+# Interstellar Black Hole Renderer — Phase 1 (Schwarzschild) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** A real-time, cross-platform (desktop + WebGPU) Schwarzschild black-hole renderer in Bevy 0.19 that matches the reference video: black shadow, tilted Doppler accretion disk with lensed halo, lensed starfield, plus optional lensed grid/planets/skybox — all driven by an egui control panel.
+
+**Architecture:** One full-screen quad with a custom `Material2d` whose fragment shader geodesic-ray-traces curved spacetime per pixel and intersects the disk, stars, planets, and grid along the bent path. All tunable state lives in a `BlackHoleParams` `Resource`, mirrored into the material uniform each frame. A `bevy_egui` panel edits the resource live.
+
+**Tech Stack:** Rust 2024, Bevy 0.19, bevy_egui 0.41, WGSL shaders, trunk (web build), WebGPU.
+
+**Spec:** `docs/superpowers/specs/2026-07-09-interstellar-blackhole-design.md`
+
+**Verified API facts (do not deviate):**
+- `Material2d` material bind group is **group 2**; use `#{MATERIAL_BIND_GROUP}` token in WGSL.
+- Imports from `bevy::sprite_render`: `Material2d`, `Material2dPlugin`, `AlphaMode2d`.
+- Component-based spawning: `commands.spawn(Camera2d)` and `(Mesh2d(h), MeshMaterial2d(h))`. No bundles.
+- `fragment_shader()` is a static fn returning `ShaderRef::Path("...".into())`; entry point is `@fragment fn fragment(...)`.
+- WGSL must `#import bevy_sprite::mesh2d_vertex_output::VertexOutput`; `in.position.xy` is pixel coords; `in.uv` is `[0,1]`.
+- Web: enable Bevy `webgpu` feature → `WgpuSettings::default()` auto-selects `BROWSER_WEBGPU`. No `RenderPlugin` backend config. `getrandom` handled transitively.
+- `AsBindGroup`: `#[uniform(0)]` (single arg = binding index), `#[texture(1)]`, `#[sampler(2)]`, `#[storage(3, read_only)]`. A struct field bound as uniform must derive `ShaderType`.
+
+---
+
+## File structure (created across tasks)
+
+```
+Cargo.toml # deps (Task 1)
+Trunk.toml # web build (Task 3)
+web/index.html # web entry (Task 3)
+assets/shaders/
+ common.wgsl # structs + constants (Task 7)
+ stars.wgsl # procedural stars (Task 11)
+ disk.wgsl # disk + Doppler (Task 12)
+ planets.wgsl # sphere intersection (Task 14)
+ grid.wgsl # Flamm grid (Task 15)
+ skybox.wgsl # cubemap sampling (Task 16)
+ geodesic_schwarzschild.wgsl # RK4 integrator (Task 9)
+ black_hole.wgsl # entry point (Task 8, grows each task)
+src/
+ main.rs # app + plugin wiring
+ params.rs # BlackHoleParams resource (Task 6)
+ camera.rs # orbit controller (Task 5)
+ ui.rs # egui panel (Task 17)
+ web.rs # wasm glue (Task 3)
+ physics.rs # CPU-mirrored math + unit tests (Task 10)
+ scene/mod.rs, disk.rs, planets.rs # scene helpers
+ render/
+ mod.rs
+ plugin.rs # BlackHolePlugin (Task 4)
+ material.rs # BlackHoleMaterial + BlackHoleUniforms (Task 7)
+tests/physics_test.rs # (Task 10)
+```
+
+---
+
+## Task 1: Project dependencies
+
+**Files:**
+- Modify: `Cargo.toml`
+
+- [ ] **Step 1: Write the dependency block**
+
+Replace the entire contents of `Cargo.toml` with:
+
+```toml
+[package]
+name = "singularity-rs"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+bevy = "0.19"
+bevy_egui = "0.41"
+
+# Web-only deps for WebGPU detection + fallback message.
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+web-sys = { version = "0.3", features = ["Gpu", "Navigator", "Window", "Document", "HtmlElement", "Element"] }
+wasm-bindgen = "0.2"
+
+# Smaller wasm binary in release web builds (per Bevy examples README).
+[profile.wasm-release]
+inherits = "release"
+opt-level = "z"
+lto = "fat"
+codegen-units = 1
+```
+
+- [ ] **Step 2: Verify it resolves**
+
+Run: `cargo check`
+Expected: compiles with no errors (downloads bevy + bevy_egui; may take a few minutes the first time).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add Cargo.toml Cargo.lock
+git commit -m "build: add bevy 0.19 and bevy_egui 0.41 dependencies"
+```
+
+---
+
+## Task 2: Minimal Bevy window opens (smoke test)
+
+**Files:**
+- Modify: `src/main.rs`
+
+- [ ] **Step 1: Write a minimal app that opens a window**
+
+Replace `src/main.rs` with:
+
+```rust
+use bevy::prelude::*;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .add_systems(Startup, setup)
+ .run();
+}
+
+fn setup(mut commands: Commands) {
+ commands.spawn(Camera2d);
+}
+```
+
+- [ ] **Step 2: Run it and confirm a blank window opens**
+
+Run: `cargo run`
+Expected: a Bevy window titled "App" opens with a black background and no panics. Close it with the window close button.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main.rs
+git commit -m "feat: minimal bevy window opens"
+```
+
+---
+
+## Task 3: Web build works (Trunk + WebGPU + fallback)
+
+**Files:**
+- Create: `Trunk.toml`
+- Create: `web/index.html`
+- Create: `src/web.rs`
+- Modify: `src/main.rs`
+
+- [ ] **Step 1: Create `Trunk.toml`**
+
+```toml
+[build]
+target = "web/index.html"
+dist = "dist"
+
+[serve]
+address = "127.0.0.1"
+port = 8080
+open = false
+```
+
+- [ ] **Step 2: Create `web/index.html`**
+
+```html
+
+
+
+
+ singularity-rs
+
+
+
+
+
+
+```
+
+- [ ] **Step 3: Create `src/web.rs` with WebGPU check + fallback**
+
+```rust
+#[cfg(target_arch = "wasm32")]
+pub fn webgpu_available() -> bool {
+ web_sys::window()
+ .and_then(|w| w.navigator())
+ .and_then(|n| n.gpu())
+ .is_some()
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn show_fallback_message() {
+ if let Some(window) = web_sys::window() {
+ if let Some(document) = window.document() {
+ if let Some(body) = document.body() {
+ let el = document.create_element("div").unwrap();
+ el.set_inner_text(
+ "WebGPU is not available in this browser. \
+ Please use a recent version of Chrome, Edge, or Firefox.",
+ );
+ el.set_attribute(
+ "style",
+ "position:fixed;inset:0;display:flex;align-items:center;\
+ justify-content:center;font-family:sans-serif;font-size:1.5rem;\
+ text-align:center;padding:2rem;background:#111;color:#eee;",
+ )
+ .ok();
+ body.append_child(&el).ok();
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Wire the check into `main` and enable canvas-fit on web**
+
+Replace `src/main.rs` with:
+
+```rust
+use bevy::prelude::*;
+use bevy::window::WindowPlugin;
+
+#[cfg(target_arch = "wasm32")]
+mod web;
+
+fn main() {
+ // On web, abort startup if WebGPU isn't available and show a message.
+ #[cfg(target_arch = "wasm32")]
+ {
+ if !web::webgpu_available() {
+ web::show_fallback_message();
+ return;
+ }
+ }
+
+ App::new()
+ .add_plugins(DefaultPlugins.set(WindowPlugin {
+ primary_window: Some(Window {
+ title: "singularity-rs".into(),
+ // On web, make the canvas track the browser window size.
+ fit_canvas_to_parent: true,
+ ..default()
+ }),
+ ..default()
+ }))
+ .add_systems(Startup, setup)
+ .run();
+}
+
+fn setup(mut commands: Commands) {
+ commands.spawn(Camera2d);
+}
+```
+
+- [ ] **Step 5: Verify desktop still builds and runs**
+
+Run: `cargo run`
+Expected: window opens, title is "singularity-rs".
+
+- [ ] **Step 6: Install trunk and the wasm target**
+
+Run:
+```bash
+cargo install --locked trunk
+rustup target add wasm32-unknown-unknown
+```
+Expected: `trunk` installed; wasm target added.
+
+- [ ] **Step 7: Verify the web build serves**
+
+Run: `trunk serve`
+Expected: serving on `http://127.0.0.1:8080`. Open it in Chrome/Edge → a black canvas fills the window (the `Camera2d` with no content). Check the browser console for no errors. Stop with Ctrl-C.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add Trunk.toml web/index.html src/web.rs src/main.rs
+git commit -m "feat: web build via trunk with WebGPU detection + fallback"
+```
+
+---
+
+## Task 4: BlackHolePlugin scaffold + full-screen quad
+
+**Files:**
+- Create: `src/render/mod.rs`
+- Create: `src/render/plugin.rs`
+- Modify: `src/main.rs`
+
+This task creates the plugin and spawns a full-screen quad whose material renders a flat color, proving the full-screen-shader pipeline works before any physics.
+
+- [ ] **Step 1: Create `src/render/mod.rs`**
+
+```rust
+pub mod plugin;
+pub mod material;
+
+pub use plugin::BlackHolePlugin;
+```
+
+- [ ] **Step 2: Create `src/render/material.rs` (placeholder flat-color material)**
+
+```rust
+use bevy::prelude::*;
+use bevy::reflect::TypePath;
+use bevy::render::render_resource::AsBindGroup;
+use bevy::sprite_render::Material2d;
+
+#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
+pub struct BlackHoleMaterial {
+ #[uniform(0)]
+ pub time: f32,
+}
+
+impl Material2d for BlackHoleMaterial {
+ fn fragment_shader() -> bevy::sprite_render::ShaderRef {
+ "shaders/black_hole.wgsl".into()
+ }
+}
+```
+
+- [ ] **Step 3: Create `src/render/plugin.rs`**
+
+```rust
+use bevy::prelude::*;
+use bevy::sprite_render::Material2dPlugin;
+
+use super::material::BlackHoleMaterial;
+
+pub struct BlackHolePlugin;
+
+impl Plugin for BlackHolePlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugins(Material2dPlugin::::default())
+ .add_systems(Startup, spawn_fullscreen_quad)
+ .add_systems(Update, update_time);
+ }
+}
+
+fn spawn_fullscreen_quad(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ mut materials: ResMut>,
+) {
+ // A large quad that covers the camera's orthographic view.
+ commands.spawn((
+ Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
+ MeshMaterial2d(materials.add(BlackHoleMaterial { time: 0.0 })),
+ // Camera2d default projection spans [-1,1] in x; scale quad to fill.
+ Transform::default(),
+ ));
+}
+
+fn update_time(time: Res