This one came from a Student, in her own words:
aqui uma nota. é pena n dar para ver a resposta certa depois de acertar. neste caso até fiquei com duvida sobre o que escrevi.
She finished a Word Match, the Check came back all-correct, and a modal appeared to say so. Behind it — unreachable — was the passage holding the four words she had just placed. She wanted to re-read her own answer and could not.

Look at that screenshot properly. Sprints and Daily Scrum are visible at the edges; Sprint Planning and Sprint Retrospective are behind the white box. The exercise is right there, complete and correct, and there is no way to look at it. The two buttons both leave.
The complaint is spatial, not informational
The first instinct is to put more into the modal: show her answers next to the correct ones, right there in the dialog. That instinct is wrong twice.
It does not scale — a Sentence Selection passage or a full Grouping Deck does not fit in a 440px box. And more importantly it misreads the complaint. She did not want a report. She wanted the thing she had made, which was already rendered, already correct, and already on screen. Nothing needed to be built. Something needed to get out of the way.
flowchart TD
A["Check → all correct"] --> B["Completion recorded"]
B --> C["success modal"]
C -->|"Review my answers
Esc · backdrop click"| D["modal gone"]
D --> E["the exercise, as she left it"]
D --> F["completed state:
Back to course · Next lesson →"]
C -.->|"before"| G["no way out
the passage unreachable"]
style D fill:#dce8dc,stroke:#5f7a5f
style E fill:#dce8dc,stroke:#5f7a5f
style F fill:#dce8dc,stroke:#5f7a5f
style G fill:#f0dcdc,stroke:#a06060
An × would have been the wrong control
The reflex is a close glyph in the corner. It is compact, it is conventional, and it assumes a vocabulary this Student has no reason to have. The person who wrote that note is not describing a dialog she failed to close; she is describing a wall.
So the control is labelled, and it says what happens: “Review my answers” — Rever as minhas respostas. It sits on its own line under the action row, scaled down, so it never reads as a third equal-weight choice next to “Next lesson →”. Esc and a click on the backdrop do the same thing and are wired too, but neither is anybody’s only route.
One string serves all three Active Lesson types, and it deliberately echoes her words — o que escrevi — rather than naming a per-type artifact like “the completed passage”.

The modal was carrying the only way forward
Here is what made this bigger than adding a button.
The exercise body stayed rendered behind the modal — including a live “Check answers” button. Dismissing onto that would have left her facing a control that could no longer mean anything, on a screen with no way onward except the sidebar. The modal held “Next lesson →”; closing it threw that away.
Worse, the button still worked. Pressing it re-ran the Check and called record_attempt/2 again, writing a second full set of Challenge Attempts for a submission the server had already judged. That is precisely the pollution CONTEXT.md warns about for a Quiz Retake: a difficulty signal corrupted by a button press that changes nothing.
So dismissal reveals a completed state. Check is gone, not disabled, and the two ways on take its place in the strip where it used to be. The action row is now a single shared component, because it was byte-identical in all three modals already:
def completion_actions(assigns) do
~H"""
<div class={@class}>
<.link navigate={~p"/catalog/#{@course}"} class="mm-btn mm-btn-ghost">
{gettext("Back to course")}
</.link>
<button type="button" phx-click="complete_and_next" class="mm-btn mm-btn-primary">
{if @progress_pct == 100, do: gettext("Finish course"), else: gettext("Next lesson →")}
</button>
</div>
"""
end
The assign that must not be the Completion
This is the load-bearing decision, and it is one line.
The obvious way to decide “is this Lesson finished?” is to ask whether a Completion exists. It is also wrong here, and getting it wrong turns a small fix into a regression.
assign_word_match/1 runs on every update and resets placements to %{}. A Word Match persists no run — CONTEXT.md says it and its Sentence Selection sibling are “simply replayable”. So a Student returning next week to a Lesson she finished gets a blank exercise. If the completed state keyed off the Completion, she would arrive at an empty passage with no Check button and no way to replay it at all.
The state therefore keys off this visit’s dismissal:
|> assign(:dismissed, false)
Three states, not two: unfinished shows Check, passed-with-modal-up shows nothing in the strip, dismissed shows the way on. That middle case is not fussiness — rendering the completed actions behind the open modal put two live complete_and_next buttons on the page at once, and the existing student_solves_* tests caught it immediately by failing on an ambiguous selector.
What Sentence Selection would have lost
Its modal carries completion_feedback — a message the Instructor wrote for a correct answer, and which lived nowhere else. Since dismissal is one-way (no reopen; she closed it on purpose), a naive close would have traded the Instructor’s words for her own Sentences.
So the message moves with the completed state, inline above the action row. She keeps both.

What Grouping does not get
Grouping raises a modal on failure too, and that one says "%{count} Cards are in the wrong group — they are marked ✗" while covering the board carrying those marks. Same shape of bug.
It is deliberately out of scope, and not carded either. Its “Try again” button is the Student’s route back into the Round, and because keep_correct re-deals only the Cards that were wrong, she loses nothing by pressing it. The failure modal stays modal — no dismiss control, no Esc, no backdrop click, wired conditionally:
phx-click-away={@dismissable && "dismiss_result"}

The test that cannot exist
Thirty ExUnit tests cover this — the event, the assign, what renders after, the Challenge Attempt count, the returning-visit regression. Every one of them would have passed against the broken markup.
The defect was a well-formed <div> with a higher z-index. LiveViewTest never hit-tests, so from ExUnit’s point of view the passage and the modal are both simply present in the DOM, and always were. This joins the list in CLAUDE.md of things the test suite structurally cannot see, next to the daisyUI collision that made a Save button unclickable while render_submit stayed green.
The browser can see it, and the proof is one call:
document.elementFromPoint(x, y) // x, y = the middle of the passage
Before dismissal it returns wm-modal. After, wm-passage. That is the entire bug and the entire fix, expressed as the only assertion that was ever capable of failing on it.
The Playwright run also confirmed the parts that are easy to assume: the control is reachable by Tab alone and fires on Enter, Esc closes the modal, the completed state does not overflow at 320px, exactly one forward button exists after dismissal, and — since this is a Word Match — the answer key is still absent from the DOM (ADR 0014).
One honest wart
The Grouping board still says “Rodada encerrada — veja seu resultado” after the result has been dismissed, which now points at something that is no longer on screen. It is copy, it is not wrong enough to widen this diff for, and it is written down here rather than quietly fixed.