- **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.
