diff --git a/PROJECT.md b/PROJECT.md
index 07f412b..81f2801 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -378,6 +378,11 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
(`HUD`, in portrait logical px — landscape has a shorter band, so the rule is conservative
there rather than over-eager).
Result: 92 of 101 raisers below the board's bottom edge, against 103 of 134 before.
+- **A raise must be ARMS OVER STAND, and `_crowdpass.py` tests all three words.** The rolls
+ repaint clothing as well as posing people, so "was stand, is now person" also catches a
+ cream shirt that came back a shade brighter (`PALE_MAX`) and a genuine arm drawn over
+ something already bright, where raising him DELETES a white block (`COVER_MAX`). Both flash
+ exactly as hard as a real error would.
- **A raiser is ~8x6 logical px, so a pose that appears and holds is invisible.** What reads as
applause is the CYCLE — arms high, hands together, up, down, at about 3.5 Hz — and how many people
are doing it at once (`crowd.pop`). Oliver on the first per-person cut: "when clapping I barely see
@@ -401,6 +406,13 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history.
## Settled feel decisions — do not regress silently
+- **ANYTHING DRAWN OVER THE CACHED STAND MUST CARRY THE STAND'S OWN TREATMENTS.** There are two —
+ `standDim` and `topFade` — and a raiser box drawn with one of them missing lights up as a bright
+ rectangle every time that spectator stands. That is what the "white flashing in the upper right
+ corner of the crowd" was (2026-08-21): the top fade was baked into the backdrop and the live
+ raisers were not repainting it. Both now come from one function each, called from `paintBackdrop`
+ and from `drawRaisers`. The gradient is built in USER space, so filling any sub-rect with it picks
+ up the right alpha for free. Two copies of either number is the same bug waiting again.
- **The frame is dark at BOTH ends.** The run-off apron already took the bottom of the screen down
to near-black below the near baseline; `TUNE.standTop` does the same at the top, so the stand fades
UP out of the dark instead of being cut off at y = 0. Oliver, 2026-08-21: "the bottom of the screen
diff --git a/assets/_crowdpass.py b/assets/_crowdpass.py
index e6dedc0..c4458b4 100644
--- a/assets/_crowdpass.py
+++ b/assets/_crowdpass.py
@@ -76,6 +76,19 @@ HUD = [(6, 6, 156, 62), # scoreboard + the GAME/SETS panel under it
(170, 6, 264, 30)] # the BLOOD panel, clear of the quit chip
MIN_AREA, MAX_AREA = 150, 3000 # an arm blob: one pair of raised arms, or one arm
+# ...AND IT HAS TO BE ARMS GOING UP OVER STAND. Oliver: "what is this white flashing while applause
+# in the upper right corner of the crowd" — and with the photographers switched off entirely it was
+# still there, so it was never them. Two different blobs were doing it, and neither is a raise:
+# PALE SWAPS — the roll repainted somebody's cream shirt, and `stand()` (which only asks whether a
+# pixel matches its row's modal colour) called the old one stand. Nothing rises; a white block
+# just changes pattern at 3.5 Hz.
+# BRIGHT COVER-UPS — a genuine arm, but drawn over something ALREADY BRIGHT (a white shirt in the
+# row behind, lum 239). Raising him deletes a white block and sitting him down brings it back,
+# which flashes exactly as hard as adding one would.
+# So a blob must be warm-ish, must not be a pale block, and must ADD light to something dark.
+PALE_MAX = 0.35 # of the blob may be bright-and-colourless (a cream shirt)
+COVER_MAX = 150 # ...and what it covers in the seated frame must be darker than this
+LIFT_MIN = 12 # ...by at least this much: arms over wall, never over a white shirt
ARM_H, ARM_W = (10, 70), (8, 90) # ...and its shape, in px of the 1408x768 sheet
BAND = 8 # rows the background colour is sampled over
PAD = 3 # px of margin around a raiser's box
@@ -127,6 +140,13 @@ def raisers(A, B):
# sheet; arms that rise out of empty stand belong to nobody and read as floating hands.
if bgA[sl[0].stop:min(A.shape[0], sl[0].stop + 6), sl[1]].mean() > 0.55:
continue
+ # ...AND IT HAS TO BE A RAISE (see PALE_MAX / COVER_MAX / LIFT_MIN above).
+ m = lab[sl] == i + 1
+ px, was = B[sl][m].astype(int), A[sl][m].astype(int)
+ if ((abs(px[:, 0] - px[:, 2]) < 26) & (px[:, 0] > 140)).mean() > PALE_MAX:
+ continue
+ if was.mean() > COVER_MAX or px.mean() - was.mean() < LIFT_MIN:
+ continue
out.append([sl[0].start, sl[1].start, sl[0].stop, sl[1].stop, i + 1])
return out, lab
diff --git a/assets/x_crowd.png b/assets/x_crowd.png
index 2f95fd7..f1bea78 100644
Binary files a/assets/x_crowd.png and b/assets/x_crowd.png differ
diff --git a/assets/x_crowd_c.png b/assets/x_crowd_c.png
index 511926a..66e2d07 100644
Binary files a/assets/x_crowd_c.png and b/assets/x_crowd_c.png differ
diff --git a/assets/x_crowd_h.png b/assets/x_crowd_h.png
index 3d01876..3c998ac 100644
Binary files a/assets/x_crowd_h.png and b/assets/x_crowd_h.png differ
diff --git a/index.html b/index.html
index 1c3e71f..165aa6c 100644
--- a/index.html
+++ b/index.html
@@ -664,6 +664,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
-
+