refactor(web): unify program-scope read-denial shapes (403/422 + shared fragment)

What

canopy-web's program-scope READ denials ship in three shapes (#1518 (closed) kept them rather than widening three more handler signatures):

  • get_case_detail participation denial → bare Err(StatusCode::FORBIDDEN) (empty 403 body; same shape as the pre-existing ?program= query gate at case_detail.rs).
  • get_tabHTTP 200 with the denial banner (render_tab_scope_denied / denied.fragment()) — an intended authz banner is recorded as an OK page.
  • get_intake_application → HTTP 200 with render_program_scope_denied.
  • (The #1518 (closed)-added surfaces — process page, artifact streams, fact-history — already return real 403/422 with the ScopeDenied fragment.)

The 200-banner shapes are not disclosures (static copy, no data), but they are dishonest statuses: monitoring can't distinguish a denial from a render, and the &78 (closed) plan's C2 called the four-shapes state out as a defect.

Acceptance criteria

  • One denial contract for scope/participation-denied reads: real status (403 out-of-scope / 422 no-or-malformed-authority) + the shared ScopeDenied fragment (full-page-wrapped where the surface is a page, bare fragment on htmx paths).
  • get_case_detail's bare-403 gains the fragment body; get_tab + get_intake_application stop returning 200 for denials (the htmx swap targets tolerate non-200 — verify hx-swap behavior or add HX-Retarget).
  • The scope-denial telemetry (record_scope_denied) fires identically across all surfaces.
  • Routed tests pin status + fragment per surface.

Context

Epic &78 (closed) / #1518 (closed) (c-deviations item 6 in plans/program-scope-enforcement.adoc). Relates to #1516 (closed)'s mutation-side denial work (403 + banner, already unified).