# Changelog All notable changes to this project will be documented here. This project loosely follows [Semantic Versioning](https://semver.org/) and uses the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. ## [Unreleased] ### Added - **`.ds-tooltip`** — a floating info-surface component (in `Overlays.uss`). A positioned card (the **consumer** sets the screen position, e.g. mouse-follow with edge-flip in C#) on the elevated surface tier with a *strong* border so it reads clearly when floating over busy content rather than nested inside a card. Slots: `__title` (body-1 bold), `__subtitle` (caption), `__body` (body-2, wraps), `__divider` (hairline), and `__row` (`__row-label` left + `__row-value` right) for stat lines. Fills the gap that item/unit tooltips previously had to cover by borrowing `.ds-card`. Rendered in the showcase's TOOLTIP section; documented in `docs/COMPONENTS.md`. - **Drag & drop** — `.ds-draggable` / `.ds-drop-zone` / `.is-drag-over` / `.ds-drag-ghost` (in `Controls.uss`) plus auto-wiring in `DesignSystemRuntime.EnsureDraggables`: mark items `.ds-draggable` and containers `.ds-drop-zone` and the runtime handles pointer drag, a floating `.ds-drag-ghost`, drop-zone highlighting, and reparent-on-drop — no code. Game inventories with custom split/merge/transfer logic drive their own pointer handling and reuse only the `.ds-drag-ghost` / `.is-drag-over` visuals. Rendered in the showcase's DRAG & DROP section. ## [1.4.1] — 2026-05-17 ### Fixed - **Codigrate `surface` was mapped onto DS `--color-surface` instead of `--color-surface-elev`** — flagged by Codigrate's author after reviewing the live showcase. In Codigrate's IDE terminology, `surface` is the *raised* chrome (sidebar, dock, toolbar) and `windowBackground` is the *body* the editor sits in — confirmed by the luminance ordering, which is consistent in every shipped palette (dark themes: `editorBackground < windowBackground < surface`; light themes: the reverse). Earlier wiring treated `surface` as the cards/sections fill — promoting the raised chrome colour to the body tier and demoting the body colour (`windowBackground`) all the way down to the page `bg`. Section cards therefore wore the colour meant for drawers and modals, and the visible page background was one layer too high in the elevation stack. **Fix**, in `CodigrateThemeApplier.FromCodigrate`: - `editorBackground` → `--color-bg` (deepest pit / page bg) — was `windowBackground`. - `windowBackground` → `--color-surface` (body / cards / sections) — was `surface`. - `surface` → `--color-surface-elev` (raised chrome — drawers, modals, sidebar) — was a synthesised `Mix(surface, black, 0.04)` on light themes and `editorBackground` on dark, both wrong by one layer. - The derived `border / borderStrong / surfaceHover` tints now mix from `windowBackground` (the body) toward `primaryForeground`, so a `.ds-card`'s 1-px border reads as a tint of its own fill, not of the page bg. - No other applier handlers needed to change — they all consume `m.Bg / m.Surface / m.SurfaceElev` symbolically, so re-pointing the three fields at the correct upstream tokens cascades correctly through every section, card, modal, drawer, toast, tab, nav, input, dropdown, slider, stepper, pagination, spinner, skeleton, avatar, badge, tag, chip, and showcase chrome class the applier touches. - **Input boxes now sit on the deepest surface tier instead of raised chrome** — second round of codigrate-author feedback: in IDE-style palettes inputs should read as editable wells INSIDE their parent card, not as buttons stacked on top of it. The original `.ds-input / .ds-textarea / .ds-search / .ds-dropdown` rules used `--color-surface-elev` (raised chrome), which made every input look like a button. The codigrate author specifically asked for `background` / `editorBackground`, which both map to our `--color-bg` slot after the surface-tier fix above. Treating the change as a DS-wide pattern rather than a codigrate-only override since it applies to every theme the system supports. - **`Inputs.uss`** — four base-fill rules switched from `--color-surface-elev` to `--color-bg`: `.ds-input .unity-text-input` (+ `__input` / `__base-text-field__input` / `__base-field__input` aliases), `.ds-search` outer wrapper, `.ds-dropdown .unity-popup-field__input` (+ aliases), `.ds-textarea .unity-text-input` (+ aliases). `:focus / .is-active / :disabled` rules still resolve to `--color-surface`, so the state change reads visibly against the new deeper base (input lifts one tier on focus instead of dropping one tier; the direction flips with light vs dark theme but the magnitude of shift stays meaningful in both). - **`CodigrateThemeApplier.cs`** — three input call sites flipped from `m.SurfaceElev` to `m.Bg`: `ApplyInputContainer` (covers `.ds-input` + `.ds-textarea` via parameterised wrapper class), `ApplySearchContainer` (outer chrome on `.ds-search`), `ApplyDropdownContainer` (inner `__input` box on `.ds-dropdown`). No focus / disabled handlers in the applier — the cascade picks up the USS `:focus / :disabled` rules naturally once the applier reverts. - **Scope verification.** Audited every `surface-elev` reference across the DS USS files via grep — only the 4 input rules are about editable fields. Every other `--color-surface-elev` use (buttons, navigation items, toasts, slider tracks, range tracks, progress backgrounds, stepper buttons, pagination chrome, view-toggles, tabs, avatars, skeletons, spinner tails, sheets, modal illustrations, empty-state icon backgrounds) is intentionally raised-chrome and stays at the elevation tier. Same audit for the applier: only the three input helpers stamp the bg property symbolically; everything else either targets unrelated UI tiers or doesn't need to change. ## [1.4.0] — 2026-05-16 External theme-provider integration: the live showcase can now load any of [Codigrate](https://codigrate.com)'s 12 IDE themes (Sequoia, Sakura, Roraima, Autumn, Aurora Borealis, Everest, Tokyo, Tallinn, Istanbul, Miami, Rio de Janeiro, Paris) at runtime and re-skin the whole tree to match, plus a `Randomize colors` button that generates plausible HSV-driven palettes for try-until-you-like-it exploration. The day / night toggle is suppressed while a third-party palette is active — codigrate carries its own `"appearance": "light" | "dark"` field — and re-enabled when you select `Design System default` from the dropdown again. Same showcase URL — `https://sinanata.github.io/unity-ui-document-design-system/` — refresh to see. ### Added - **`CodigrateThemeProvider.cs`** (showcase-only) — fetches `https://codigrate.com/assets/themes/list.json` plus the selected theme's `.palette.json` via `UnityWebRequest` on Editor / Standalone. WebGL goes straight to the bundled fallback (codigrate.com sets no `Access-Control-Allow-Origin` headers, verified with `curl -sI -H "Origin: https://example.com"` — Cloudflare passes the request through but the origin emits no `Access-Control-*` response headers, so browser-context fetches deterministically fail the preflight). Caches the list + parsed palettes for the lifetime of the scene; one JsonUtility round-trip per palette. - **`Assets/Showcase/Resources/CodigrateThemes/`** — 13 bundled JSON `TextAsset` resources (`list.json` + 12 `.json`) mirroring the upstream codigrate URLs at build time. Loaded via `Resources.Load("CodigrateThemes/")`; the slug is derived from the upstream `json` path's basename (`assets/themes/nature/sequoia-theme/sequoia.palette.json` → `sequoia`). Live fetch on Editor / Standalone still wins so codigrate edits land without a rebuild; bundled is the WebGL path of record and a network-failure fallback elsewhere. - **`CodigrateThemeApplier.cs`** — translates a codigrate `tokens.interface` block (12 colours) into the broader DS palette (~25 colours: surfaces, borders, brand variants with hover / press / soft alpha tints, `text-on-accent` derived from accent luminance) and stamps the result onto the showcase tree via INLINE styles. Reasoning lives in the file header: Unity 6's public UI Toolkit API has no path for setting CSS custom properties at runtime (see *Notes*), so an explicit walk-by-class is the only stable path. Coverage matrix: - **Surfaces** — `.ds-section`, `.ds-card`, `.ds-modal`, `.ds-dialog`, `.ds-sheet`, `.ds-empty__icon-bg`, `.ds-modal__illustration`, `.ds-toast` (+ `--success / --info / --warning / --danger` border + icon tints), `.ds-animal-card` (+ `__check`, `__image`, `__star`, `__title`), `.ds-animal-detail` (+ `__hero`, `__description`, `__info-row__icon / __label / __value`). - **Buttons** — `.ds-btn--primary / --secondary / --tertiary / --danger` (base + hover via `PointerEnter` / `PointerLeave` callbacks, press via the `.ds-btn--pressed` showcase modifier, disabled via the `unity-disabled` class), `.ds-btn--ghost` (two-axis hover: transparent → surface-elev fill, border → border-strong), `.ds-btn--icon` (+ `.ds-btn--icon-danger` soft-fill variant), `.back-btn` (+ inner `.back-btn__icon / __label`), and inner `.unity-label` colour for every `.ds-btn--with-icon` variant. - **Inputs** — `.ds-input`, `.ds-textarea`, and `.ds-search` descend into the Unity-emitted `.unity-text-input / .unity-text-field__input / .unity-base-text-field__input / .unity-base-field__input` children where the visible bg actually lives. `.ds-dropdown` descends into `.unity-popup-field__input / .unity-base-popup-field__input / .unity-base-field__input`, plus the selected-value text and the chevron tint. - **Tabs + view toggles** — `.ds-tabs`, `.ds-view-toggle` wrappers + `.ds-tab.is-active` and `.ds-view-toggle__btn.is-active` (primary fill, text-on-accent / tint-on-accent on the active variant). - **Navigation** — `.ds-side-nav / .ds-side-rail / .ds-bottom-nav` containers + the `is-active` items (primary-soft fill, primary-tinted icon + label). `.ds-profile` (+ `__avatar`, `__name`, `__chevron`). - **Toggles / checks / radios** — snapshot `Toggle.value` / `RadioButton.value` and stamp the inner `.unity-toggle__checkmark` / `.unity-radio-button__checkmark-background / __checkmark` per state. A `RegisterValueChangedCallback` keeps the visual in sync when the user flips the control; `Revert` unregisters it so swapping themes doesn't leak handlers. - **Sliders + range + progress** — descend into `.unity-base-slider__tracker / __dragger`, `.unity-min-max-slider__tracker / __dragger / __min-thumb / __max-thumb`, `.unity-progress-bar__background / __progress`. - **Steppers + pagination + loading** — `.ds-stepper / __btn / __value`, `.ds-pagination / __btn (+ is-active) / __ellipsis`, `.ds-spinner` (asymmetric 4-side border so each side stamped individually + tracked as a single `BorderColor` touch for revert), `.ds-skeleton`. - **Notification + avatar** — `.ds-notif-icon`, `.ds-notif-dot` (+ `__count`), `.ds-avatar` placeholder fill. - **Badges / tags / chips** — `.ds-badge--common / --rare / --epic / --legendary` (bg tint only; rarity text colour stays constant), `.ds-tag--amphibious / --aquatic / --nocturnal`, `.ds-chip--equipped / --new / --owned / --limited / --event / --sale` (bg + border + text + inner `.ds-chip__icon` tint; chip text cascades to the inner `.unity-label` through the `color` property). - **Icons** — default `.ds-icon` tint sweep at the top of `Apply` so plain chevrons / search / settings glyphs follow the palette's `TextSecondary`; the modifier variants (`.ds-icon--primary / --secondary / --disabled / --accent / --danger / --warning / --info / --on-accent`) overwrite afterwards. Rarity modifiers (`.ds-icon--rarity-common / --rarity-rare / --rarity-epic / --rarity-legendary`) are intentionally left at DS defaults so a Common pill stays green-tinted regardless of the active palette. - **Showcase chrome** — `.showcase-drawer-frame / -strip / -placeholder-text / -placeholder-icon`, the COLORS-section swatches, `.ds-scrollbar-demo`. - **`Randomize colors` button** in the COLORS section — generates an HSV-driven palette in the toggle's current mood (sibling brand hues for secondary / tertiary, amber warning, red danger), stamps it through the same applier as codigrate, and adds a `Random palette` entry to the dropdown for revert. Re-clicking the button rolls a fresh palette in the same mood; flipping the day / night toggle re-rolls in the opposite mood. - **`Themes by Codigrate` link button** — opens `https://codigrate.com` via `Application.OpenURL`. - **Theme dropdown** in the COLORS section — `Design System default` + 12 codigrate names + `Random palette`. The default option reverts every inline stamp to `StyleKeyword.Null` so the DS USS cascade takes over again (and the day / night toggle picks up where it left off without any state-coupling between this file and the toggle). - **`UpdateHexLabelsFromOverride`** in `ShowcaseBootstrap` — the COLORS section's 13 hex labels now reflect the active palette regardless of source. Falls back to the existing dark / light dictionary when no override is active. ### Fixed - **Drawers ignored the day / night toggle.** The three drawer demos in `DesignSystemShowcase.uxml` had baked the DS dark `--color-bg` (`rgb(11, 15, 23)`) + `--color-border` (`rgb(38, 48, 65)`) into the wrapper's inline `style=` attribute, plus the dark `--color-text-disabled` (`rgb(103, 112, 133)`) on the placeholder labels and the icons inside. Inline styles always beat USS class rules, so toggling `.theme-light` (or any future theme provider) never re-painted the demo frames. **Fix**: introduced four showcase-only helper classes (`.showcase-drawer-frame / -strip / -placeholder-text / -placeholder-icon`) in `ShowcaseTheme.uss` that bind those colours to tokens via `var(--color-...)`, and the three drawer demo wrappers now use the classes instead of inline RGB. Token cascade flows through the theme toggle and any external palette. - **Drawer chrome snap-cut during theme swap.** The `.ds-drawer.ds-drawer--top / --right / --bottom / --left` compound rules in `Controls.uss` set `transition-property: translate` — the slide animation only — which overrode the universal `.ds-root *` colour-transition list from `ShowcaseTheme.uss` (compound `0,2,0` beats universal `0,1,0`). Net effect: the drawer's surface + border colour jump-cut during a theme swap while the rest of the tree eased over 240 ms. **Fix**: extended each compound selector's `transition-property` list to include `background-color, color, border-*-color, -unity-background-image-tint-color`. The slide keeps its 200 ms duration; the colour properties pick up the universal 240 ms cadence. Push variants got the same extension on their `max-width` / `max-height` rules. ### Notes - **Why not override `--color-*` at runtime?** Unity 6's public UI Toolkit API has no path for setting CSS custom properties on a `VisualElement` — `style` is typed and covers only the built-in properties; you can only get values via `RegisterCallback`. The internal `StyleVariableContext` is reflection-only and not stable across versions. An explicit per-class walk-and-stamp is therefore the only public-API path that hits every visible element while keeping the DS USS as the single source of truth for everything else (spacing, radii, transitions, layout). If a future Unity release exposes `style.SetCustomProperty(name, value)`, the applier collapses to a single root-level call. - **Static codigrate mirror exists for a reason.** The upstream JSON is unauthenticated and Cloudflare-fronted, but the host doesn't return `Access-Control-Allow-Origin` headers, so browser-context fetches (WebGL) deterministically fail the CORS preflight. The bundled `Resources/CodigrateThemes/` mirror is the WebGL path of record; Editor and Standalone go live so upstream edits land without a rebuild. - **Hover swaps re-installed via pointer callbacks.** Inline styles beat the `:hover` USS rule that would otherwise drive hover swaps on brand buttons, so `Apply` registers `PointerEnter` / `PointerLeave` callbacks per brand-variant button that stamp the hover colour on enter and the base colour on leave. `Revert` unregisters them so a theme swap doesn't leak duplicate listeners. - **Dropdown popup under codigrate.** Unity adds `GenericDropdownMenu` as a SIBLING of `rootVisualElement` under `panel.visualTree`, so the applier's `root.Query(...)` doesn't reach it — the popup chrome stays at the existing `ShowcaseDropdownPopup.uss` defaults regardless of which palette is active. A second pass keyed to `panel.visualTree` would close the gap; deferred. ## [1.3.1] — 2026-05-11 ### Fixed - **Showcase overlay was injecting in all scenes.** Added a simple check in `ShowcaseBootstrap` checking if the current scene name is equal to the Showcase scene name. ## [1.3.0] — 2026-05-08 Burger panels and drawers ship as a first-class component, in response to [issue #1](https://github.com/sinanata/unity-ui-document-design-system/issues/1). One `.ds-drawer` class with composable direction (`top` / `right` / `bottom` / `left`) and mode (`overlay` / `push`) modifiers covers both transition styles called out in the issue: top-to-down growing-overlay, and left-to-right growing-overlay-with-shrinking-siblings. Auto-hiding scrollbar lands as a one-class `:hover` modifier with a touch-friendly runtime helper. Showcase has four new live demos at the bottom of the page — same URL, refresh to see. ### Added - **Drawer rules** appended to `Controls.uss` (rather than living in their own file). Unity's USS asset importer caches the parent stylesheet's `@import` resolution; new-file additions can fail to load until a `-ClearCache` rebuild, and a contributor reading the showcase output saw the drawer demos render unstyled because of exactly that. Bundling the rules into an existing imported file sidesteps the cache cliff entirely. The matrix is `direction × mode`: - **Direction** modifiers: `.ds-drawer--top`, `.ds-drawer--right`, `.ds-drawer--bottom`, `.ds-drawer--left`. Each pins the drawer to its named edge and chooses the off-edge resting state (`translate: 0 -100%`, `100% 0`, `0 100%`, `-100% 0` respectively). - **Mode** modifiers: default is **overlay** (`position: absolute`, slides via `translate`, the cheap path — no layout reflow) and `.ds-drawer--push` (in the flex flow, animates `max-width` for left/right or `max-height` for top/bottom from `0` to the authored size, so flex-grow siblings shrink to make room — the "shrinking siblings" pattern in image #2 of issue #1). - **State** is toggled by either an `is-open` class on a `.ds-drawer-wrap` ancestor (recommended; one class flip animates drawer + backdrop together) or by `ds-drawer--open` on the drawer itself (freestanding usage). Both selectors are wired in the USS so authors can pick whichever fits their tree. - **Chrome** is optional and follows the existing BEM convention: `.ds-drawer__header` / `__title` / `__close` / `__body`. The body slot is a flex column by default; wrap it in a `` for long content. - **Transition timing** uses the existing `--transition-medium` (200 ms) token + `ease-in-out`; theme swap and drawer slide animate at the same cadence. - **`.ds-drawer__backdrop`** — sibling to the drawer inside the wrapper. Dim layer (`var(--color-overlay)`) that fades in/out with the drawer. The runtime helper toggles `pickingMode` in lockstep with the open class because UI Toolkit's `opacity: 0` does NOT disable pointer picking — an invisible-but-rendered backdrop would otherwise shadow the burger button beneath it. - **`.ds-burger`** — three-bar hamburger that morphs into an `x` when its `.is-open` class is set. Pure-USS animation: top bar gains `translate: 0 6px; rotate: 45deg`, middle bar fades, bottom bar gains `translate: 0 -6px; rotate: -45deg`. Optional — drop the `.ds-icon--menu` glyph into a `.ds-btn--icon` for a static burger icon when you don't want the morph. - **`.ds-scroll--auto-hide`** modifier (`Controls.uss`) — fades the scrollbars from `opacity: 0` to `opacity: 1` on `:hover` of the wrapping ``. Scoped to `.ds-scroll--auto-hide .unity-scroll-view__vertical-scroller / __horizontal-scroller` so it composes with the existing slim 8-px scrollbar styling — no extra rules needed beyond the modifier. Also responds to an `is-scrolling` marker class for touch devices, where `:hover` doesn't fire. - **`DesignSystemRuntime.WireDrawer(opener, wrapperOrDrawer, params closers)`** — single-call wiring. Toggles `is-open` on the wrapper when `opener` is clicked; removes it when any closer is clicked. Auto-detects when `opener` carries `.ds-burger` and mirrors the open class so the bars-to-x animation fires. For non-button closers (typically the backdrop) it both registers a `PointerDownEvent` and tracks them in a list so it can flip their `pickingMode = PickingMode.Position` while open / `Ignore` while closed — the "invisible backdrop blocks the burger" bug is precluded automatically. - **`DesignSystemRuntime.WireScrollAutoHide(scrollView)`** — touch-friendly auto-hide. Adds an `is-scrolling` class to the `ScrollView` for ~700 ms after each `WheelEvent` / `PointerDownEvent`, which the auto-hide rule responds to. Desktop users get the pure-USS `:hover` rule for free; this helper covers mobile, where there's no hover signal. - **Showcase**: four new sections at the bottom of `DesignSystemShowcase.uxml`, generic UI patterns rather than game-specific copy: - `DRAWER — TOP OVERLAY` — "Document" frame; burger opens a top-edge "Menu" drawer with New / Open / Save buttons. - `DRAWER — RIGHT OVERLAY` — "Library" frame; burger opens a right-side "Filters" drawer over a dim backdrop, with three toggle filters. Click the backdrop to dismiss. - `DRAWER — RIGHT PUSH` — "Editor" frame; burger opens a right-side "Inspector" drawer that pushes the central column to make room. Body holds a text input, a slider, and a toggle (so the demo also exercises `.ds-input`, `.ds-slider`, `.ds-toggle` inside a drawer body). - `AUTO-HIDING SCROLLBAR` — vertical `ScrollView` with `.ds-scroll--auto-hide`. Mouse: hover to reveal the scrollbar; leave to hide. Touch: scroll/wheel/tap flashes the bar for 700 ms. ### Changed - **`DesignSystem.uss`** now imports `Drawers.uss`. - **`Mobile.uss`** adds drawer-specific overrides: side drawers cap at `88%` of viewport width (so the user keeps a peek of the page behind), top/bottom drawers cap at `60%` height with a `280 px` max, and `.mobile .ds-drawer__header` / `__body` use tighter padding to match the existing mobile-rail spacing pattern. ### Notes - **Picking + invisible elements.** Unity's UI Toolkit doesn't disable picking on `opacity: 0` elements (and `visibility: hidden` only ships from Unity 6.0 on, with some quirks across panel scales). Code-side `pickingMode` toggling is the simplest robust path; the runtime helper does it for you. If you wire your own drawer without the helper, set `closer.pickingMode = PickingMode.Ignore` while the drawer is closed. - **Push-mode `width` vs `max-width`.** The drawer animates `max-width` (or `max-height`) instead of `width` (`height`) directly — the inner chrome stays at its authored size while the clip animates down to zero, which avoids a one-frame "0-px layout flash" of the inner header / body and keeps the icon + title from re-shuffling per frame during the transition. ## [1.2.0] — 2026-05-05 HiDPI sizing fix for the live web showcase (Retina Macs, iPhones, iPads were rendering every component at 1/DPR of its declared size), mobile-narrow heading wrap, themed + crisp dropdown popup, and keyboard + gamepad navigation through the Unity Input System. Same showcase URL — `https://sinanata.github.io/unity-ui-document-design-system/` — refresh to see the new build. ### Added - **Keyboard + gamepad navigation.** `ShowcaseBootstrap.EnsureInputSystem` spawns an `EventSystem` + `InputSystemUIInputModule` before the showcase UIDocuments are created. No actions are pre-assigned; `InputSystemUIInputModule.OnEnable` auto-calls `AssignDefaultActions()` (`com.unity.inputsystem@1.18`, line 1646) which wires Tab + arrows + Enter + Escape + gamepad D-pad / left stick / South (A) / East (B) buttons to UI Toolkit's `NavigationMoveEvent` / `NavigationSubmitEvent` / `NavigationCancelEvent` — Unity's `Button`, `Toggle`, `TextField`, `Slider`, `DropdownField` all respond out of the box. The bridge is idempotent: if a host project already has its own `EventSystem`, the bootstrap leaves it alone. - **`Assets/Showcase/Resources/ShowcaseFocusRing.uss`** — focus-visible stylesheet loaded after `ShowcaseTheme.uss`. `.ds-btn` / `.ds-btn--:focus` lights up the existing 1-px transparent border in `var(--color-primary)` so the ring appears *inside* the button box (no layout shift). `.ds-tab`, `.ds-pagination__btn`, `.ds-stepper__btn`, `.ds-view-toggle__btn` use `border-width: 0` in their base styles, so the focus state is communicated via a `var(--color-surface-hover)` background wash instead — the existing `transition-property: background-color` smooths the swap. Toggle / check focus paints on the inner `.unity-toggle__checkmark` track because `:focus-visible` isn't a Unity USS pseudo-class. `.ds-input` already had `:focus { border-color: var(--color-primary) }` in `Inputs.uss`, no change needed. - **`SetInitialFocus`** in `ShowcaseBootstrap`. Anchors focus on the `theme-toggle` Toggle (or the `promo-github` button as a fallback) one frame after layout resolves, so the first gamepad / Tab keypress moves focus from a known starting point instead of doing nothing. - **`Assets/Showcase/Runtime/WebGLDevicePixelRatio.jslib`** — minimal `mergeInto(LibraryManager.library, { LoLDS_GetDevicePixelRatio: () => window.devicePixelRatio || 1 })` bridge. Used by the panel-scale calculation; required because Unity's `Screen.dpi` returns 0 on WebGL when the browser doesn't expose physical PPI. Plugin importer is scoped to WebGL only. - **`GeometryChangedEvent` listener** on the showcase root so `ApplyMobileClass` re-evaluates when the browser window resizes, the mobile device rotates, or DevTools opens. Previously the mobile flip ran exactly once one frame after bootstrap; resizing past 768 px wouldn't flip the class until reload. ### Fixed - **Components rendered at half size on Retina Macs and 1/3 size on iPhones.** The WebGL template sets `canvas.width = window.innerWidth × window.devicePixelRatio` so Unity renders into a HiDPI buffer for crisp text, which means `Screen.width` reports the *buffer* pixel count (5120 on a 5K iMac with default scaling, 1290 on an iPhone 14 Pro Max), not CSS pixels. With `PanelScaleMode.ConstantPixelSize` and the default `panelSettings.scale = 1`, every `36-px` `.ds-btn` was rendering at 36 buffer-px = 18 CSS-px on DPR-2 displays and 12 CSS-px on DPR-3 phones. **Fix**: `MakePanelSettings` now sets `ps.scale = GetEffectiveDpr()`. On WebGL the DPR comes from the new `WebGLDevicePixelRatio.jslib` bridge; on Standalone (where the bridge returns nothing) it derives from `Screen.dpi / 96` and floors at 1 so non-HiDPI desktops stay unchanged. Same `36 panel-px = 36 CSS-px` rendering on every device, with the HiDPI buffer preserved for sharpness. - **Mobile layout never triggered on iPhone 14 Pro Max** (and other DPR-3 devices). `ApplyMobileClass` was comparing `Screen.width < 768` — 1290 buffer-px is never < 768, so the `.mobile` class wasn't applied even though the CSS viewport is 430 px wide. **Fix**: read `rootVisualElement.layout.width` instead, which is in panel coordinates = CSS pixels on every device (because `ps.scale = DPR`). Falls back to `Screen.width / DPR` only if layout hasn't resolved yet. - **Doc-overlay panel docked off-screen on every Retina display** once the panel-scale fix landed. `ShowcaseDocOverlay.PositionPanelNear` was clamping with `if (left + PANEL_WIDTH > Screen.width)` — that mixes panel-coordinate `left` (CSS-px) with `Screen.width` (buffer-px), pushing the panel ~`(DPR - 1) × screen-width` past the right edge on Retina. **Fix**: read `_overlayDoc.rootVisualElement.layout.width / .height` for clamping; both `worldBound` and `Screen` are now in the same unit space. - **Stale `Screen.width` semantics comment** in the WebGL template's `