Luna ships one responsive model, no fork. Drop in your UI, reference the theme, and it scales smoothly from a small phone up through tablets to an 8K desktop — with zero per-screen config on the default path. Underneath, adaptivity is split into two independent axes, the same split modern web and native frameworks make:

Keeping them separate is the whole trick: a breakpoint never resizes anything, and the scaler never changes the layout. A third axis, the safe area, keeps content clear of device chrome (notch, home indicator).
One unit, one signal. Luna pins a single unit — 1 Luna px = 1 dp (density-independent pixel, 1/160″, ≈ a web CSS pixel) — and drives everything from one number, the logical width (
Screen.width ÷ FinalScale): the width the layout actually experiences after scaling. Breakpoints key off it, on every platform, so a phone lands onsm, a tablet onmd/lg, and a desktop onlgby their real usable width — no platform sniffing in the layout path.
Luna keeps exactly one breakpoint class on the root of every registered UIView (including ones that load later), chosen by the current logical width against a small ladder. The shipped ladder:
| Breakpoint | Logical width | Typical devices |
|---|---|---|
.breakpoint-sm | < 600 | phones (portrait), narrow windows, foldable cover screens |
.breakpoint-md | 600 – 1023 | tablets (portrait), phones (landscape), small desktop windows |
.breakpoint-lg | ≥ 1024 | tablets (landscape), desktop, large monitors |
Resolution is single-active: the entry with the largest minimum ≤ the logical width wins, and only its class is on the root. Base (unprefixed) USS rules are the desktop/lg layout; you author .breakpoint-sm { … } (and optionally .breakpoint-md { … }) to reflow down.
.my-card { width: 24%; } /* base = desktop / lg */
.breakpoint-sm .my-card { width: 100%; } /* phone override, down */md and lg need no new tokens to start. The Essentials theme authors only .breakpoint-sm overrides; md and lg simply inherit the base (desktop) tokens. Because those tokens are already correct at density scale, a tablet renders the desktop arrangement at a comfortable physical size with zero extra authoring — the tablet case is handled for free. Add .breakpoint-md rules only where a tablet genuinely wants something between phone and desktop.
Real reflows from the flagship sample. Each pairs the base desktop rule with its .breakpoint-sm override:
/* Tab bar — fixed-width pills on desktop become equal-flex items on phone (TabBar.uss) */
.tab-bar__item { flex-basis: auto; width: 168px; margin: 0 var(--space-xs); }
.breakpoint-sm .tab-bar__item { flex-basis: 0; width: auto; margin: 0; } /* share the row evenly */
.tab-bar__icon { width: 96px; height: 96px; }
.breakpoint-sm .tab-bar__icon { width: 52px; height: 52px; } /* shrink to fit 5 tabs */
/* Home tab — the CTA row is right-anchored on desktop, re-centered on phone (HomeTab.uss) */
.home-tab__play-row { position: absolute; right: var(--space-3xl); bottom: var(--space-xl); }
.breakpoint-sm .home-tab__play-row { right: auto; left: 50%; translate: -50% 0; bottom: var(--space-3xl); }
/* Hero detail — a fixed 440px side column stretches full-width on phone (HeroDetail.uss) */
.hero-detail__tabs-column { width: 440px; max-width: 440px; }
.breakpoint-sm .hero-detail__tabs-column { width: 100%; max-width: 480px; }Why logical width and not raw pixels or platform? A breakpoint should react to how much room the layout has, which is neither the raw pixel count (a dense phone has more pixels than a laptop but far less room) nor the platform flag (a landscape phone and a foldable have real desktop-ish width). Dividing the physical width by the scale collapses all of that into one honest number, so the same threshold means the same thing in the editor, on device, and on desktop. A landscape phone crossing 600 logical px correctly gets the wider md arrangement; a desktop window dragged below 600 correctly gets sm. That's the web container-width model, and it's what makes tablets Just Work.
Live. The active class re-resolves automatically on resize, rotation, or a Device-Simulator swap, via a throttled (~5 Hz) screen check — you never drive it by hand. Screen metrics are read through UnityEngine.Device.Screen, so the editor Device Simulator feeds real per-device values (and exercises the mobile scaling path).
Tune the ladder on the Responsive Settings asset: it's a name → minimum-logical-width map. Add tiers (e.g. an xl at 1600) and author matching .breakpoint-xl USS, or collapse to just sm/lg if you don't want a mid tier — the runtime resolves whatever you configure.
WebGL / size-policy override. The layout ladder is always width-based, but the size policy (mobile density vs desktop fit, below) is chosen by device class, and Application.isMobilePlatform is false inside a mobile browser. For a mobile web build, force the mobile size policy so a phone browser gets density scaling instead of desktop fit:
LunaUIManager.Instance.SetDeviceClassMode(DeviceClassMode.ForceMobile); // Auto | ForceMobile | ForceDesktopOrientation. Alongside the width class, Luna keeps one orientation class — .orientation-portrait / .orientation-landscape — on every view root (the USS analog of @media (orientation: …); square counts as landscape). Width breakpoints already cover most rotation effects, since rotating changes the logical width, so reach for orientation only when a rule is genuinely about orientation rather than room — e.g. pinning a HUD rail to the long edge. It compounds with the ladder:
.hud-rail { flex-direction: row; } /* landscape: along the top */
.orientation-portrait .hud-rail { flex-direction: column; } /* portrait: down the side */
.breakpoint-sm.orientation-landscape ... { ... } /* phone AND landscape */A breakpoint chooses the arrangement; the scaler (LunaUIScaler) chooses the size and writes it to PanelSettings.scale. Unity hands you raw physical pixels (unlike the browser/OS, which give you a pre-scaled logical pixel), so Luna reconstructs the right per-device scale — and is the single scale authority (see the panel note below).
scale = dpi ÷ 160. The 160 is the unit contract (1 px = 1 dp), not a tunable knob — every token and utility is authored against it, so it's a const, not a serialized field. Overall-size taste lives in the token values and the player UI-Scale, never here.
Screen.dpi is often mis-reported, so Luna reads DisplayMetrics.densityDpi via JNI (the value every native app sizes with); iOS and the editor Simulator use Screen.dpi directly; WebGL already reports devicePixelRatio × 96.[50, 700]), Luna assumes a ~400 dp short side (scale = shortSidePx ÷ 400, clamped 1–3.5) instead of a fixed dpi — accurate within ~±15% for nearly every phone, and it can never collapse the UI to a fraction of its size on a misreporting device.MobileMinLogicalWidth (default 360 dp) across — so a foldable's cover screen still fits. Only ever shrinks.scale = clamp( min(width ÷ 1920, height ÷ 1080), 0.75, 2.0 ) against a 1920×1080 reference. It's a FIT: the constrained axis binds, so the reference never overflows; an ultrawide gets extra live canvas on the long axis rather than letterbox bars. 0.75 is the legibility floor — below it the UI stops shrinking and the layout reflows instead (the logical viewport grows past the reference, dropping the window into md/sm), so a small window stays readable rather than turning tiny. 2.0 caps huge monitors (reached around 2× reference, i.e. 4K).Desktop mode is a single Advanced enum (DesktopUIScaleMode), not a per-screen choice:
| Mode | Behavior |
|---|---|
| Scale to window (default) | Window-FIT proportional scaling, clamped 0.75–2.0, as above. |
| Constant | Fixed design size (1×); pair with breakpoint reflow for a fixed-size, reflowing desktop UI. |
| Match DPI | Follows the OS display scale: dpi ÷ 96, quantized to quarter steps (100 % / 125 % / 150 % / …) and never below 1×. Falls back to 1× when dpi is unreadable. |
Device class gates size, not layout.
DeviceClassMode(Auto=Application.isMobilePlatform) picks only the size policy — mobile density vs desktop fit. A Retina / 4K-laptop desktop (~220–280 dpi) stays desktop and takes the fit path; it's never mistaken for a phone. Layout is always the width ladder.
Which panels get scaled? All on-screen ones, automatically. Luna auto-discovers the PanelSettings from your UIViews as they register and drives every one (dedup'd), including per-layer panels. World-space panels (renderMode == WorldSpace) and render-texture panels (targetTexture != null) are skipped — those size in world units or into a fixed texture, and device density must never resize them.
⚠️ Panels must be Constant Pixel Size
Luna writes
PanelSettings.scaleat runtime, and Unity applies that multiplier on top of the panel's ownScale Mode. If the panel is Scale With Screen Size or Constant Physical Size, Unity's factor multiplies with Luna's and the two compound — the classic "UI right on one device, way off on another." Only Constant Pixel Size makes Unity's factor1.0, leaving Luna the single authority.Luna enforces this: at runtime it warns once and force-corrects any driven screen-space panel that isn't Constant Pixel Size, and the debug window's Validate Panels button (also
Tools ▸ CupkekGames ▸ Luna Responsive Validate Panels) fixes the assets themselves. World-space / render-texture panels keep their own mode untouched.
Player UI-Scale:
LunaUIManager.Instance.SetUserUIScale(1.25f); // clamped 0.5–2.0, persisted (PlayerPrefs "Luna.UIScale.v2")Wire your settings-screen slider to this; the final scale is baseScale × userScale, so it multiplies on top of the per-device base in every mode. This is the one place overall size is a matter of taste.
Player text-scale (accessibility). UI-Scale zooms everything — the display-zoom knob. A separate text-only scale enlarges copy without inflating the layout, the sp / iOS Dynamic Type analog that most Unity games skip:
LunaUIManager.Instance.SetUserTextScale(130); // percent; persisted (PlayerPrefs "Luna.TextScale.v1"); 100 = offIt puts a text-scale-<percent> class on every view root; the theme restates only the --text-* tokens at that step, so type grows while controls, spacing, and icons hold. USS has no calc(), so the steps are authored as literal blocks — the Essentials theme ships 85 / 115 / 130 (with .breakpoint-sm.text-scale-* compounds for the phone canvas). Offer exactly the steps your theme authors in your settings UI, and add more .text-scale-N blocks to Tokens.uss if you want finer granularity. It rides the same class mechanism as breakpoints, so late-loaded views pick it up automatically.
Why a per-device unit? A phone is held closer than a monitor, so its "logical pixel" is denser — there's no single scale right for both. Luna's mobile unit is the dp (
dpi/160), the same thingdevicePixelRatio/ Android density / iOS@xencode under the hood; desktop uses a96/1920×1080reference. Because the unit is fixed, a 44 dp control is ~7 mm — a proper touch target — on every phone, automatically.
The Essentials sample theme ships the authoring layer you write responsive UI with. It's a starter kit you can adopt wholesale or replace — none of it is hard-wired into the runtime.
Tailwind-style utilities (layout-utilities.uss, spacing-utilities.uss, loaded globally via LunaUIDemoTheme.tss). The base class is the desktop default; the shipped breakpoint variant prefix is sm- (the phone override, active under .breakpoint-sm).
<!-- row on desktop, column on phone -->
<ui:VisualElement class="flex-row sm-flex-col" />
<!-- shown on desktop, hidden on phone -->
<ui:VisualElement class="flex sm-hidden" />Families: flex-row/col, flex-wrap/nowrap, justify-*, items-*, self-*, grow/shrink, hidden/flex, width/height fractions (w-full, w-1-2, w-1-3, w-2-3, w-1-4, w-3-4, h-full), and padding/margin on a 4px scale — p-*, px-*, py-*, m-*, mx-*, my-* (p-0 p-1 p-2 p-3 p-4 p-6 p-8 = 0/4/8/12/16/24/32 px), plus mx-auto. Each base class is the desktop default; the common steps also ship sm- phone variants. Two forced deviations from Tailwind because USS class names can't contain : or /: the prefix is sm-foo (not sm:foo) and fractions are w-1-2 (not w-1/2).
Direction vs Tailwind (read this). Tailwind is mobile-first and cumulative —
sm:means "at and above 640 px." Luna'ssm-is desktop-first and single-tier — it's the phone override only, active solely under.breakpoint-sm, and exactly one breakpoint class is ever on the root. Same vocabulary, opposite direction. Author the base class as your desktop/lglook andsm-as the phone step-down. (Need a tablet-specific tweak? Addmd-variants alongside thesm-ones and the matching.breakpoint-md— the runtime already resolvesmd.)
Real utility combos from the flagship sample — the base classes are the desktop layout; the sm- variants override on phone, so a whole screen reflows from the UXML alone with no custom USS:
<!-- Profile: two 50% columns on desktop, stacked full-width on phone -->
<ui:VisualElement class="profile-view__stats-achievements-row flex-row items-start sm-flex-col sm-items-stretch">
<ui:VisualElement class="profile-view__stats-block grow w-1-2 sm-grow-0 sm-w-full" />
<ui:VisualElement class="profile-view__achievements-block grow w-1-2 sm-grow-0 sm-w-full" />
</ui:VisualElement>
<!-- Shop: a horizontal bundle row that becomes a vertical stack on phone -->
<ui:VisualElement class="shop-tab__bundles-row flex-row items-start justify-center
sm-flex-col sm-items-stretch sm-justify-start" />
<!-- Leaderboard toolbar: trim the horizontal padding on phone -->
<ui:VisualElement class="leaderboard-tab__toolbar flex-row items-center justify-between
flex-wrap py-3 px-8 sm-px-4" />Global design tokens (Tokens.uss, also loaded via the theme). Spacing, radii, type scale, semantic text roles, the ink ladder, and component-size tokens (--control-h-*, --icon-*, --avatar-*, --badge-*, --bar-h-*, --field-w-*), plus fixed hairline widths (--line-*), all live at the panel :root, alongside --color-*. .breakpoint-sm overrides the text and component-size tokens down for the phone canvas. The phone values follow dp norms under the pinned unit — body text 16 dp, primary touch controls 44 dp+ (matching HIG 44 pt / Material 48 dp) — so don't shrink them to taste-tune overall size; that's the user-scale slider's job.
The override is just the same custom property restated under .breakpoint-sm, so every rule that reads it through var() updates at once — you tune one block, not every component:
/* Tokens.uss — desktop reference at :root, phone values under .breakpoint-sm */
:root { --text-body: 18px; --text-title: 28px; }
.breakpoint-sm { --text-body: 16px; --text-title: 20px; }
/* A component just reads the token; switching breakpoints re-flows it for free (GameTokens.uss) */
.gf-scroll-tight .unity-scroll-view__content-container { padding-left: var(--space-xl); }
.breakpoint-sm .gf-scroll-tight .unity-scroll-view__content-container { padding-left: var(--space-lg); }Why tokens are global, not per-screen. UITK voids the entire declaration if any
var()inside it is unresolved. A globally-loaded utility (.p-3 { padding: var(--space-md) }) or component rule can land on any element — including modals and transition layers that get reparented out of a screen's subtree — so its tokens must resolve panel-wide. Defining tokens once at the panel root (not per-UXML) is what guarantees borders, radii, spacing, and fonts never silently drop. Size tokens use direct literals only (never--icon-md: var(--space-xl)) for the same reason — a chained, unresolved link would void the value.
A breakpoint picks the arrangement and the scaler picks the size; the safe area keeps content clear of device chrome — notch, status bar, rounded corners, home indicator. Luna reads Screen.safeArea and pushes the resulting insets as padding onto the elements you opt in.
Opt in by class. Tag a wrapper with luna-safe-area and Luna pads it to the device insets. Edge modifiers narrow which edges are padded:
| Class | Pads |
|---|---|
luna-safe-area | all four edges |
luna-safe-area--top / --bottom / --left / --right | only that edge |
luna-safe-area--horizontal / --vertical | that pair |
Scale-correct + live. Insets are converted to panel units (divided by the active UI scale, so a dense phone doesn't get ~3× too much padding) and re-pushed on rotation/resize via the same throttled tick the scaler uses. Every registered UIView is covered, including per-layer panels; world-space / render-texture panels are untouched. Desktop and notch-less devices resolve to zero insets — the tag is simply inert there.
⚠️ The one rule: tag a dedicated wrapper
Luna writes inline padding on the tagged element, which overrides any USS padding on that same element. So put
luna-safe-areaon a wrapper whose only job is the inset, and keep your real padding on its children.This isn't a workaround — it's the correct mobile look for free. The iOS large-title nav bar wants the bar background to bleed up under the status bar while the content sits below the notch. Untagged backgrounds stay full-bleed; only the inner wrapper insets:
xml<ui:VisualElement class="top-banner luna-fx ..."> <!-- bg bleeds full-width, under the notch --> <ui:VisualElement class="luna-safe-area--top"> <!-- only the content insets --> <!-- logo, buttons, currency chips… --> </ui:VisualElement> </ui:VisualElement>Don't stack safe-area classes for the same edge up a parent → child chain, or you'll double-inset. Flat ownership — each piece of chrome owns its edge once.
In the flagship sample. GameUiHub.uxml wraps the whole shell exactly this way: one full-bleed gradient background, then the top banner and the bottom tab bar each in their own single-edge wrapper, so the chrome clears the notch and the home indicator while the background bleeds under both:
<ui:VisualElement name="ShellRoot" class="shell-root grow w-full">
<ui:VisualElement picking-mode="Ignore" class="shell-root__bg luna-fx luna-fx-matte" /> <!-- bleeds edge-to-edge -->
<ui:VisualElement class="shell-chrome shrink-0 grow-0 luna-safe-area--top"> <!-- banner clears the notch -->
<ui:Instance template="TopBanner" />
</ui:VisualElement>
<ui:VisualElement name="ContentSlot" class="shell-content grow shrink" />
<ui:VisualElement class="shell-chrome shrink-0 grow-0 luna-safe-area--bottom"> <!-- tab bar clears the home indicator -->
<ui:Instance template="TabBar" />
</ui:VisualElement>
</ui:VisualElement>Config lives on the Responsive Settings asset (the SafeAreaConfig section); it falls back to sensible defaults when no asset is assigned:
-7) or add breathing room (+8).Read it from code:
SafeAreaInsets i = LunaUIManager.Instance.SafeArea; // panel units: i.Top / i.Bottom / i.Left / i.RightThe modern move is to lean on intrinsic layout so most adaptivity needs no breakpoint at all — and it composes cleanly with the width ladder:
GridView dynamic columns — SetDynamicItemPerLine(minItemWidth, maxColumns) + RegisterDynamicItemPerLineUpdate() fit as many columns as the width allows and re-pack on resize. This is the UITK analogue of CSS repeat(auto-fit, minmax(min, 1fr)), and it's effectively a container query — the grid reacts to its own width. See GridView.flex-wrap: wrap + % widths reflow continuously.Both straight from the flagship sample — the column count and wrap points come from the available width, not a breakpoint:
// HeroesTab.cs — width-driven columns, recomputed on resize (the CSS auto-fit / minmax analogue)
_grid.SetDynamicItemPerLine(_minCardWidth, _maxColumns); // as many columns as fit
_grid.RegisterDynamicItemPerLineUpdate(); // re-pack on resize/* Inventory.uss — percentage-width tiles wrap on their own: 10-up on desktop, 3-up on phone */
.inventory .inventory__section-pairs > * { width: 9.5%; margin: 0.5%; }
.breakpoint-sm .inventory .inventory__section-pairs > * { width: 27%; margin: 3%; }
/* HeroesTab.uss — flex slots fill the row with no breakpoint: min 150px, grow to fill, cap at 240px */
.heroes-tab__list .grid-line > * { flex-grow: 1; flex-shrink: 1; flex-basis: 150px; max-width: 240px; }Reserve the breakpoints for the genuine arrangement flips; let flex / GridView handle the continuous middle. This is also why the md tier needs so little authoring — intrinsic layout already absorbs most of the tablet range.
Style a component by its own width, not the viewport's. Breakpoints react to the whole viewport. But a reusable component doesn't always get room proportional to the viewport: a HeroCard in a 3-up grid gets ~300 px; the same card dropped into a 180 px sidebar gets ~180 px — yet the viewport is lg in both cases, so the breakpoint can't tell the card is cramped. LunaContainerQuery is the fix: a manipulator that toggles classes by the element's own resolved width, the UITK analog of CSS @container / SwiftUI ViewThatFits.
heroCard.AddManipulator(new LunaContainerQuery(("cq-wide", 260f))); // (class, minWidth)….hero-card { flex-direction: column; } /* narrow: art above text */
.cq-wide .hero-card { flex-direction: row; } /* ≥260px: art beside text */Now the card re-lays-out whenever its container crosses 260 px — in a sidebar, a modal, a phone grid — and no breakpoint has to know. Tiers use the same single-active, largest-minWidth-wins semantics as the ladder (add a ("cq-narrow", 0f) tier if you want an explicit narrow class below all others). It's driven by GeometryChangedEvent but only swaps classes when the winning tier changes, so resizing within a band costs one comparison. This is GridView's dynamic-column trick generalized to any element — the last piece for authoring components that adapt anywhere without a breakpoint.
Scaling and safe area work out of the box with built-in defaults — nothing to wire. To customize them, and to tune the breakpoint ladder, create a Responsive Settings asset and assign it:
Assets ▸ Create ▸ CupkekGames ▸ Luna UI ▸ Responsive Settings.LunaUIManager ▸ Responsive Settings.The asset ships a populated sm/md/lg ladder, the size config, and the safe-area config. The Essentials sample ships a pre-configured asset and theme, so importing it gives you the whole stack already wired — including panels set to Constant Pixel Size.
Tools ▸ CupkekGames ▸ Luna Responsive — a live window showing the runtime Screen size, DPI, applied scale, logical width, active breakpoint, and resolved safe-area insets, plus:
state.txt + a device screenshot per profile to Assets/LunaResponsiveExports/, a ready-made regression matrix);It reads the runtime screen, so values are trustworthy in Play Mode and the Device Simulator. (It reports diagnostics only — it doesn't render per-breakpoint USS visuals.)
For builds, tick Log UI Scale On Boot on LunaUIManager (or call LogUIScaleState()): it logs one line with screen size, DPI (reported + validated), device-class path, final scale (base × user), logical width, active breakpoint, orientation, text-scale, and safe-area insets — readable in Player.log / logcat / Xcode.
ResponsiveDemo (Showcase ▸ Components) — drag a viewport-width slider (or the Phone / Tablet / Desktop presets) and watch a mini-storefront re-tier across sm / md / lg as the width crosses 600 and 1024, resolved through the real ladder; a separate UI-scale slider exercises the size axis independently.GridViewListViewResponsiveDemo / GridViewPaginationResponsiveDemo — width-driven dynamic columns (self-responsive). GridViewListViewResponsiveDemo also drives GridViewList's per-breakpoint config — scroller visibility / row height / scroll physics swapped on sm.// Layout — LunaResponsiveSettingsSO (the Responsive Settings asset, assigned on LunaUIManager)
public string CurrentBreakpoint { get; } // active name, "" if below all tiers
public string ClassPrefix { get; } // "breakpoint-"
public DeviceClassMode DeviceClass { get; } // Auto | ForceMobile | ForceDesktop (size policy)
public event Action<string> BreakpointChanged;
public bool ResolveIsMobile(); // size-policy decision
public void SetDeviceClassModeValue(DeviceClassMode mode);
public void SetBreakpoint(string name); // "" / null clears
public string ResolveForWidth(int logicalWidth); // ladder resolution
public void ApplyForLogicalWidth(int logicalWidth); // resolve + apply the winning class
public IEnumerable<(string name, int minWidth)> GetBreakpoints();
// Device class + breakpoint application — LunaUIManager
public LunaResponsiveSettingsSO ResponsiveSettings { get; }
public void SetDeviceClassMode(DeviceClassMode mode); // runtime override (e.g. WebGL UA sniff)
public void RefreshResponsive(); // recompute scale → ladder + orientation + scale + safe area
public void AddClassToAllUIViews(string className);
public void RemoveClassFromAllUIViews(string className);
public static string ResolveOrientationClass(int width, int height); // "orientation-portrait" | "orientation-landscape"
public const string PortraitClass = "orientation-portrait";
public const string LandscapeClass = "orientation-landscape";
// Scaling — LunaUIManager + LunaUIScaler
public void SetUserUIScale(float scale); // player slider; clamped 0.5–2.0, persisted
public float UserUIScale { get; }
public void SetUserTextScale(int percent); // text-only a11y; clamped 50–200, persisted; 100 = off
public int UserTextScale { get; }
public LunaUIScaler.Result GetUIScaleResult(); // { MobileClass, DpiValid, ValidatedDpi, BaseScale, UserScale, FinalScale, DesktopMode }
public void LogUIScaleState();
public static int LunaUIScaler.LogicalWidth(int screenWidthPx, float finalScale); // the ladder's signal
public const float LunaUIScaler.MobileReferenceDpi = 160f; // the pinned dp unit (not a knob)
public enum DesktopUIScaleMode { ScaleWithWidth, Manual, AutoDpi } // labels: Scale to window (default) / Constant / Match DPI
// Container queries — per-element width classes (add to any VisualElement)
public class LunaContainerQuery : Manipulator {
public LunaContainerQuery(params (string className, float minWidth)[] tiers);
public string CurrentClass { get; } // active class, "" below all tiers
}
// Safe area — LunaUIManager (panel units; updated on resize / rotation / config change)
public SafeAreaInsets SafeArea { get; } // .Top / .Bottom / .Left / .RightAfter boot, the breakpoint, the scale, and the safe area all update automatically on screen changes — no per-frame cost beyond a lightweight throttled check.
Runtime/Scripts/Responsive/LunaResponsiveSettingsSO.cs, Runtime/Scripts/Responsive/LunaUIScaler.cs, Runtime/Scripts/Responsive/LunaDisplayDensity.cs, Runtime/Scripts/Responsive/LunaContainerQuery.cs, Runtime/Scripts/Responsive/LunaSafeArea.cs, Runtime/Scripts/Managers/LunaUIManager.csSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction