diff --git a/app.js b/app.js
new file mode 100644
index 0000000..0a34bdd
--- /dev/null
+++ b/app.js
@@ -0,0 +1,695 @@
+(function () {
+ "use strict";
+
+ var STORAGE_KEY = "webos-reference-lab-state";
+ var focusables = [];
+ var focusedIndex = 0;
+ var logItems = [];
+ var networkSubscribed = false;
+ var lastNetworkStatus = null;
+ var canvas = document.getElementById("motionCanvas");
+ var ctx = canvas.getContext("2d");
+ var sceneNames = ["Aurora Signal", "Ember Orbit", "Orchid Pulse"];
+ var themes = ["theme-aurora", "theme-ember", "theme-orchid"];
+ var state = readStorage();
+ var lastFrame = 0;
+ var requestFrame = window.requestAnimationFrame || function (callback) {
+ return window.setTimeout(function () {
+ callback(new Date().getTime());
+ }, 16);
+ };
+
+ function $(id) {
+ return document.getElementById(id);
+ }
+
+ function readStorage() {
+ var fallback = {
+ scene: 0,
+ theme: 0,
+ opens: 0,
+ lastAction: "Fresh install"
+ };
+
+ try {
+ var raw = window.localStorage.getItem(STORAGE_KEY);
+ if (!raw) {
+ return fallback;
+ }
+
+ var parsed = JSON.parse(raw);
+ fallback.scene = Number(parsed.scene) || 0;
+ fallback.theme = Number(parsed.theme) || 0;
+ fallback.opens = Number(parsed.opens) || 0;
+ fallback.lastAction = parsed.lastAction || fallback.lastAction;
+ return fallback;
+ } catch (error) {
+ return fallback;
+ }
+ }
+
+ function writeStorage() {
+ try {
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ } catch (error) {
+ addLog("Storage", "localStorage unavailable");
+ }
+ }
+
+ function setText(id, value) {
+ var node = $(id);
+ if (node) {
+ node.textContent = value;
+ }
+ }
+
+ function compactJson(value) {
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch (error) {
+ return String(value);
+ }
+ }
+
+ function setDetail(title, payload) {
+ setText("detailTitle", title);
+ setText("detailJson", compactJson(payload));
+ }
+
+ function addLog(label, message) {
+ var stamp = new Date();
+ var time = two(stamp.getHours()) + ":" + two(stamp.getMinutes()) + ":" + two(stamp.getSeconds());
+ logItems.unshift({
+ label: label,
+ message: message,
+ time: time
+ });
+ logItems = logItems.slice(0, 4);
+ renderLog();
+ }
+
+ function renderLog() {
+ var log = $("eventLog");
+ var html = "";
+ var i;
+
+ for (i = 0; i < logItems.length; i += 1) {
+ html += '
' + escapeHtml(logItems[i].time + " " + logItems[i].label) + ' ' + escapeHtml(logItems[i].message) + "
";
+ }
+
+ log.innerHTML = html;
+ }
+
+ function escapeHtml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function two(value) {
+ value = Number(value) || 0;
+ return value < 10 ? "0" + value : String(value);
+ }
+
+ function applyTheme() {
+ var i;
+
+ for (i = 0; i < themes.length; i += 1) {
+ document.body.classList.remove(themes[i]);
+ }
+
+ state.scene = state.scene % sceneNames.length;
+ state.theme = state.theme % themes.length;
+ document.body.classList.add(themes[state.theme]);
+ setText("sceneLabel", sceneNames[state.scene]);
+ }
+
+ function initFocus() {
+ focusables = Array.prototype.slice.call(document.querySelectorAll("[data-focusable]"));
+
+ focusables.forEach(function (node, index) {
+ node.addEventListener("focus", function () {
+ focusCard(index);
+ });
+
+ node.addEventListener("click", function () {
+ focusCard(index);
+ runAction(node.getAttribute("data-action"));
+ });
+ });
+
+ focusCard(0);
+ }
+
+ function focusCard(index) {
+ var i;
+
+ if (!focusables.length) {
+ return;
+ }
+
+ if (index < 0) {
+ index = focusables.length - 1;
+ }
+
+ if (index >= focusables.length) {
+ index = 0;
+ }
+
+ focusedIndex = index;
+ for (i = 0; i < focusables.length; i += 1) {
+ focusables[i].classList.toggle("is-focused", i === focusedIndex);
+ }
+
+ if (document.activeElement !== focusables[focusedIndex]) {
+ focusables[focusedIndex].focus();
+ }
+ }
+
+ function moveFocus(direction) {
+ var current = focusables[focusedIndex];
+ var currentRect;
+ var currentCenter;
+ var bestIndex = focusedIndex;
+ var bestScore = Infinity;
+ var i;
+
+ if (!current) {
+ return;
+ }
+
+ currentRect = current.getBoundingClientRect();
+ currentCenter = centerOf(currentRect);
+
+ for (i = 0; i < focusables.length; i += 1) {
+ var candidate = focusables[i];
+ var rect = candidate.getBoundingClientRect();
+ var center = centerOf(rect);
+ var dx = center.x - currentCenter.x;
+ var dy = center.y - currentCenter.y;
+ var primary;
+ var secondary;
+ var score;
+
+ if (i === focusedIndex) {
+ continue;
+ }
+
+ if (direction === "left" && dx >= -4) {
+ continue;
+ }
+
+ if (direction === "right" && dx <= 4) {
+ continue;
+ }
+
+ if (direction === "up" && dy >= -4) {
+ continue;
+ }
+
+ if (direction === "down" && dy <= 4) {
+ continue;
+ }
+
+ primary = direction === "left" || direction === "right" ? Math.abs(dx) : Math.abs(dy);
+ secondary = direction === "left" || direction === "right" ? Math.abs(dy) : Math.abs(dx);
+ score = primary * 8 + secondary;
+
+ if (score < bestScore) {
+ bestScore = score;
+ bestIndex = i;
+ }
+ }
+
+ if (bestIndex !== focusedIndex) {
+ focusCard(bestIndex);
+ }
+ }
+
+ function centerOf(rect) {
+ return {
+ x: rect.left + rect.width / 2,
+ y: rect.top + rect.height / 2
+ };
+ }
+
+ function handleKeydown(event) {
+ var code = event.keyCode || event.which;
+ var keyName = event.key || String(code);
+
+ setText("keyValue", keyName);
+
+ if (code === 37) {
+ event.preventDefault();
+ moveFocus("left");
+ } else if (code === 38) {
+ event.preventDefault();
+ moveFocus("up");
+ } else if (code === 39) {
+ event.preventDefault();
+ moveFocus("right");
+ } else if (code === 40) {
+ event.preventDefault();
+ moveFocus("down");
+ } else if (code === 13) {
+ event.preventDefault();
+ runAction(focusables[focusedIndex].getAttribute("data-action"));
+ } else if (code === 461 || code === 10009 || code === 27) {
+ event.preventDefault();
+ handleBack();
+ } else if (code === 403) {
+ cycleScene();
+ } else if (code === 404) {
+ refreshAll();
+ } else if (code === 405) {
+ runAction("storage");
+ } else if (code === 406) {
+ logItems = [];
+ renderLog();
+ }
+ }
+
+ function handleBack() {
+ addLog("Back", "platformBack requested");
+
+ if (window.webOS && typeof window.webOS.platformBack === "function") {
+ window.webOS.platformBack();
+ }
+ }
+
+ function runAction(action) {
+ state.lastAction = action;
+
+ if (action === "clock") {
+ getClock();
+ } else if (action === "device") {
+ getDeviceInfo();
+ } else if (action === "network") {
+ getNetworkStatus();
+ } else if (action === "launch") {
+ launchBrowser();
+ } else if (action === "storage") {
+ saveLocalState();
+ } else if (action === "motion") {
+ cycleScene();
+ }
+
+ writeStorage();
+ }
+
+ function refreshAll() {
+ loadAppInfo();
+ loadLaunchParams();
+ getClock();
+ getDeviceInfo();
+ getNetworkStatus();
+ getLGUDID();
+ addLog("Refresh", "runtime probes requested");
+ }
+
+ function loadAppInfo() {
+ var fallback = {
+ id: window.webOS && window.webOS.fetchAppId ? window.webOS.fetchAppId() : "browser.preview",
+ title: "webOS TV Reference Lab",
+ root: window.webOS && window.webOS.fetchAppRootPath ? window.webOS.fetchAppRootPath() : window.location.href
+ };
+
+ if (window.webOS && typeof window.webOS.fetchAppInfo === "function") {
+ window.webOS.fetchAppInfo(function (info) {
+ var payload = info || fallback;
+ payload.root = fallback.root;
+ setText("appTitle", payload.title || payload.id || "App");
+ setDetail("App Info", payload);
+ addLog("App", "appinfo.json loaded");
+ });
+ } else {
+ setText("appTitle", fallback.title);
+ setDetail("App Info", fallback);
+ }
+
+ setText("platformValue", platformLabel());
+ }
+
+ function platformLabel() {
+ var platform = window.webOS && window.webOS.platform ? window.webOS.platform : {};
+ var labels = [];
+ var key;
+
+ for (key in platform) {
+ if (platform.hasOwnProperty(key) && platform[key]) {
+ labels.push(key);
+ }
+ }
+
+ if (window.PalmSystem && window.PalmSystem.deviceInfo) {
+ labels.push("PalmSystem");
+ }
+
+ return labels.length ? labels.join(", ") : "Browser";
+ }
+
+ function loadLaunchParams() {
+ var params = {};
+
+ if (window.webOSDev && typeof window.webOSDev.launchParams === "function") {
+ params = window.webOSDev.launchParams();
+ }
+
+ if (!params || Object.keys(params).length === 0) {
+ params = {
+ mode: "preview",
+ scene: sceneNames[state.scene],
+ opens: state.opens
+ };
+ }
+
+ setText("launchParams", compactJson(params));
+ }
+
+ function getClock() {
+ if (window.webOS && window.webOS.service) {
+ window.webOS.service.request("luna://com.palm.systemservice", {
+ method: "clock/getTime",
+ parameters: {},
+ onSuccess: function (args) {
+ var payload = {
+ utc: args.utc,
+ localtime: args.localtime,
+ timezone: args.timezone,
+ source: "LS2"
+ };
+ setText("clockValue", formatNow());
+ setDetail("System Clock", payload);
+ addLog("Clock", "LS2 response received");
+ },
+ onFailure: function (args) {
+ useBrowserClock(args);
+ }
+ });
+ } else {
+ useBrowserClock({
+ errorText: "PalmServiceBridge is not available"
+ });
+ }
+ }
+
+ function useBrowserClock(error) {
+ var payload = {
+ local: new Date().toString(),
+ timezone: window.webOS && window.webOS.systemInfo ? window.webOS.systemInfo().timezone : "browser",
+ source: "Browser fallback",
+ error: error && (error.errorText || error.errorCode)
+ };
+
+ setText("clockValue", formatNow());
+ setDetail("System Clock", payload);
+ addLog("Clock", "browser fallback used");
+ }
+
+ function formatNow() {
+ var now = new Date();
+ return two(now.getHours()) + ":" + two(now.getMinutes());
+ }
+
+ function getDeviceInfo() {
+ if (window.webOS && typeof window.webOS.deviceInfo === "function") {
+ window.webOS.deviceInfo(function (info) {
+ var payload = info || {};
+ payload.webOSTVjs = window.webOS.libVersion || "unknown";
+ setDetail("Device Info", payload);
+ addLog("Device", payload.modelName || "device info updated");
+ });
+ } else {
+ setDetail("Device Info", {
+ userAgent: window.navigator.userAgent,
+ screen: window.screen.width + "x" + window.screen.height,
+ webOSTVjs: "not loaded"
+ });
+ addLog("Device", "browser info shown");
+ }
+ }
+
+ function getNetworkStatus() {
+ if (networkSubscribed) {
+ if (lastNetworkStatus) {
+ setDetail("Connection", lastNetworkStatus);
+ }
+ addLog("Network", "subscription already active");
+ return;
+ }
+
+ networkSubscribed = true;
+
+ if (window.webOSDev && window.webOSDev.connection) {
+ window.webOSDev.connection.getStatus({
+ subscribe: true,
+ onSuccess: function (status) {
+ var online = status.isInternetConnectionAvailable || status.wired || status.wifi || window.navigator.onLine;
+ lastNetworkStatus = status;
+ setText("networkValue", online ? "Online" : "Offline");
+ setDetail("Connection", status);
+ addLog("Network", "connection status updated");
+ },
+ onFailure: function (error) {
+ networkSubscribed = false;
+ useBrowserNetwork(error);
+ }
+ });
+ } else {
+ networkSubscribed = false;
+ useBrowserNetwork({
+ errorText: "webOSDev.connection is not available"
+ });
+ }
+ }
+
+ function useBrowserNetwork(error) {
+ var payload = {
+ onLine: window.navigator.onLine,
+ source: "Browser fallback",
+ error: error && (error.errorText || error.errorCode)
+ };
+
+ lastNetworkStatus = payload;
+ setText("networkValue", payload.onLine ? "Online" : "Offline");
+ setDetail("Connection", payload);
+ addLog("Network", "browser status shown");
+ }
+
+ function getLGUDID() {
+ if (window.webOSDev && typeof window.webOSDev.LGUDID === "function") {
+ window.webOSDev.LGUDID({
+ onSuccess: function (payload) {
+ addLog("LGUDID", maskId(payload.id));
+ },
+ onFailure: function () {
+ addLog("LGUDID", "not available in this runtime");
+ }
+ });
+ }
+ }
+
+ function maskId(id) {
+ if (!id || id.length < 8) {
+ return "hidden";
+ }
+
+ return id.slice(0, 4) + "..." + id.slice(id.length - 4);
+ }
+
+ function launchBrowser() {
+ var target = "https://webostv.developer.lge.com/";
+ var payload = {
+ id: "com.webos.app.browser",
+ target: target
+ };
+
+ setDetail("Launch Browser", payload);
+
+ if (window.webOSDev && typeof window.webOSDev.launch === "function" && window.PalmServiceBridge) {
+ window.webOSDev.launch({
+ id: window.webOSDev.APP.BROWSER,
+ params: {
+ target: target
+ },
+ onSuccess: function () {
+ addLog("Launch", "browser launch requested");
+ },
+ onFailure: function (error) {
+ addLog("Launch", error.errorText || "launch failed");
+ }
+ });
+ } else {
+ window.open(target, "_blank");
+ addLog("Launch", "window.open fallback");
+ }
+ }
+
+ function saveLocalState() {
+ state.opens += 1;
+ state.theme = (state.theme + 1) % themes.length;
+ state.lastAction = "storage";
+ applyTheme();
+ writeStorage();
+ loadLaunchParams();
+ setDetail("Local State", {
+ key: STORAGE_KEY,
+ value: state
+ });
+ addLog("Storage", "state saved");
+ }
+
+ function cycleScene() {
+ state.scene = (state.scene + 1) % sceneNames.length;
+ state.theme = state.scene;
+ applyTheme();
+ writeStorage();
+ loadLaunchParams();
+ setDetail("Canvas Scene", {
+ scene: sceneNames[state.scene],
+ canvas: canvas.width + "x" + canvas.height,
+ animation: "requestAnimationFrame"
+ });
+ addLog("Canvas", sceneNames[state.scene]);
+ }
+
+ function resizeCanvas() {
+ var rect = canvas.getBoundingClientRect();
+ var ratio = window.devicePixelRatio || 1;
+ var width = Math.max(320, Math.floor(rect.width * ratio));
+ var height = Math.max(180, Math.floor(rect.height * ratio));
+
+ if (canvas.width !== width || canvas.height !== height) {
+ canvas.width = width;
+ canvas.height = height;
+ }
+ }
+
+ function drawFrame(timestamp) {
+ paintCanvas(timestamp);
+ requestFrame(drawFrame);
+ }
+
+ function paintCanvas(timestamp) {
+ var width;
+ var height;
+ var i;
+ var t;
+
+ if (!lastFrame) {
+ lastFrame = timestamp;
+ }
+
+ resizeCanvas();
+ width = canvas.width;
+ height = canvas.height;
+ t = timestamp * 0.001;
+
+ ctx.clearRect(0, 0, width, height);
+ drawBackdrop(width, height, t);
+
+ for (i = 0; i < 9; i += 1) {
+ drawOrb(width, height, t, i);
+ }
+
+ drawBars(width, height, t);
+ lastFrame = timestamp;
+ }
+
+ function drawBackdrop(width, height, t) {
+ var gradient = ctx.createLinearGradient(0, 0, width, height);
+
+ if (state.scene === 1) {
+ gradient.addColorStop(0, "#2a1710");
+ gradient.addColorStop(0.55, "#11161b");
+ gradient.addColorStop(1, "#293227");
+ } else if (state.scene === 2) {
+ gradient.addColorStop(0, "#1b112a");
+ gradient.addColorStop(0.5, "#101620");
+ gradient.addColorStop(1, "#203226");
+ } else {
+ gradient.addColorStop(0, "#0b1720");
+ gradient.addColorStop(0.55, "#11141a");
+ gradient.addColorStop(1, "#1d2b21");
+ }
+
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, height);
+ ctx.globalAlpha = 0.18;
+ ctx.fillStyle = "#ffffff";
+
+ for (var i = 0; i < 7; i += 1) {
+ var y = (height / 7) * i + Math.sin(t + i) * 18;
+ ctx.fillRect(0, y, width, 1);
+ }
+
+ ctx.globalAlpha = 1;
+ }
+
+ function drawOrb(width, height, t, index) {
+ var colors = [
+ ["#6ce3cf", "#f3cf56"],
+ ["#ff7c67", "#83d96c"],
+ ["#c49cff", "#6aa9ff"]
+ ];
+ var palette = colors[state.scene];
+ var radius = width * (0.035 + (index % 3) * 0.012);
+ var x = width * (0.12 + index * 0.095) + Math.sin(t * (0.7 + index * 0.05)) * width * 0.045;
+ var y = height * (0.48 + Math.sin(t * 0.8 + index) * 0.27);
+ var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
+
+ gradient.addColorStop(0, palette[index % 2]);
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
+ ctx.globalAlpha = 0.78;
+ ctx.fillStyle = gradient;
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ }
+
+ function drawBars(width, height, t) {
+ var x = width * 0.64;
+ var y = height * 0.22;
+ var barWidth = width * 0.035;
+ var gap = width * 0.018;
+ var i;
+
+ for (i = 0; i < 7; i += 1) {
+ var level = 0.22 + Math.abs(Math.sin(t * 1.5 + i * 0.7)) * 0.56;
+ ctx.fillStyle = i % 2 ? "rgba(255,255,255,0.26)" : "rgba(155,231,211,0.62)";
+ ctx.fillRect(x + i * (barWidth + gap), y + height * (0.52 - level), barWidth, height * level);
+ }
+ }
+
+ function init() {
+ state.opens += 1;
+ applyTheme();
+ writeStorage();
+ initFocus();
+ loadAppInfo();
+ loadLaunchParams();
+ getClock();
+ getNetworkStatus();
+ getDeviceInfo();
+ window.addEventListener("keydown", handleKeydown);
+ window.addEventListener("online", function () {
+ useBrowserNetwork();
+ });
+ window.addEventListener("offline", function () {
+ useBrowserNetwork();
+ });
+ paintCanvas(0);
+ requestFrame(drawFrame);
+ addLog("Ready", "reference lab started");
+ }
+
+ window.onerror = function (message) {
+ addLog("Error", message);
+ };
+
+ init();
+}());
diff --git a/appinfo.json b/appinfo.json
index 17d51bf..9bf9ebb 100644
--- a/appinfo.json
+++ b/appinfo.json
@@ -4,7 +4,7 @@
"vendor": "My Company",
"type": "web",
"main": "index.html",
- "title": "test app",
+ "title": "webOS TV Reference Lab",
"icon": "icon.png",
"largeIcon": "largeIcon.png"
-}
\ No newline at end of file
+}
diff --git a/index.html b/index.html
index dbb738e..246ffac 100644
--- a/index.html
+++ b/index.html
@@ -8,51 +8,108 @@ SPDX-License-Identifier: Apache-2.0
- new app
-
+
+
+ webOS TV Reference Lab
+
-
-
-
+
+
+
+
+ webOS TV
+ Reference Lab
+
-
-
-
Hello, World!
-
+
+
+ App
+ Loading
+
+
+ Clock
+ --:--
+
+
+ Network
+ Checking
+
+
+ Platform
+ Detecting
+
+
+
+
+
+
+
+
+
Aurora Signal
+
Ready
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..1d6cbf4
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,389 @@
+html,
+body {
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ background: #090b0f;
+ color: #f7f4e8;
+ font-family: Arial, Helvetica, sans-serif;
+ overflow: hidden;
+}
+
+button {
+ font: inherit;
+}
+
+.app-shell {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ height: 100%;
+ padding: 34px 42px 30px;
+}
+
+.topbar {
+ align-items: stretch;
+ display: flex;
+ flex: 0 0 auto;
+ gap: 26px;
+ justify-content: space-between;
+ min-height: 86px;
+}
+
+.brand-block {
+ min-width: 280px;
+}
+
+.eyebrow,
+.panel-label {
+ color: #9be7d3;
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 1;
+ margin: 0 0 10px;
+ text-transform: uppercase;
+}
+
+h1 {
+ font-size: 46px;
+ line-height: 1;
+ margin: 0;
+}
+
+.status-strip {
+ display: flex;
+ flex: 1 1 auto;
+ gap: 12px;
+ justify-content: flex-end;
+ min-width: 0;
+}
+
+.status-pill {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 8px;
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ justify-content: center;
+ max-width: 230px;
+ min-width: 140px;
+ padding: 14px 16px;
+}
+
+.status-pill span {
+ color: #b7bdc7;
+ font-size: 13px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.status-pill strong {
+ color: #ffffff;
+ display: block;
+ font-size: 20px;
+ line-height: 1.2;
+ margin-top: 6px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.stage-row {
+ display: flex;
+ flex: 1 1 auto;
+ gap: 20px;
+ min-height: 230px;
+}
+
+.visual-stage,
+.launch-panel,
+.detail-panel {
+ background: rgba(255, 255, 255, 0.07);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 8px;
+ box-sizing: border-box;
+}
+
+.visual-stage {
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+ position: relative;
+}
+
+#motionCanvas {
+ display: block;
+ height: 100%;
+ width: 100%;
+}
+
+.stage-overlay {
+ align-items: flex-start;
+ bottom: 18px;
+ display: flex;
+ flex-direction: column;
+ left: 22px;
+ position: absolute;
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
+}
+
+.stage-overlay p {
+ color: #c2fff3;
+ font-size: 18px;
+ font-weight: 700;
+ margin: 0 0 6px;
+ text-transform: uppercase;
+}
+
+.stage-overlay strong {
+ color: #ffffff;
+ font-size: 36px;
+ line-height: 1.05;
+}
+
+.launch-panel {
+ flex: 0 0 300px;
+ padding: 22px;
+}
+
+pre {
+ font-family: "Courier New", Courier, monospace;
+ margin: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+#launchParams {
+ color: #d7e0ee;
+ font-size: 15px;
+ line-height: 1.45;
+ max-height: 178px;
+ overflow: hidden;
+}
+
+.workbench {
+ display: flex;
+ flex: 1 1 auto;
+ gap: 20px;
+ min-height: 276px;
+}
+
+.feature-grid {
+ display: flex;
+ flex: 1 1 auto;
+ flex-wrap: wrap;
+ gap: 14px;
+ min-width: 0;
+}
+
+.feature-card {
+ background: #171b22;
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ box-sizing: border-box;
+ color: #ffffff;
+ cursor: pointer;
+ display: flex;
+ flex: 1 1 30%;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: 126px;
+ min-width: 220px;
+ outline: none;
+ padding: 18px;
+ position: relative;
+ text-align: left;
+ transform: translateZ(0);
+ transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
+}
+
+.feature-card:before {
+ background: #6ce3cf;
+ border-radius: 999px;
+ content: "";
+ height: 7px;
+ left: 18px;
+ position: absolute;
+ right: 18px;
+ top: 12px;
+}
+
+.feature-card.is-focused,
+.feature-card:focus {
+ background: #222832;
+ border-color: #ffffff;
+ box-shadow: 0 0 0 4px rgba(155, 231, 211, 0.28);
+ transform: scale(1.035);
+ z-index: 2;
+}
+
+.feature-card:active {
+ transform: scale(1.01);
+}
+
+.card-icon {
+ align-items: center;
+ align-self: flex-start;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ box-sizing: border-box;
+ color: #ffffff;
+ display: flex;
+ font-size: 18px;
+ font-weight: 800;
+ height: 44px;
+ justify-content: center;
+ margin-top: 12px;
+ width: 58px;
+}
+
+.feature-card strong {
+ display: block;
+ font-size: 25px;
+ line-height: 1.08;
+ margin-top: 12px;
+}
+
+.feature-card small {
+ color: #b9c1ce;
+ display: block;
+ font-size: 14px;
+ line-height: 1.25;
+ margin-top: 8px;
+}
+
+.accent-cyan:before {
+ background: #6ce3cf;
+}
+
+.accent-green:before {
+ background: #83d96c;
+}
+
+.accent-yellow:before {
+ background: #f3cf56;
+}
+
+.accent-red:before {
+ background: #ff7c67;
+}
+
+.accent-purple:before {
+ background: #c49cff;
+}
+
+.accent-blue:before {
+ background: #6aa9ff;
+}
+
+.detail-panel {
+ display: flex;
+ flex: 0 0 390px;
+ flex-direction: column;
+ gap: 14px;
+ padding: 22px;
+}
+
+.detail-head {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ flex: 0 0 auto;
+ padding-bottom: 16px;
+}
+
+.detail-head strong {
+ color: #ffffff;
+ display: block;
+ font-size: 28px;
+ line-height: 1.1;
+}
+
+.json-view {
+ background: rgba(0, 0, 0, 0.22);
+ border-radius: 8px;
+ box-sizing: border-box;
+ color: #f1f7ff;
+ flex: 1 1 auto;
+ font-size: 15px;
+ line-height: 1.42;
+ min-height: 96px;
+ overflow: hidden;
+ padding: 14px;
+}
+
+.event-log {
+ display: flex;
+ flex: 0 0 92px;
+ flex-direction: column;
+ gap: 7px;
+ justify-content: flex-end;
+ overflow: hidden;
+}
+
+.log-line {
+ color: #c7cfda;
+ font-size: 14px;
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.log-line strong {
+ color: #ffffff;
+}
+
+.theme-ember .eyebrow,
+.theme-ember .panel-label {
+ color: #ffc46b;
+}
+
+.theme-ember .feature-card.is-focused,
+.theme-ember .feature-card:focus {
+ box-shadow: 0 0 0 4px rgba(255, 196, 107, 0.28);
+}
+
+.theme-orchid .eyebrow,
+.theme-orchid .panel-label {
+ color: #dfb3ff;
+}
+
+.theme-orchid .feature-card.is-focused,
+.theme-orchid .feature-card:focus {
+ box-shadow: 0 0 0 4px rgba(223, 179, 255, 0.28);
+}
+
+@media (max-width: 1180px) {
+ .app-shell {
+ overflow-y: auto;
+ padding: 26px;
+ }
+
+ body {
+ overflow: auto;
+ }
+
+ .topbar,
+ .stage-row,
+ .workbench {
+ flex-direction: column;
+ }
+
+ .status-strip {
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ }
+
+ .launch-panel,
+ .detail-panel {
+ flex-basis: auto;
+ }
+
+ .visual-stage {
+ min-height: 280px;
+ }
+}