432acba9c1
The column of mask knobs belongs to an armed LINEAR or RADIAL shape, and it stayed on the stage after the hand had moved on: arm a shape, draw it, click another chip or another tab, and the strip of mask sliders was still there belonging to a tool that was no longer in hand. A capture-phase `pointerdown` on `window`, armed only while `maskTool` is set, now puts the tool down in the same gesture that reaches for something else. A press on the rail (the tabs) or on any chip except the shape's own two dismisses; a press inside the column is left alone, because the column's own chips handle their click themselves, and a press on the photo is left alone, because dragging on the photo is how the shape is drawn. It is the dismissal the STRAIGHTEN tool already had, one effect over, so the tab-switch effect needed no `setMaskTool(null)` of its own. A RAW's blown highlight came out magenta, and was measured before it was touched. `example-sony.ARW` through the lab (`rawblow.html`, the same camera white/black and the same `rgb_cam` the app's develop uses): white 16380, black 512, cam_mul green-normalised to (2.581, 1, 1.553). The pixels the sensor could not hold — 0.7% of the frame, raw max channel at or past 0.99 of the white level — average (0.775, 1.416, 1.108) in raw, and per channel 26.3% / 96.0% / 46.7% are at or over white: green is 1.4x the white level while red is still under it. The develop shader clamped every channel to 1.0 BEFORE the white-balance gains, and that one clamp is the whole cast. Green is the channel the gains are normalised to, so it stopped at 1.0, while red and blue — which need their 2.581 and 1.553 — were already past it and were carried over by the multiply. The blown area therefore left the matrix at (1.0, 0.53, 0.62) instead of at white: measured on the develop output, (254.4, 217.0, 242.9) — red and blue 38 above green, which is magenta. On the stage, pixels with red and blue over 235 and green under 225 in the same framing: 1191 with the clamp, 33 without it. The shader now only floors at zero, keeps the channel ratios through the matrix, and fades whatever ran past white towards white (`mix(rgb / mx, 1, 1 - 1/mx)`, the desaturate-to-white dcraw uses for the same problem). The blown area comes out (254.5, 253.9, 246.6): an overflow that stays bright and stops taking a hue, broken per channel at sd 8.7 / 28.0 / 26.4 today against 5.4 / 3.3 / 17.8 now. Whole-frame averages move by 0.008/0.278/0.140 of a level — the fade only touches pixels that were over white, which are the 0.7%. "cannot be rescued" is the second half of the same fact, and it is now a measurement rather than a hope: LIGHT's HIGHLIGHT row is a curve over what develop emitted, and while red and blue were pinned at 255 the curve had nothing to pull on. The fade leaves a compressed ramp there instead, which is what the row now pulls. LibRaw's own reconstruction modes are not the answer on this file: `-H` 1, 2 and 3 hand back byte-identical develop output to `-H` 0 (`pxAt65535` is 0 — the sensor never reached its 65535, only the camera's white level), so `SETTINGS.highlight` stays 0. What the app cannot do is keep the two stops above white, because the band still leaves develop as 8-bit JPEG; that ceiling is named at the point of the fade, for whoever needs RAW highlights recovered rather than merely correct. `npm run typecheck` and `npm run build` are clean (bundle `index-BJy_3HCz.js`). Probes: verify-mask-column (dev server, real photo, arm a shape and draw it, then reach for another chip and for LIGHT and FX — 8 checks, 8 pass: the column stands while the shape is armed, survives a press on the photo and on the shape's own kind chips, and goes on any other chip or tab), rawblow (the real ARW through the real develop maths in the page, before/after chains side by side, which is where the magenta and the reconstruction modes were measured), probe-raw-highlight (the app itself, ARW uploaded, blown pixels counted and the HIGHLIGHT row driven to both ends).