/** * viewportScrim.js — make a `position:fixed` full-screen SCRIM actually cover the VIEWPORT, * in every host, including one that has captured the fixed containing block. * * ── THE DEFECT ──────────────────────────────────────────────────────────────────────────────── * A modal scrim is written `position:fixed; inset:0` on the assumption that fixed coordinates are * viewport coordinates. That holds only while NOTHING in the ancestor chain establishes a * containing block for fixed descendants. VerticalTabsLayout — the cap-5 shell every plugin is * hosted in as a tab — sets `contain:layout` on .vtl-content, so inside the studio the SAME scrim * stretches to the CONTENT PANE instead. MEASURED 2026-09-22 at a 1280x900 viewport, probe * inserted into each component's real parent chain in the running studio: * * upg poster-wall .modal-scrim ......... 1024x793 @ (256,107) * PosterWall .modal-scrim .............. 1024x793 @ (256,107) * DataTable .dt-scrim .................. 1024x793 @ (256,107) * SemanticTreeLegend / LegendTools ..... 1024x793 @ (256,107) * ...all captured by: div.vtl-content [contain:layout] * * 256 is the rail width and 107 the header height. The scrim still LOOKS like a modal — it dims * the pane it sits in — while leaving the rail and header UNCOVERED AND CLICKABLE behind it. That * is why this reads as cosmetic and is not: a modal that does not capture input is not modal. * * ── WHY NOT THE OBVIOUS FIXES ───────────────────────────────────────────────────────────────── * ✗ Drop `contain` from the layout. It is load-bearing for a different reason (it stops a rail * collapse from reflowing a heavy mounted map) and removing it trades this bug for that one. * Identical reasoning to FilterDropdown.fixedOrigin() — the fix belongs where the ASSUMPTION * lives, not where the containment does. * ✗ width:100vw; height:100vh. MEASURED: fixes the SIZE but NOT the offset — the box still starts * at (256,64), so it overflows off-screen right and bottom while the rail stays uncovered. A * viewport-sized scrim in the wrong place is still the wrong scrim. * ✗ . These components render INSIDE SHADOW ROOTS (measured: .dt-wrap's root * is a ShadowRoot whose host is ). Teleporting to document.body leaves the * shadow-scoped CSS behind and the modal renders UNSTYLED. * * ── THE FIX ─────────────────────────────────────────────────────────────────────────────────── * Do not ASSUME the containing block is the viewport — MEASURE it, exactly as fixedOrigin() does, * then subtract it and size to the viewport. Verified to produce 0,0 1280x900 inside the captured * pane. Correct in EVERY host, including hosts that do not exist yet: when nothing captures, the * measured origin is (0,0) and the style collapses to plain inset:0. * * ── SHADOW-DOM NOTE (a false reassurance this replaces) ─────────────────────────────────────── * poster-wall.vue carried a comment saying "position:fixed inside a shadow root is still * viewport-relative, so this covers the whole app". The premise is TRUE and IRRELEVANT: a shadow * boundary scopes STYLES and SELECTORS, it does not create a containing block. What creates one is * transform / filter / perspective / backdrop-filter / will-change of those — or contain:layout| * paint|strict|content on an ancestor, which is exactly what the shell sets. Reasoning about the * shadow boundary answers a question nobody asked and hides the one that matters. * * USAGE (in any component that owns a fixed full-screen scrim): * import { useViewportScrim } from '/library/utils/viewportScrim.js' * const { scrimRef, scrimStyle } = useViewportScrim(someOpenRef) *