diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py
index 08ea0e0..953d9aa 100644
--- a/src/apps/dashboard/views.py
+++ b/src/apps/dashboard/views.py
@@ -317,6 +317,7 @@ def sky_view(request):
"saved_sky": request.user.sky_chart_data,
"saved_birth_dt": request.user.sky_birth_dt,
"saved_birth_place": request.user.sky_birth_place,
+ "page_class": "page-sky",
})
diff --git a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
index 15f455f..d1766ba 100644
--- a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
+++ b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
@@ -118,6 +118,10 @@ const NatusWheel = (() => {
const size = Math.min(rect.width || 400, rect.height || 400);
_cx = size / 2;
_cy = size / 2;
+ // viewBox pins the coordinate system to size×size; preserveAspectRatio
+ // centres it inside the SVG element regardless of its aspect ratio.
+ svgEl.setAttribute('viewBox', `0 0 ${size} ${size}`);
+ svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
_r = size * 0.46; // leave a small margin
R = {
diff --git a/src/functional_tests/test_my_sky.py b/src/functional_tests/test_my_sky.py
index 00cced9..9c246b2 100644
--- a/src/functional_tests/test_my_sky.py
+++ b/src/functional_tests/test_my_sky.py
@@ -5,6 +5,7 @@ natus (natal chart) interface where the user can save their personal sky
to their account (stored on the User model, independent of any game room).
"""
+from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from apps.applets.models import Applet
@@ -13,6 +14,35 @@ from apps.lyric.models import User
from .base import FunctionalTest
+# Minimal chart fixture — matches the NatusWheel data shape.
+_CHART_FIXTURE = {
+ "planets": {
+ "Sun": {"sign": "Pisces", "degree": 340.0, "retrograde": False},
+ "Moon": {"sign": "Gemini", "degree": 72.0, "retrograde": False},
+ "Mercury": {"sign": "Aquarius", "degree": 310.0, "retrograde": False},
+ "Venus": {"sign": "Aries", "degree": 10.0, "retrograde": False},
+ "Mars": {"sign": "Capricorn", "degree": 280.0, "retrograde": False},
+ "Jupiter": {"sign": "Cancer", "degree": 100.0, "retrograde": False},
+ "Saturn": {"sign": "Capricorn", "degree": 290.0, "retrograde": True},
+ "Uranus": {"sign": "Capricorn", "degree": 285.0, "retrograde": False},
+ "Neptune": {"sign": "Capricorn", "degree": 283.0, "retrograde": False},
+ "Pluto": {"sign": "Scorpio", "degree": 218.0, "retrograde": False},
+ },
+ "houses": {
+ "cusps": [10, 40, 70, 100, 130, 160, 190, 220, 250, 280, 310, 340],
+ "asc": 10.0, "mc": 100.0,
+ },
+ "elements": {"Fire": 1, "Water": 2, "Stone": 4, "Air": 1, "Time": 0, "Space": 1},
+ "aspects": [],
+ "distinctions": {
+ "1": 1, "2": 0, "3": 0, "4": 1, "5": 0, "6": 0,
+ "7": 0, "8": 0, "9": 0, "10": 4, "11": 0, "12": 0,
+ },
+ "house_system": "O",
+ "timezone": "America/New_York",
+}
+
+
class MySkyAppletTest(FunctionalTest):
"""My Sky applet appears on the dashboard and links to the sky page."""
@@ -116,3 +146,55 @@ class MySkyLocalStorageTest(FunctionalTest):
self.assertEqual(values["lon"], "-0.1278")
self.assertEqual(values["place"], "London, UK")
self.assertEqual(values["tz"], "Europe/London")
+
+
+class MySkyAppletWheelTest(FunctionalTest):
+ """Saved natal chart renders as an interactive wheel inside the My Sky applet."""
+
+ def setUp(self):
+ super().setUp()
+ Applet.objects.get_or_create(
+ slug="my-sky",
+ defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
+ )
+ self.gamer = User.objects.create(email="stargazer@test.io")
+ self.gamer.sky_chart_data = _CHART_FIXTURE
+ self.gamer.sky_birth_place = "Lindenwold, NJ, US"
+ self.gamer.save()
+
+ # ── T3 ───────────────────────────────────────────────────────────────────
+
+ def test_saved_sky_wheel_renders_with_tooltips_in_applet(self):
+ """When the user has saved sky data, the natal wheel appears in the My Sky
+ applet with working element-ring and planet tooltips."""
+ self.create_pre_authenticated_session("stargazer@test.io")
+ self.browser.get(self.live_server_url)
+
+ # 1. Wheel SVG is drawn inside the applet
+ self.wait_for(lambda: self.assertTrue(
+ self.browser.find_element(
+ By.CSS_SELECTOR, "#id_applet_my_sky .nw-root"
+ )
+ ))
+
+ # 2. Hovering an element-ring slice shows the tooltip
+ slice_el = self.browser.find_element(
+ By.CSS_SELECTOR, "#id_applet_my_sky .nw-element-group"
+ )
+ ActionChains(self.browser).move_to_element(slice_el).perform()
+ self.wait_for(lambda: self.assertEqual(
+ self.browser.find_element(By.ID, "id_natus_tooltip")
+ .value_of_css_property("display"),
+ "block",
+ ))
+
+ # 3. Hovering a planet also shows the tooltip
+ planet_el = self.browser.find_element(
+ By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-group"
+ )
+ ActionChains(self.browser).move_to_element(planet_el).perform()
+ self.wait_for(lambda: self.assertEqual(
+ self.browser.find_element(By.ID, "id_natus_tooltip")
+ .value_of_css_property("display"),
+ "block",
+ ))
diff --git a/src/static_src/scss/_natus.scss b/src/static_src/scss/_natus.scss
index 6604c7d..9d72709 100644
--- a/src/static_src/scss/_natus.scss
+++ b/src/static_src/scss/_natus.scss
@@ -507,6 +507,87 @@ body[class*="-light"] #id_natus_tooltip {
.tt-title--pu { color: rgba(var(--priPu), 1); }
}
+// ── My Sky dashboard applet ───────────────────────────────────────────────────
+
+#id_applet_my_sky {
+ display: flex;
+ flex-direction: column;
+
+ h2 { flex-shrink: 0; }
+
+ .natus-svg {
+ flex: 1;
+ min-height: 0;
+ max-width: none;
+ max-height: none;
+ align-self: center;
+ }
+}
+
+// ── Sky full page (aperture + column layout) ──────────────────────────────────
+
+html:has(body.page-sky) {
+ overflow: hidden;
+}
+
+body.page-sky {
+ overflow: hidden;
+
+ .container {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ }
+
+ .row {
+ flex-shrink: 0;
+ }
+}
+
+// Sky page fills the aperture; its content can scroll past the bottom edge
+.sky-page {
+ position: relative;
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+
+// Stack wheel above form; allow body to grow past viewport (page scrolls, not body)
+.sky-page .natus-modal-body {
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+// Wheel takes its natural square size from its width — never shrinks for the form
+.sky-page .natus-wheel-col {
+ order: -1;
+ flex: 0 0 auto;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ max-width: 480px;
+ max-height: 480px;
+ align-self: center;
+}
+
+// Form col runs horizontally below the wheel (same compact pattern as narrow-portrait modal)
+.sky-page .natus-form-col {
+ flex: 0 0 auto;
+ flex-direction: row;
+ align-items: flex-end;
+ border-right: none;
+ border-top: 0.1rem solid rgba(var(--terUser), 0.12);
+}
+
+.sky-page .natus-form-main {
+ flex: 1;
+ min-width: 0;
+ overflow-y: visible;
+}
+
// ── Sidebar z-index sink (landscape sidebars must go below backdrop) ───────────
@media (orientation: landscape) {
diff --git a/src/templates/apps/dashboard/_partials/_applet-my-sky.html b/src/templates/apps/dashboard/_partials/_applet-my-sky.html
index 4263480..bba6da5 100644
--- a/src/templates/apps/dashboard/_partials/_applet-my-sky.html
+++ b/src/templates/apps/dashboard/_partials/_applet-my-sky.html
@@ -1,6 +1,25 @@
+{% load static %}
My Sky
+ {% if request.user.sky_chart_data %}
+
+ {{ request.user.sky_chart_data|json_script:"id_my_sky_data" }}
+ {% endif %}