diff --git a/RELEASING.md b/RELEASING.md
index e749d2d..8dcb7f5 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -116,9 +116,16 @@ python3 tools/screenshots.py
That rewrites all five in
`fastlane/metadata/android/en-US/images/phoneScreenshots/` at 1170×2532. They
-are rendered from `public_html/index.html` with headless Chromium, not taken on
-a phone, so they reproduce exactly — anything that changes the look of the app
-is a reason to re-run it.
+are rendered from `public_html/index.html` with headless Chromium rather than
+taken on a phone, so anything that changes the look of the app is a reason to
+re-run it.
+
+**Near-exact, not exact.** Two runs of identical code differ by a few bytes in
+shots 1 and 4: a 4×13 pixel sliver at the delay bar's fill edge, where the
+`scaleX` boundary lands mid-pixel and the rasteriser rounds it differently from
+one run to the next. Max channel difference 11, invisible. So a `git diff` after
+re-running is not evidence that anything changed — compare the pixels before
+believing it, and `git checkout` the files back if that sliver is all it is.
The script sets each scene by seeding `localStorage` before the app boots, so
the app renders its own saved state rather than having the DOM poked from
diff --git a/public_html/index.html b/public_html/index.html
index 362cd1e..e71940e 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -209,26 +209,73 @@
.btn--vacant{visibility:hidden}
.btn:focus-visible{outline:2px solid var(--icon-hot);outline-offset:-6px;border-radius:8px}
- /* reset armed for a second tap — the ring unwinds over the same 2s as the timeout */
+ /* Reset confirms two different ways, because the two input methods can't share
+ one. A finger holds the button and the ring fills under it; lifting early
+ springs the ring back, which is the only way to say "keep going" without
+ words — and the bar carries no words by design, since it is shared by two
+ players sitting opposite each other. Keyboard and assistive activation
+ can't hold, so they keep the older two-presses-within-2s, where the ring
+ drains to show the window closing. */
#reset{position:relative}
- #reset.armed{color:var(--icon-hot)}
+ #reset.armed,#reset.holding{color:var(--icon-hot)}
#reset .ring{
position:absolute;
top:50%;left:50%;
- width:clamp(36px,9.6vw,48px);height:clamp(36px,9.6vw,48px);
+ /* As big as the bar will take, so a thumb can't cover it: the button is the
+ bar's full height, leaving 4px of dark above and below. width:auto is
+ load-bearing — .btn svg sets a width for the icons, and inheriting it here
+ would give the ring that width against this height and draw an ellipse.
+ It ends up a few px wider than the button's own slot, which is only ever
+ empty space; the neighbouring glyph stays a good 12px clear. */
+ height:calc(100% - 8px);
+ width:auto;
+ aspect-ratio:1;
transform:translate(-50%,-50%) rotate(-90deg); /* start the arc at 12 o'clock */
- display:none;
+ opacity:0;
+ /* Hiding waits out the spring-back, then fades. display:none here would cut
+ the retreat off at the instant the finger lifts, and that retreat is the
+ whole reason the gesture teaches itself. The 120ms is a real duration on
+ purpose: a 0s transition with a delay is allowed to be treated as no
+ transition at all, which would take the delay with it. */
+ transition:opacity 120ms linear 180ms;
pointer-events:none;
}
- #reset.armed .ring{display:block}
+ /* appearing is instant — no duration, no delay */
+ #reset.armed .ring,#reset.holding .ring{opacity:1;transition:none}
+
#reset .ring circle{
fill:none;
- stroke:currentColor; /* the same flash the buttons use */
- stroke-width:2.6;
+ /* user units, so it scales with the ring: 2 of 40 lands between 2.5 and
+ 3.4 real px across phone sizes, a shade heavier than the icon strokes
+ because this one has to read past a thumb */
+ stroke-width:2;
+ }
+ /* the whole circle, dim, there from the moment the finger lands: it shows how
+ far there is to go, which an arc growing out of nothing cannot */
+ #reset .ring-track{stroke:var(--icon)}
+ /* and the bright arc that covers it, from 12 o'clock clockwise */
+ #reset .ring-fill{
+ stroke:var(--icon-hot);
stroke-linecap:round;
stroke-dasharray:113.1; /* 2πr, r=18 */
- animation:unwind 2s linear forwards;
+ stroke-dashoffset:113.1; /* empty */
+ /* The spring back. 180ms is the ceiling, not the figure: reversing a
+ transition part-way shortens it in proportion, so letting go at 41% takes
+ 74ms, and the arc never spends longer retreating than it spent filling.
+ The dim track outlasts it either way — the ring holds for 180ms and then
+ fades, so even a stray tap shows a whole circle appear and go, which is
+ what says there was something to finish. */
+ transition:stroke-dashoffset 180ms ease-out;
}
+ #reset.holding .ring-fill{
+ stroke-dashoffset:0;
+ transition-duration:975ms; /* keep in step with HOLD_MS */
+ transition-timing-function:linear;
+ }
+ /* the keyboard path: a full ring draining over the 2s window. An animation
+ beats a transition on the same property, which is fine here only because
+ .armed and .holding are never set at once. */
+ #reset.armed .ring-fill{animation:unwind 2s linear forwards}
@keyframes unwind{
from{stroke-dashoffset:0}
to{stroke-dashoffset:113.1}
@@ -362,6 +409,13 @@
@media (prefers-reduced-motion:reduce){
*{transition-duration:.01ms!important}
+ /* ...except the reset ring, which reports how far through the hold you are.
+ Collapsing it would fill the ring the instant you touched the button and
+ tell you it was done 650ms before it was. This one is state, not
+ decoration. Higher specificity than the * above, so it wins despite both
+ being !important. */
+ #reset .ring-fill{transition-duration:180ms!important}
+ #reset.holding .ring-fill{transition-duration:975ms!important}
}
@@ -387,8 +441,12 @@