# Inline links in modal bodies
> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).
A modal can carry clickable, Tab-focusable links in its prose — `ui::modal_links` owns the mechanism, `app::modal::docs_link` the one-line "see the manual" footnote built on it.
- **A link-bearing modal wraps its own body; every other modal still does not.** ratatui's `WordWrapper` is private and `Paragraph::line_count` answers a count, never a mapping, so there is no way to ask where a span landed. `wrap_rows` ports `WordWrapper` for `trim: false` and the pre-wrapped rows are painted with **no** `Wrap` set, so a residual disagreement clips a trailing cell instead of reflowing rows out from under the rects computed beside them; an empty `links` slice takes the old path verbatim. The port owes the body more than row breaks: `wrap_one_line` folds each line's style into every piece and copies its alignment onto every row it produced, and `link_rects` derives its own offset via `row_start_col`, or a centred row paints where the hit-test isn't. Pinned by `wrap_matches_ratatui_paragraph_wrapping` and `a_centred_link_hit_tests_where_it_is_painted`, both reading real renders back.
- **Link identity is structural — `(line_idx, span_idx)` — never inferred from style.** `ui::markdown_cheat_sheet` styles illustrative snippets with `theme.link_text` because they *depict* links, and `monochrome_dark` spends `UNDERLINED` on H2–H6 as well as every link slot. Either sniff would hand the user a clickable affordance resolving to nothing.
- **`ModalLinkTarget` is deliberately not `LinkTarget`.** `Anchor` and `Footnote` would resolve against whatever document sits behind the overlay, and a `LocalFile` naming a manual page is redirected to the embedded set *only while a page is already open* — so from a modal it would resolve `keybindings.md` against the working directory and open whatever sits next to the user's shell. It is a **struct**, not an enum: a modal link goes to a manual section and nowhere else. It briefly carried a second `Url` arm for an address handed to the browser, which no modal ever declared — add that back *with* its caller, not ahead of one.
- **Only *informational* modals get a footnote.** On a modal that asks a question, following a link closes the prompt — and closing the media prompts *is* an answer ("no, this session"). A modal with dismissal bookkeeping builds its own outcome instead of calling `follow_and_close`, so the bookkeeping still runs. Each footnote is a named `DocsFootnote` const so its test can resolve the target against the real page. The capabilities notice is the one carrying **no** fragment: `docs/terminal-compatibility.md` is a page about what that modal just reported, so its opening paragraphs are the answer and landing mid-page would skip them — its test asserts the page parses *and* that the fragment is still absent, since adding one would move where a reader lands without failing anything else.
- **Tab walks one ring: links first, then buttons.** `focused` keeps meaning "focused button index" with `focused_link` beside it, so existing modals read state unchanged; `y`/`Y` stays a shortcut for the focused *button* only. `LinkableResponse` wraps `ModalResponse` rather than adding a variant, which would be a compile error in every modal that will never carry a link. Following a link needs viewport dimensions that only `handle_key` carries, so `App::follow_modal_link` reads the `last_doc_height` / `last_doc_width` refreshed each frame in `prepare_viewport` rather than changing thirty-six `handle_click` signatures — and routes a manual page through the dirty guard, since opening one replaces the document.
- **A modal link is not an `Action`, so it must re-state the gates it bypasses.** `follow_modal_link` is a close-callback, which is exactly what puts it outside `diff_safe_action` — the gate that makes navigating away mid-review impossible everywhere else. That is reachable: `App::new` pushes the config-warning and capabilities notices *before* `main` calls `enter_read_only_diff`, so a link-bearing modal sits over a `git difftool` review on the first frame. Ungated, following one discarded the review with no confirmation *and* recorded a nav entry stamped `Mode::Diff`, which `restore_nav_entry` re-applies verbatim to a fresh editor holding no `DiffState` — a blank document area under a diff hint row. So it is refused with `flash_action_unavailable("diff review")`, not with the quit-confirm modal: a review is abandoned only by an explicit decision, and clicking a footnote is not one. The refusal is written against *replacing the live document*, which is what every `ModalLinkTarget` does today; a target that merely handed a URL to the browser would be safe mid-review and would owe an arm of its own. A second gate a modal callback can skip owes the same treatment here.