diff --git a/CLAUDE.md b/CLAUDE.md
index ede7e2e..1b834f9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -85,7 +85,7 @@ precise than anything you can infer.
| Term / acronym definitions | `reference/glossary.yaml` |
| The ORIGINAL 1995 player manual (controls, per-mech stats, coolant loops) | `reference/manual/Tesla40_BT_manual.pdf` (+ alignment audit in `context/pod-hardware.md` §Manual) |
| The complete verbatim detail (fallback) | `docs/PROGRESS_LOG.md` (the old 2236-line CLAUDE.md) |
-| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md`, `docs/RESPAWN_REARM_PLAN.md`, `docs/KD_SCOREBOARD_PLAN.md`, `docs/DIST_LAYOUT_PLAN.md` |
+| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md`, `docs/RESPAWN_REARM_PLAN.md`, `docs/KD_SCOREBOARD_PLAN.md`, `docs/DIST_LAYOUT_PLAN.md`, `docs/MOUSELOOK_PLAN.md` |
---
diff --git a/docs/CONTROLS.html b/docs/CONTROLS.html
index 3ef800c..b593689 100644
--- a/docs/CONTROLS.html
+++ b/docs/CONTROLS.html
@@ -743,6 +743,7 @@
| Buttons | 0x00 – 0x47 |
| No function | 0x16 0x17 0x19 0x1E 0x1F 0x38–0x3E — the pod never wired these |
| Key names | A–Z · 0–9 · F1–F12 · NUMPAD0–9 · NUMPADDOT · UP DOWN LEFT RIGHT · SPACE · LSHIFT LCTRL LALT · MINUS EQUALS COMMA PERIOD LBRACKET RBRACKET BACKTICK |
+ | Mouse | MOUSE4 · MOUSE5 · MOUSEMIDDLE — buttons only; left/right also press cockpit buttons |
| Pad | A B X Y · DPAD_UP/DOWN/LEFT/RIGHT · LB RB · BACK START · LS RS |
| Pad axes | LX LY RX RY LT RT |
diff --git a/docs/CONTROLS.md b/docs/CONTROLS.md
index 136a058..ece9804 100644
--- a/docs/CONTROLS.md
+++ b/docs/CONTROLS.md
@@ -82,6 +82,16 @@ control in turn — stick, twist, throttle, fire buttons — writes the bindings
the solo menu to try them. It only rewrites its own section of `bindings.txt`, so hand edits
elsewhere in the file survive.
+**Mouse buttons** are bindable like any key — `MOUSE4`, `MOUSE5` (the side buttons),
+`MOUSEMIDDLE`, and `MOUSELEFT`/`MOUSERIGHT`. The side and middle buttons are the free ones; left
+and right are how you press cockpit buttons, so binding those fires on every panel click too.
+Mouse *movement* is not bindable — see `docs/MOUSELOOK_PLAN.md`.
+
+```
+key MOUSE4 button 0x40 # side button = main trigger
+key MOUSE5 button 0x46 # side button = middle thumb
+```
+
**RGB keyboards** with Windows Dynamic Lighting mirror the panel: any key you have bound to a
lamp button glows in the panel's own colours and flashes in step with the on-screen buttons. Turn
it off with `BT_KEYLIGHT=0` in `environ.ini`.
diff --git a/docs/MOUSELOOK_PLAN.md b/docs/MOUSELOOK_PLAN.md
new file mode 100644
index 0000000..009b8c8
--- /dev/null
+++ b/docs/MOUSELOOK_PLAN.md
@@ -0,0 +1,130 @@
+# MOUSELOOK_PLAN — scope: mouse movement as a cockpit axis
+
+**Status: SCOPED, NOT IMPLEMENTED.** Written 2026-07-26 at the author's request, alongside the
+mouse-BUTTON support that did ship (4.11.565). Read §2 before agreeing to build it — the hard
+part is not reading the mouse.
+
+---
+
+## 1. What shipped, and what this is instead
+
+**Shipped:** mouse *buttons*, as key names (`MOUSE4`, `MOUSE5`, `MOUSEMIDDLE`, `MOUSELEFT`,
+`MOUSERIGHT`). Zero new machinery — Win32 hands mouse buttons out as virtual keys and the binding
+path already stores VKs and polls `GetAsyncKeyState`, so five table entries were the whole change.
+
+**This document:** mouse *movement* driving `JoystickX`/`JoystickY` (torso twist and aim) — the
+thing a modern player means by "mouse look". That is a real feature, not a table entry, for three
+reasons that have nothing to do with reading the device.
+
+## 2. The three real problems
+
+### 2.1 The cursor is already spoken for ⚠ (the blocking one)
+
+The glass cockpit's whole premise is that **every one of the 72 pod buttons is a mouse target** —
+left-click presses, right-click latches, and under the exploded layout there are seven separate
+windows to click in. Mouse-look wants the opposite: a captured, hidden, recentred cursor that
+never touches a button.
+
+These cannot both be true at once, so the feature is really a **mode question**, and the answer
+decides the whole design:
+
+| Model | Feel | Cost |
+|---|---|---|
+| **A. Hold-to-look** (`key MOUSE5 mouselook` or a modifier) | Cursor stays free; press to steer, release to click buttons | No mode confusion, discoverable, but you cannot look and hold a trigger comfortably |
+| **B. Toggle** (a key flips captured/free) | Full mouse-look while engaged | Needs a visible on-screen indicator or players get stuck "unable to click anything" |
+| **C. Region** (look while the pointer is over the 3D view, click when over a display) | No mode at all | Fails in the exploded layout (displays are separate windows) and near the surround's corner MFDs, which overlap the view by design |
+| **D. Always captured**, panel clicks via a "release" key | Cleanest for a shooter | Throws away the cockpit's defining interaction; **not recommended** |
+
+**Recommendation: A, with B as an option.** A costs nothing when unbound and cannot strand a
+player. It is also the only one that behaves identically in the surround and exploded layouts.
+
+### 2.2 The pod's channels are POSITIONAL; a mouse is not
+
+`JoystickX`/`JoystickY` are absolute stick deflections in −1..1 that `MechControlsMapper`
+interprets per control mode. A mouse produces *deltas*. Bridging that is a design choice, not a
+conversion:
+
+- **Accumulate** (recommended): integrate deltas into the channel, clamp to ±1 — a "virtual
+ stick". Reuses every existing rule (composition, the #36 release edge, the gait detent).
+- **Rate**: treat the delta as a velocity of torso angle. Feels better for aiming, but the mapper
+ has no rate input — it would mean a new channel semantic and a new path through the mapper.
+ Bigger and less authentic.
+
+With Accumulate, the stick does not spring back (a mouse has no centre). Options, all cheap:
+a `spring ` trailing option that decays toward 0, and/or a recentre key (0x42 torso centre
+is already bound to the Up arrow).
+
+### 2.3 Control mode changes what it means
+
+BASIC steers with the stick; MID/ADV twist the torso with it and steer with the pedals
+([[experience-levels]], [[pod-hardware]]). So mouse-look **steers the mech in BASIC and aims the
+torso in MID/ADV** — authentic, but it will read as a bug to anyone who hasn't been told. The
+docs must say so, and the feature is most useful in MID/ADV.
+
+## 3. Proposed shape (if we build it)
+
+New row type — the sanctioned way to extend the grammar (old builds skip unknown rows with a
+logged warning, so a player's file stays forward-compatible):
+
+```
+mouseaxis axis [invert] [sensitivity ] [spring ] [deadzone ]
+key mouselook # hold-to-look (model A)
+```
+
+Example default (shipped commented out):
+
+```
+#mouseaxis X axis JoystickX sensitivity 1.0 spring 0
+#mouseaxis Y axis JoystickY sensitivity 1.0 spring 0 invert
+#key MOUSE5 mouselook
+```
+
+### Where the code goes
+
+| Piece | Where | Notes |
+|---|---|---|
+| Raw delta capture | `L4PADRIO` (new), fed from the window proc | **`WM_INPUT` (RawInput)**, not `WM_MOUSEMOVE`: relative motion, no cursor clamping at screen edges, no pointer acceleration. Register `HID_USAGE_GENERIC_MOUSE` with `RIDEV_INPUTSINK` off (foreground only). |
+| Capture/hide | same | `SetCapture` + `ShowCursor(FALSE)` + recentre per frame while engaged; release on focus loss |
+| Bindings | `L4PADBINDINGS` | `mouseAxisBindings[]` + the `mouselook` action, parallel to `padAxisBindings` |
+| Channel write | `PadRIO::Poll` | Beside the pad/joy axis writes, with the SAME per-binding previous-value array so it inherits the #36 release edge and the #24 disconnect rule |
+| Docs | `bindings.txt` header, `docs/CONTROLS.md`, `docs/CONTROLS.html` | including the control-mode caveat |
+
+Estimated size: ~250 lines plus docs. No engine or reconstruction changes — this is entirely in
+the glass desktop layer, like PadRIO itself.
+
+## 4. Authenticity note
+
+**The pod had no mouse.** There is nothing to reconstruct here and no binary behaviour to be
+faithful to — this is a desktop-convenience layer, exactly like PadRIO and the glass cockpit
+itself. That is fine (the layer exists precisely so developers and players can fly without a
+cabinet), but it means every decision above is a *design* decision and should be judged on feel,
+not fidelity. It also means it must stay out of the pod build's path: gate it the way PadRIO is
+gated, so `BT_PLATFORM=pod` never sees it.
+
+## 5. Verification plan
+
+1. `mouseaxis` rows parse; unknown-option rows warn and skip (grammar-compatibility rule).
+2. Hold-to-look engages/releases; cursor visible again on release AND on focus loss (the #24
+ disconnect rule, mouse edition).
+3. Channel values track motion, clamp at ±1, and release cleanly to 0 (the #36 latch bug is
+ exactly the hazard here).
+4. **Panel clicks still work** while a mouselook binding exists but is not engaged — in BOTH the
+ surround and exploded layouts. This is the regression that matters.
+5. Control-mode sweep: BASIC steers, MID/ADV twists — confirmed against `[mppr]` trace.
+6. Pod + dev profiles: no mouse path constructed at all.
+
+## 6. Open questions
+
+1. Model A or B (§2.1)? Everything else follows from it.
+2. Should mouse-look imply a crosshair/reticle change, so the player can see it is engaged?
+3. Does the exploded layout need its own answer, given the seven windows each have their own
+ client area and the main window may not have focus?
+4. Is this wanted for the tester builds at all, or only for the operator/dev seat? It is not
+ pod-authentic, and the cockpit's mouse-click identity is a feature people have praised.
+
+## Key Relationships
+
+- Extends: [[glass-cockpit]] (PadRIO, the desktop input layer) · Constrained by: [[pod-hardware]]
+ (the RIO channel model), [[experience-levels]] (control modes)
+- Prior art in-tree: `L4JOY` (DirectInput layer + the wizard) is the closest analogue for adding
+ a whole new input device cleanly.
diff --git a/engine/MUNGA_L4/L4PADBINDINGS.cpp b/engine/MUNGA_L4/L4PADBINDINGS.cpp
index 08cf9ce..af68103 100644
--- a/engine/MUNGA_L4/L4PADBINDINGS.cpp
+++ b/engine/MUNGA_L4/L4PADBINDINGS.cpp
@@ -61,8 +61,42 @@ static const NamedValue keyNames[] =
{ "BACKTICK", VK_OEM_3 }, { "LBRACKET", VK_OEM_4 },
{ "BACKSLASH", VK_OEM_5 }, { "RBRACKET", VK_OEM_6 },
{ "QUOTE", VK_OEM_7 },
+ //
+ // MOUSE BUTTONS (2026-07-26). Win32 hands these out as virtual keys, and
+ // the whole binding path already stores VKs and reads them with
+ // GetAsyncKeyState -- so they need no verb, no parser change and no new
+ // machinery, just a name. `key MOUSE4 button 0x40` works exactly like any
+ // other key row, axes included.
+ //
+ // ⚠ MOUSELEFT/MOUSERIGHT are how the player presses cockpit buttons (left
+ // = press, right = latch). The poll is focus-guarded but not click-aware,
+ // so binding either ALSO fires on every panel click. They are named here
+ // for completeness; the middle and side buttons are the free ones.
+ //
+ { "MOUSELEFT", VK_LBUTTON }, { "MOUSERIGHT", VK_RBUTTON },
+ { "MOUSEMIDDLE", VK_MBUTTON },
+ { "MOUSE4", VK_XBUTTON1 }, { "MOUSE5", VK_XBUTTON2 },
+ { "MOUSEX1", VK_XBUTTON1 }, { "MOUSEX2", VK_XBUTTON2 },
};
+//
+// A mouse button rather than a keyboard key? (Used where a binding only
+// makes sense for a real key -- the RGB lamp mirror, which paints keycaps.)
+//
+static int
+ IsMouseVirtualKey(int virtualKey)
+{
+ return (virtualKey == VK_LBUTTON || virtualKey == VK_RBUTTON ||
+ virtualKey == VK_MBUTTON || virtualKey == VK_XBUTTON1 ||
+ virtualKey == VK_XBUTTON2) ? 1 : 0;
+}
+
+int
+ BTPadBindingIsMouseKey(int virtualKey)
+{
+ return IsMouseVirtualKey(virtualKey);
+}
+
static const NamedValue padButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A },
@@ -202,6 +236,10 @@ static const char *defaultProfileText =
"# key axis set jump/detent\n"
"# pad button \n"
"# padaxis axis [invert] [slew ]\n"
+"# may be a MOUSE BUTTON: MOUSEMIDDLE, MOUSE4, MOUSE5 (the two\n"
+"# side buttons; MOUSEX1/MOUSEX2 are aliases). MOUSELEFT and MOUSERIGHT\n"
+"# also work but are how you press cockpit buttons -- bind them and they\n"
+"# fire on every panel click too. Mouse MOVEMENT is not bindable.\n"
"# Channels: Throttle JoystickX JoystickY LeftPedal RightPedal Turn\n"
"# (Turn = signed composite: + drives the right pedal, - the left --\n"
"# made for mapping a gamepad stick to turning)\n"
@@ -240,6 +278,10 @@ static const char *defaultProfileText =
"key RALT button 0x3F\n"
"\n"
"# --- the four mappable fire buttons ------------------------------------\n"
+"# The mouse's side buttons are free if you want them on the trigger:\n"
+"# key MOUSE4 button 0x40\n"
+"# key MOUSE5 button 0x46\n"
+"# key MOUSEMIDDLE button 0x41\n"
"key SPACE button 0x40 # main trigger\n"
"key NUMPAD0 button 0x40 # main trigger\n"
"key NUMPAD1 button 0x45 # pinky\n"
diff --git a/engine/MUNGA_L4/L4PADRIO.cpp b/engine/MUNGA_L4/L4PADRIO.cpp
index 13b2c00..40229a0 100644
--- a/engine/MUNGA_L4/L4PADRIO.cpp
+++ b/engine/MUNGA_L4/L4PADRIO.cpp
@@ -230,6 +230,14 @@ PadRIO::PadRIO():
{
continue;
}
+ // A keyboard lamp array has no mouse buttons to paint.
+ {
+ extern int BTPadBindingIsMouseKey(int virtualKey);
+ if (BTPadBindingIsMouseKey(binding.virtualKey))
+ {
+ continue;
+ }
+ }
Logical duplicate = False;
for (int j = 0; j < count; ++j)
{