diff --git a/documentation/global-ui-controls.md b/documentation/global-ui-controls.md
index 1d94f56..046ed06 100644
--- a/documentation/global-ui-controls.md
+++ b/documentation/global-ui-controls.md
@@ -7,6 +7,7 @@ sound mute, and offline mode. They are not regular canvas elements and are not
stored in `tour_pages.ui_schema_json.elements`.
They behave like runtime chrome:
+
- button dimensions use canvas-relative percentages
- button positions are relative to the visible canvas using `xPercent/yPercent`
- constructor edit mode renders controls even when hidden
@@ -16,6 +17,7 @@ They behave like runtime chrome:
## Implementation Files
Backend:
+
- `backend/src/db/models/global_ui_control_defaults.js`
- `backend/src/db/models/project_ui_control_settings.js`
- `backend/src/db/api/global_ui_control_defaults.ts`
@@ -26,6 +28,7 @@ Backend:
- `backend/src/services/project_ui_control_settings.ts`
Frontend:
+
- `frontend/src/types/uiControls.ts`
- `frontend/src/components/UiControls/UiControlsSettingsForm.tsx`
- `frontend/src/components/Runtime/RuntimeControls.tsx`
@@ -58,27 +61,39 @@ hardcoded defaults are only a safety fallback.
Each control (`fullscreen`, `sound`, `offline`) supports:
-| Field | Purpose |
-|-------|---------|
-| `enabled` | Disable action while keeping the control selectable in edit mode |
-| `hidden` | Hide in runtime/preview; still render as ghost in constructor edit mode |
-| `xPercent`, `yPercent` | Canvas-relative position |
-| `anchor` | Which button point is placed at the coordinate |
-| `buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent` | Canvas-width-relative dimensions |
-| `defaultIconUrl`, `activeIconUrl` | Optional custom asset URL/storage key per state |
-| `defaultBackgroundColor`, `activeBackgroundColor` | Button background color per state |
-| `defaultBorderColor`, `activeBorderColor` | Button border color per state |
-| `hoverBackgroundColor`, `color` | Shared hover background and icon color |
-| `opacity`, `boxShadow`, `zIndex`, `order` | Presentation and initial ordering |
+| Field | Purpose |
+| ------------------------------------------------------------- | ----------------------------------------------------------------------- |
+| `enabled` | Disable action while keeping the control selectable in edit mode |
+| `hidden` | Hide in runtime/preview; still render as ghost in constructor edit mode |
+| `xPercent`, `yPercent` | Canvas-relative position |
+| `anchor` | Which button point is placed at the coordinate |
+| `buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent` | Canvas-width-relative dimensions |
+| `defaultIconUrl`, `activeIconUrl` | Optional custom asset URL/storage key per state |
+| `defaultBackgroundColor`, `activeBackgroundColor` | Button background color per state |
+| `defaultBorderColor`, `activeBorderColor` | Button border color per state |
+| `hoverBackgroundColor`, `color` | Shared hover background and icon color |
+| `opacity`, `boxShadow`, `zIndex`, `order` | Presentation and initial ordering |
Active state means downloaded/offline for the offline button, muted for the
sound button, and fullscreen for the fullscreen button.
-When runtime is embedded in an iframe, the fullscreen control first tries the
-browser Fullscreen API for the presentation document, then tries to fullscreen
-the embedding iframe when same-origin access is available. For cross-origin
-wrappers it posts `tour-builder:request-fullscreen` to `window.parent`; exit
-attempts post `tour-builder:exit-fullscreen`.
+When runtime is embedded in an iframe, the fullscreen control requests
+fullscreen on the embedding iframe first when same-origin access is available.
+For cross-origin wrappers it posts `tour-builder:request-fullscreen` to
+`window.parent` before attempting child-document fullscreen, because a
+permissions-policy rejection inside the iframe can consume the user activation
+needed by the parent fallback. If the iframe itself grants fullscreen but the
+parent does not listen for the message, the child-document fullscreen fallback
+can still enter fullscreen. Standalone runtime pages use the browser Fullscreen
+API for the presentation document. Exit attempts post
+`tour-builder:exit-fullscreen`.
+
+Browser fullscreen is not equivalent for top-level pages and iframes. A
+top-level runtime page can request document fullscreen from a user click, while
+a cross-origin iframe normally needs `allow="fullscreen"` / `allowfullscreen`
+or a parent page that handles `tour-builder:request-fullscreen` and fullscreens
+the iframe element.
+
The runtime keeps a local fullscreen-active state after an embedded parent
fullscreen request because the child document may not expose a local
`document.fullscreenElement` while the parent iframe is fullscreen. The hook
@@ -86,16 +101,24 @@ listens to both `fullscreenchange` and `webkitfullscreenchange` for native
fullscreen exits.
```html
-
+