- **Drag to reorder** (`src/mobile/reorder.js` — one Svelte action, on the container, for the playlist page's rows and the player sheet's queue alike). Press and hold a row for 320ms and it lifts (with a short tap in the phone's motor), the rows below make room, and letting go lands it. Release without having moved and the row's sheet opens exactly as a long press always opened it — which matters most on the queue, where a row has no ⋯ and `oncontextmenu` is the only door to Play now, Move to next and Remove from queue. A press that moves more than 8px before the hold lands is a scroll, and the gesture lets go of it. Nothing carries `touch-action: none`: a flick that starts on a row still scrolls the list. `touch-action` is latched at touchstart and cannot be flipped mid-gesture, so the page's own scroll is refused instead by a non-passive `touchmove` listener added at the lift — which works because the finger has been still for 320ms and the browser has not started a scroll yet. A click and a contextmenu guard sit on the container in the CAPTURE phase while a lift is live (a released drag would otherwise fire the row's own click and play the track, and Android's own 500ms long press would open the sheet in the middle of the drag); both are off when no lift is live, so a tap and a long press behave as they always have. Every row is measured once at the lift, in the scroller's CONTENT space, so the edge pull — within 64px of either edge, ramped, up to 14px a frame — leaves the snapshot true and the row keeps up with the finger. Each displaced row moves by the exact difference between its own top and its neighbour's rather than by one assumed row height (the desktop's `rowH`), because the queue face puts an album separator between rows. The drop reuses the mutations the Move rows already use: `moveItem` and `moveInQueue` each take a numeric target now as well as `"up"` / `"down"` / `"top"`, so there is still exactly one call to `playlist_set_order` with the whole id order. It is optimistic — the rows go where you put them at once, and come back with the error toast if the backend says no. Two refusals guard the middle of a gesture: the playlist drops nothing if a refetch has replaced its rows (`pls.dragging` also defers the page's own re-read while a drag is live, the phone's version of the desktop's `skippedWhileDrag`), and the queue drops nothing if `player.qi` has moved, because a track boundary shifts the whole rendered window and the absolute indices would then address the wrong rows. Dragging is off while the playlist's filter has anything in it — a filtered list's visible order is not the stored order — and the note under the field says so; the sheet's Move rows address rows by id and stay correct under a filter, which is why they are the way to reorder without clearing it.