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; + } +}