HEAL came back a stain larger and softer than the stain it was asked to
remove, and the model's half of it answered in a colour that appears nowhere
near the repair. Three causes, all inside `engine/inpaint.ts`:
- The fallback's reading ring was the hole dilated by two pixels whatever the
brush, so the boundary of the fill was the dirt's own penumbra and the fill
came back as the dirt. The pad is now proportional to the hole's own radius
(`rimPad`), the fill is an inverse-square average of the clean ring relaxed
into one field by 36 Jacobi rounds, and the hole's grain is read against a
2px box of the clean pixels and taken from the pixel it mirrors through its
own middle. The texture half before this was a no-op — `detail = rgba[p] -
fR[idx]` with `fR[idx]` the same pixel's own colour, so every donor handed
over zero and the hole was the smooth field alone (15.6% of the ground's
detail). Measured on a stain of radius 10 with a halo to 16 under a 12px
brush (`probe-fallback.mjs`): the repair landed 22.3 levels below the ground
it sits in, and comes back 0.06 off it.
- The plate the model reads under the mask was the mean of every pixel of the
crop, so the network met a hard step at the mask's own edge and answered in
that colour. It is now the rim's own light spread inward (`cropPlate`), and
the mask is downsampled by "any hole pixel in this cell" rather than by a
corner sample. Step across the mask's edge: 69.3 levels before, 9.5 after.
- The same repair came back different every time the reader zoomed out of it:
the crop was sized from the stage's own pixels on the zoom ladder
(`PREVIEW_QUANTUM`), so each zoom handed the model a different grid, and
every grid invents its own pixels. The crop's box is now its share of the
FRAME (`INPAINT_REF_LONG`, `exportEngine` 6e), so one spot is one grid at
every zoom and at the export's own size.
The patch is deliberately not baked into the photo: a recipe stays reusable on
other frames and the repair stays a pass. ponytail: the pixels handed to the
model are still read from whatever the stage rendered at, so the answer is the
same repair to within the resampling of its input; a patch cache keyed on the
frame's identity and the spots is the rung that makes it identical, and the one
to take if a zoom still shows a repair move.
VIGNETTING now runs both ways (-10..10): down darkens the four corners, up
lifts the lens' own falloff out of them with a white ink — the same curve and
the same weight. EV was defined and live on the phone but absent from the wide
panel's own Light row, which is `DevelopPanels.tsx` and nothing else.
`npx tsc --noEmit`, `vite build`, `heal-check`, `studio-drag` and the other
suites pass. `studio-export-check` fails one assertion here on the code at HEAD
as well — a frame left behind files its waiting look 9s after the pick — so it
is not this change.
The phone screen was painted in literals — `#101014`, `#f4f4f5` and forty more
— so it stayed dark whatever theme was set: pick Light, press Mobile Cloud,
and the reader is handed a black page with white ink.
Every surface and ink in `styles/mobile-library.css` is now one alias
(`--ml-bg`, `--ml-text`, …) that reads the theme's own tokens, the faint
linings and fills are a `color-mix` of the ink at the alpha they already
carried, and the amber the file already asked `var(--accent)` for is now what
the star, the pressed pill and the selected frame wear too. Only the four
status inks carry a light-theme override, the tokens having no word for them.
The five inline dark styles left in `MobileLibrary.tsx` read the same aliases.
Measured on the running container at 390×800, Light chosen on the desktop: the
phone was `rgb(16,16,20)` on ink `rgb(244,244,245)` whichever theme was set;
it is now `rgb(255,255,255)` on `rgb(20,24,29)` under Light and `rgb(0,0,0)` on
`rgb(242,243,245)` under Dark, held through the handover and on the way back. A
contrast sweep of the grid and the reader in both themes leaves nothing under
3:1 but the tick glyph, which its own box draws.
`mobile-pick-check` asked for one frozen amber; it asks for the accent the app
is using, which is what that outline has become.
The column files a local folder as an absolute key — `/2012` — while a frame
carries its own `dir` as a path from the root, `2012`. `shown` set the two
against each other as they stood, so a frame's dir was never the node's dir, and
never under it either: every folder in the disk tree was counted in the row
above it and stood in no pane at all — the wall white, the frames nowhere.
The key sheds the slash it leads with before the two are set against each other,
the way `markOf` already cleans a reading's own path.
Measured on the live account's own catalogue — 112761 frames, 6 under `/2012`:
the node matched 0 frames before, 6 after; the root's own list is untouched
(927).