Function
On an iPhone the enlarged video reaches the whole screen only after a real
scroll hides Safari's toolbar, and a rotation brings that toolbar back. There
is nothing the page can do about it programmatically, so this tells the
learner the one thing that works. Whether it should be on screen is not
its decision: LessonVideoPlayer renders it while the mode is active and
useBrowserChromeVisible says the chrome is there, and unmounts it the
moment either stops being true.
It is an ordinary child of the player box, absolutely positioned along its
top edge, so it moves with the pinned player and needs no portal. The
wrapper is pointer-events-none: the gesture it asks for has to land on the
player and travel through it to the document, and a hint that swallowed it
would defeat itself. Only the dismiss control takes the pointer.
Dismissal is local state, so it lasts exactly as long as this instance — the rest of the enlarged session — and the next one starts fresh.
Every cue here names the finger's direction, never the page's. The two are opposite — the page scrolls down because the finger travels up — and only one of them may be spoken, since a hint that names both asks for two things. It names the finger's, because the page is the one thing the learner cannot watch move: the player is pinned and the backdrop covers everything behind it, so a line about the page going down reads as an instruction to put the video back. The arrow points up, the pill enters travelling up, the arrow's travel goes up, and the copy says the same.
That binds the copy: the verb has to be one of the hand moving the video
— dragging it in English, throwing it in Spanish — and never one of
scrolling. Which it is per locale is that locale's call; what binds them all
is that the verb acts on the video and none of them names the page. A scroll
verb beside an upward arrow is the two-cues-disagreeing
defect this component has twice been rewritten to remove. A direction word is
allowed now, but only anchored to the video — a bare "arriba" is what a
learner holding a phone in landscape reads against the wrong axis, while
"arriba" said of something they can see on screen resolves in any
orientation. messages.test.ts holds the scroll verbs out of every locale.
The direction lives in the glyph, not in a class. An earlier version drew
one arrow and flipped it with rotate-180, which renders correctly in every
engine — and still shipped the bug once, on a phone that picked up the new JS
while holding a cached stylesheet: it drew the copy beside an arrow pointing
the wrong way, because the rule that flipped it was new to the stylesheet and
that device did not have it yet. ArrowUp cannot fail that way. Keep it: what
CSS carries here is the motion, and a stylesheet that never arrives should
cost a still arrow, never a wrong one.
The travel is bounded rather than endless. The learner enlarged the video to
watch it, and a cue that never stops moving over it works against the thing
they asked for, so the arrow demonstrates the direction and then holds still,
still pointing. arrow-lift in globals.css is Tailwind's bounce widened,
and carries a name of its own precisely so a cached stylesheet leaves the
arrow still instead of moving it the wrong way; see the comment at its use
site for why the bound is a half iteration and not a whole one. All of the
motion is dropped under prefers-reduced-motion, which leaves exactly the
still pointing arrow.
role="status" lets assistive technology hear the hint without focus
leaving the player. The visible line carries only the gesture — gesture and
outcome together do not hold one line at a phone's width — so
screenReaderMessage is the one that says both, and the visible line is
aria-hidden so the two do not stack into a doubled announcement. The glyphs
are lucide-react's, like the resume overlay's: this is app chrome drawn over
the video, not a player control.
The hint drawn over the enlarged video while the browser's own chrome still takes part of the screen, asking for the gesture that reclaims it.