diff --git a/DESIGN.md b/DESIGN.md index 0bccd43..7c69d41 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,5 +1,5 @@ --- -version: "2.1" +version: "2.3" name: "ExamServer Open World — Calm Playable Map" description: "A beginner cyber range that reveals complexity only after the learner asks for it." colors: @@ -59,6 +59,48 @@ components: textColor: "{colors.text}" rounded: "{rounded.sm}" height: "44px" +missionStage: + breakpoint: "900px" + typography: + state: "0.4375rem" + micro: "0.5rem" + detail: "0.5625rem" + action: "0.625rem" + objective-mobile: "0.6875rem" + compact: "0.75rem" + objective-desktop: "0.8125rem" + node-mobile: "0.875rem" + node-desktop: "0.9375rem" + display-desktop: "clamp(30px, 3.25vw, 46px)" + display-tablet: "clamp(23px, 6.5vw, 30px)" + display-mobile: "clamp(22px, 6.4vw, 27px)" + geometry: + control: "44px" + action: "46px" + node-desktop: "220px × 90px" + unknown-desktop: "152px × 68px" + node-mobile: "66px" + selected-mobile: "78px" + route-grid-play: "42px" + route-grid-ops: "40px" + route-grid-focus: "44px" + palettes: + play: + routeInk: "#171611" + selectedBackground: "#11110E" + unknown: "#777166" + possibleEdge: "#716C62" + ops: + nodeBorder: "#31545B" + unknown: "#49636A" + routeEdge: "#71DDC4" + possibleEdge: "#879893" + focus: + unknown: "#817E75" + routeEdge: "#4B4944" + alphaLayers: + texture: [0.022, 0.028, 0.03, 0.04, 0.05, 0.07, 0.08] + shadow: [0.12, 0.18, 0.26, 0.28, 0.34, 0.35, 0.42] --- # ExamServer Open World Design Contract @@ -71,6 +113,18 @@ components: 三つの見た目は配色プリセットではない。情報の意味、操作、状態を共有しながら、異なる構図・書体・境界・動きで同じ世界を演出する。 +### Mission stage refinement + +簡素化後の初期画面を、見出し、目標、地図、選択地点が別々の帯に分かれた管理画面にしない。これらは一枚の `mission stage` として読み、上から `mission / objective / route / action` の一方向へ因果をつなぐ。 + +- mission: 画面種別と世界名を短く示す。長い説明文や独立したheroを置かない。 +- objective: mission直下の一文として置き、カード化しない。 +- route: 最大面積を渡し、ノード群が横幅の65%以上を使う。作業面全体を囲うだけの矩形を使わない。 +- action: 選択地点と `次の一手` を一つの操作単位にする。地図と無関係な全幅footerに見せない。 +- tools: 一本の引き手をmission stageの縁へ接続し、独立したボタンが空中に浮いたように見せない。 + +最初の10秒は「風切モータースの準備経路が見える → Debian標的が選択中だと分かる → 次の一手を押せる」の順に進む。説明を探す、凡例を読む、メニューを開く行為は含めない。 + ## Visual targets - `docs/design/themes/play-mission-deck.png` — PLAY / Mission Deck @@ -169,7 +223,7 @@ components: - 主基準: 1366×768。検証: 1672×941、1280×720、360×800。 - 900px未満でも初期画面へ補助情報を縦積みしない。一本の `ツール` だけを残し、選んだ情報をbottom sheetとして表示する。 - 狭いheaderもブランドと `メニュー` の一段だけにし、公開状態、画面移動、テーマ、演習終了は開いた後に表示する。 -- モバイルでは選択地点と `次の一手` を地図より前へ置き、地図一覧は残りの画面内でスクロールできる。 +- モバイルでは地図を一本の縦経路として始め、選択地点と `次の一手` を同じ行へ統合する。初期画面内に先頭二地点と選択地点を入れ、残りの経路だけを作業面内でスクロールさせる。 - 横スクロール、sticky headerによるfocus隠れ、44px未満の主要操作を許さない。 ## Rejected directions @@ -177,6 +231,9 @@ components: - generic dashboard、KPI、ranking、streak、trophy、card soup - 事実、行動、履歴、接続、設定をすべて常時表示するcontrol-room layout - glassmorphism、巨大な角丸container、無意味なgradient +- 見出し、目標、地図、選択地点をそれぞれ独立した帯やcardへ分割すること +- 巨大な矩形の中央へ小さなgraphをfitさせ、空白を作業面に見せること +- mission/mapのcomponent規則へ色やfont-sizeを直書きすること。`missionStage` のsemantic token宣言を唯一の入口にする - fake terminal、code rain、stock hacker photography - chat人格、自由文AI prompt、仮説より先に完全commandを見せること - runtimeで生成画像を背景として読み込み、offline bundleを重くすること diff --git a/TASK_CONTRACT.md b/TASK_CONTRACT.md index d84afe9..534ea63 100644 --- a/TASK_CONTRACT.md +++ b/TASK_CONTRACT.md @@ -81,3 +81,36 @@ - Automatic detection is event-based and privacy-bounded, not full command monitoring. - Live mode requires Kali internet access while Debian remains isolated. Fully offline sessions use the local telemetry API without Vercel. - 旧三列レイアウトとモバイルでの全パネル縦積みは廃止し、desktopはside drawer、narrow画面はbottom sheetへ統一する。 + +## UI refinement contract — 2026-07-29 + +### Goal + +- User request: 整理後も微妙に見えるUIを、情報量を戻さず完成品らしい作戦盤へ仕上げる。 +- Real problem being solved: 見出し、目標、地図、選択地点が別々の帯として並び、中央の広い枠に小さなノードが浮くため、簡素だが弱い画面に見える。 +- Core mechanism affected: 発見した経路が一本につながり、次の一手を自分で選ぶ瞬間。 + +### Approach + +- Chosen approach: `mission / route / action` を一枚のステージへ再構成し、地図ノードと経路を拡大する。補助情報は既存の一本のツールに残す。 +- Rejected alternative 1: 画像heroや装飾カードを追加する。地図より装飾が主役になり、認知負荷を戻すため。 +- Rejected alternative 2: 旧Mission Deckの三列・下段イベント構成へ戻す。情報は豊かでも、最初の判断が散るため。 +- Why this is coherent: 新しい機能や状態を増やさず、既存の因果関係だけを視覚的に強くする。 + +### Change shape + +- Add: 地図上の段階ラベル、選択地点とCTAを一体化する視覚構造、控えめな座標/紙面テクスチャ。 +- Remove: 地図全体を囲う重い矩形、別帯として重複する見出し・目標・選択地点の境界、過剰な空白。 +- Merge: 見出しと目標をmission headerへ、選択地点と主CTAをroute actionへ統合する。 +- Rename: UI上の主要操作名は変更しない。 +- Leave unchanged: データモデル、公開/ライブ状態、drawer、テーマ切替、focus管理、API、安全境界。 + +### Observable acceptance + +- 1280×720の初見5秒で「準備経路を見て、選択地点から次の一手へ進む画面」と説明できる。 +- 1280×720で地図ノード群が作業面の横幅の65%以上を使い、巨大な空の枠に見えない。 +- 初期表示の強い面は地図ステージと主CTAだけで、補助カテゴリ名や本文を表示しない。 +- 390×844でheader、mission、現在目標、選択地点、主CTA、地図の最初の2地点が初期画面に入る。 +- PLAY、OPS、FOCUSは同じ情報順と操作を保ち、色を外しても選択中/発見済み/未発見を区別できる。 +- 主要操作は44px以上、横スクロールなし、keyboard、Escape、focus return、reduced motionを維持する。 +- 新規runtime依存、外部font、背景写真、偽データを追加しない。 diff --git a/apps/lab-guide/.openai/hosting.json b/apps/lab-guide/.openai/hosting.json new file mode 100644 index 0000000..1adcaf4 --- /dev/null +++ b/apps/lab-guide/.openai/hosting.json @@ -0,0 +1,5 @@ +{ + "project_id": "appgprj_6a6802b720d48191864459449afa456a", + "d1": "DB", + "r2": null +} diff --git a/apps/lab-guide/src/App.test.tsx b/apps/lab-guide/src/App.test.tsx index 65462ed..147f16e 100644 --- a/apps/lab-guide/src/App.test.tsx +++ b/apps/lab-guide/src/App.test.tsx @@ -13,6 +13,7 @@ import { ManualFlagForm } from "./components/ManualFlagForm"; import { fallbackPositionFor, WORLD_POSITIONS, + worldEdgeFor, } from "./components/MapCanvas"; import { SituationConsultation } from "./components/SituationConsultation"; import { createFixtureClient } from "./fixtures"; @@ -99,7 +100,7 @@ describe("Lab guide", () => { expect( await screen.findByRole("heading", { - name: "風切モータースの世界を先に歩く", + name: "風切モータースへ接続する", }), ).toBeVisible(); expect(screen.queryByLabelText(/接続コード/)).not.toBeInTheDocument(); @@ -287,6 +288,72 @@ describe("Lab guide", () => { expect(subset[3]).not.toEqual(fallbackPositionFor("map-04")); }); + it("keeps desktop map selection keyboard-operable without focusable graph chrome", async () => { + const user = userEvent.setup(); + render( + , + ); + + const desktopNode = (id: string) => { + const node = screen + .getByTestId(`rf__node-${id}`) + .querySelector("button.world-node"); + if (!(node instanceof HTMLButtonElement)) { + throw new Error(`desktop world node ${id} is not a button`); + } + return node; + }; + + const nodeWrapper = await screen.findByTestId("rf__node-browse-kali"); + const kaliNode = desktopNode("browse-kali"); + const targetNode = desktopNode("browse-target"); + const nextAction = screen.getByTestId("next-action-map"); + + expect(kaliNode).toHaveAttribute( + "aria-label", + "Kali、観察と攻撃を行う側", + ); + expect(kaliNode).toHaveAttribute("aria-pressed", "false"); + expect(targetNode).toHaveAttribute("aria-pressed", "true"); + expect(nodeWrapper).not.toHaveAttribute("tabindex"); + expect(nodeWrapper).not.toHaveAttribute("role"); + expect(kaliNode.querySelector("button")).toBeNull(); + expect( + worldEdgeFor({ + id: "keyboard-route", + from: "browse-kali", + to: "browse-link", + state: "known", + }), + ).toMatchObject({ + selectable: false, + focusable: false, + deletable: false, + }); + expect(nextAction.closest("button")).toBe(nextAction); + expect(nextAction.closest(".world-node")).toBeNull(); + + kaliNode.focus(); + await user.keyboard("{Enter}"); + + await waitFor(() => + expect(desktopNode("browse-kali")).toHaveAttribute( + "aria-pressed", + "true", + ), + ); + expect(desktopNode("browse-target")).toHaveAttribute( + "aria-pressed", + "false", + ); + expect(screen.getByTestId("next-action-map").closest("button")).toBe( + screen.getByTestId("next-action-map"), + ); + }); + it("exercises all fourteen telemetry map IDs in the success fixture", async () => { const projection = await createFixtureClient("success").getState(); expect(projection.graph.nodes.map((node) => node.id)).toEqual( @@ -340,7 +407,7 @@ describe("Lab guide", () => { ).toBeVisible(); expect(screen.queryByLabelText("見つけたflag")).not.toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: "次の一手" })); + await user.click(screen.getByTestId("next-action-map")); expect( screen.getByRole("heading", { name: "次に確かめることを選ぶ" }), @@ -361,7 +428,7 @@ describe("Lab guide", () => { const user = userEvent.setup(); render(); - await user.click(await screen.findByRole("button", { name: "次の一手" })); + await user.click(await screen.findByTestId("next-action-map")); await user.click( screen.getByRole("button", { name: /公開ファイルに別の手掛かりがないか探す/, @@ -385,7 +452,7 @@ describe("Lab guide", () => { const user = userEvent.setup(); render(); - await user.click(await screen.findByRole("button", { name: "次の一手" })); + await user.click(await screen.findByTestId("next-action-map")); await user.click( screen.getByRole("button", { name: "相談ツールを開く" }), ); diff --git a/apps/lab-guide/src/browse.ts b/apps/lab-guide/src/browse.ts index afc9062..6863bbe 100644 --- a/apps/lab-guide/src/browse.ts +++ b/apps/lab-guide/src/browse.ts @@ -9,7 +9,7 @@ const browseProjection: LabProjection = { sessionId: "public-guide", revision: 0, status: "active", - heading: "風切モータースの世界を先に歩く", + heading: "風切モータースへ接続する", lede: "標的がなくても遊び方を確認できます。演習では発見に合わせて、この地図が変化します。", objective: "必要な機材と安全な接続範囲を理解する", @@ -89,7 +89,7 @@ const browseProjection: LabProjection = { detail: "観察と攻撃を行う側", icon: "terminal", state: "discovered", - position: { x: 40, y: 260 }, + position: { x: 30, y: 205 }, }, { id: "browse-link", @@ -97,7 +97,7 @@ const browseProjection: LabProjection = { detail: "演習専用の有線経路", icon: "network", state: "discovered", - position: { x: 285, y: 260 }, + position: { x: 300, y: 205 }, }, { id: "browse-target", @@ -105,28 +105,28 @@ const browseProjection: LabProjection = { detail: "風切モータースの業務環境", icon: "server", state: "selected", - position: { x: 535, y: 260 }, + position: { x: 590, y: 205 }, }, { id: "browse-unknown-a", label: "未発見", icon: "file", state: "undiscovered", - position: { x: 190, y: 70 }, + position: { x: 980, y: 35 }, }, { id: "browse-unknown-b", label: "未発見", icon: "file", state: "undiscovered", - position: { x: 430, y: 70 }, + position: { x: 980, y: 205 }, }, { id: "browse-unknown-c", label: "未発見", icon: "file", state: "undiscovered", - position: { x: 430, y: 455 }, + position: { x: 980, y: 375 }, }, ], edges: [ diff --git a/apps/lab-guide/src/components/ExplorationMap.tsx b/apps/lab-guide/src/components/ExplorationMap.tsx index c10292f..bceae68 100644 --- a/apps/lab-guide/src/components/ExplorationMap.tsx +++ b/apps/lab-guide/src/components/ExplorationMap.tsx @@ -72,11 +72,6 @@ export function ExplorationMap({ const [activeDisclosure, setActiveDisclosure] = useState(null); const previousRevision = useRef(projection.revision); - const selectedNode = useMemo( - () => - projection.graph.nodes.find((node) => node.id === selectedNodeId), - [projection.graph.nodes, selectedNodeId], - ); useEffect(() => { setSelectedNodeId((currentSelection) => { @@ -158,7 +153,7 @@ export function ExplorationMap({
-
+
{experience === "live" ? ( ) : null} -
-
- 探索地図 +
+
+ + {experience === "browse" ? "MISSION 00" : "LIVE MISSION"} + + 探索地図 +

{projection.heading}

+ +
+
-
-
-
- {projection.status === "complete" ? (
) : null} -
-

- 探索地図 -

- -
+
+ - {selectedNode && selectedNode.state !== "undiscovered" ? ( -
- -
- 選択中の地点 - {selectedNode.label} -
- {selectedNode.progress ? ( - - {selectedNode.progress} - - ) : null} - +
+

+ 探索地図 +

+ openConsultation()} + />
- ) : null} +
{ node: GraphNode; index: number; + onSelect: () => void; + onOpen: () => void; } type WorldFlowNode = Node; @@ -64,28 +68,111 @@ export function fallbackPositionFor(nodeId: string) { }; } +function displayPositionFor(node: GraphNode, nodeCount: number) { + const position = node.position ?? fallbackPositionFor(node.id); + if (nodeCount <= 6) return position; + + return { + x: Math.round(position.x * 1.65), + y: position.y, + }; +} + +export function worldEdgeFor( + edge: LabProjection["graph"]["edges"][number], +): Edge { + return { + id: edge.id, + source: edge.from, + target: edge.to, + type: "smoothstep", + className: `world-edge world-edge--${edge.state ?? "known"}`, + selectable: false, + focusable: false, + deletable: false, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + }, + }; +} + function WorldNode({ data, selected }: NodeProps) { const node = data.node; + const discovered = node.state !== "undiscovered"; + const stateLabel = + selected + ? "選択中" + : node.state === "discovered" + ? "発見済み" + : "未発見"; + return ( -
+ <> - - {node.state !== "undiscovered" ? ( - - - - ) : null} - - {node.label} - {node.detail ? {node.detail} : null} - {node.progress ? {node.progress} : null} - + {discovered ? ( + + ) : ( +
+ + + + {node.label} + +
+ )} + + + -
+ ); } @@ -95,46 +182,38 @@ interface MapCanvasProps { projection: LabProjection; selectedNodeId?: string; onSelectNode: (nodeId: string) => void; + onOpenNode: () => void; } export function MapCanvas({ projection, selectedNodeId, onSelectNode, + onOpenNode, }: MapCanvasProps) { const nodes = useMemo( () => projection.graph.nodes.map((node, index) => ({ id: node.id, type: "world", - position: node.position ?? fallbackPositionFor(node.id), - data: { node, index }, + position: displayPositionFor(node, projection.graph.nodes.length), + data: { + node, + index, + onSelect: () => onSelectNode(node.id), + onOpen: onOpenNode, + }, selected: selectedNodeId === node.id, - selectable: node.state !== "undiscovered", + selectable: false, draggable: false, - focusable: node.state !== "undiscovered", - ariaLabel: - node.state === "undiscovered" - ? "未発見の地点" - : `${node.label}${node.detail ? `、${node.detail}` : ""}`, + deletable: false, + focusable: false, })), - [projection.graph.nodes, selectedNodeId], + [onOpenNode, onSelectNode, projection.graph.nodes, selectedNodeId], ); const edges = useMemo( - () => - projection.graph.edges.map((edge) => ({ - id: edge.id, - source: edge.from, - target: edge.to, - type: "smoothstep", - className: `world-edge world-edge--${edge.state ?? "known"}`, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 16, - height: 16, - }, - })), + () => projection.graph.edges.map(worldEdgeFor), [projection.graph.edges], ); @@ -146,22 +225,20 @@ export function MapCanvas({ edges={edges} nodeTypes={nodeTypes} fitView - fitViewOptions={{ padding: 0.08, minZoom: 0.45, maxZoom: 1 }} - minZoom={0.45} - maxZoom={1} + fitViewOptions={{ padding: 0.04, minZoom: 0.42, maxZoom: 1.05 }} + minZoom={0.42} + maxZoom={1.05} nodesConnectable={false} nodesDraggable={false} - elementsSelectable + nodesFocusable={false} + edgesFocusable={false} + elementsSelectable={false} panOnDrag={false} zoomOnDoubleClick={false} zoomOnPinch={false} zoomOnScroll={false} preventScrolling={false} proOptions={{ hideAttribution: true }} - onNodeClick={(_, node) => { - const graphNode = node.data.node; - if (graphNode.state !== "undiscovered") onSelectNode(node.id); - }} > @@ -176,23 +253,40 @@ export function MapCanvas({ 未発見
) : ( - + + {selectedNodeId === node.id ? ( + + ) : null} + )} ))} diff --git a/apps/lab-guide/src/styles.css b/apps/lab-guide/src/styles.css index d4a1b6c..9db17d6 100644 --- a/apps/lab-guide/src/styles.css +++ b/apps/lab-guide/src/styles.css @@ -1,6 +1,5 @@ @import "./styles/base.css"; @import "./styles/shared.css"; -@import "./styles/map.css"; @import "./styles/consultation.css"; @import "./styles/responsive.css"; @import "./styles/themes.css"; @@ -8,3 +7,4 @@ @import "./styles/disclosure-themes.css"; @import "./styles/disclosure-responsive.css"; @import "./styles/calm-shell.css"; +@import "./styles/map.css"; diff --git a/apps/lab-guide/src/styles/calm-shell.css b/apps/lab-guide/src/styles/calm-shell.css index a1cc101..0012a3c 100644 --- a/apps/lab-guide/src/styles/calm-shell.css +++ b/apps/lab-guide/src/styles/calm-shell.css @@ -240,7 +240,6 @@ /* Main task surface ----------------------------------------------------- */ -:root[data-theme] .map-screen, :root[data-theme] .consultation-screen { display: block; height: calc(100dvh - var(--header-height)); @@ -248,7 +247,6 @@ overflow: hidden; } -:root[data-theme] .map-stage, :root[data-theme] .consultation-stage { width: 100%; height: 100%; @@ -257,15 +255,6 @@ background-color: var(--canvas); } -:root[data-theme] .map-stage { - display: flex; - flex-direction: column; - gap: 12px; - max-width: 1540px; - padding: clamp(14px, 2.2vh, 22px) clamp(18px, 3.2vw, 48px) 16px; - overflow: hidden; -} - :root[data-theme] .consultation-stage { display: block; max-width: 920px; @@ -280,14 +269,12 @@ align-items: end; } -:root[data-theme] .stage-intro, :root[data-theme] .consultation-intro { min-width: 0; padding: 0; border: 0; } -:root[data-theme] .stage-intro::before, :root[data-theme] .consultation-intro::before { display: none; } @@ -302,7 +289,6 @@ letter-spacing: 0.1em; } -:root[data-theme] .stage-intro h1, :root[data-theme] .consultation-intro h1 { margin: 0; font-family: var(--heading); @@ -378,88 +364,6 @@ display: none; } -/* Map gets the remaining viewport -------------------------------------- */ - -.map-workspace { - position: relative; - flex: 1 1 auto; - display: flex; - min-width: 0; - min-height: 260px; - overflow: hidden; - background: color-mix(in srgb, var(--surface) 56%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 72%, transparent); - border-radius: 5px; - flex-direction: column; -} - -:root[data-theme="play"] .map-workspace { - background: color-mix(in srgb, var(--surface) 68%, transparent); - border: 2px solid #11110e; - border-radius: 2px; -} - -:root[data-theme="ops"] .map-workspace { - border-color: #203b42; - border-radius: 2px; -} - -:root[data-theme="focus"] .map-workspace { - background: transparent; - border-width: 1px 0; - border-radius: 0; -} - -:root[data-theme] .map-workspace .map-canvas { - flex: 1 1 auto; - width: 100%; - height: 100%; - min-height: 0; - margin: 0; -} - -:root[data-theme] .selected-node-brief { - flex: 0 0 auto; - display: grid; - grid-template-columns: 40px minmax(0, 1fr) auto auto; - gap: 11px; - align-items: center; - min-height: 62px; - padding: 8px 10px; - margin: 0; - border-width: 1px; - border-radius: 4px; - box-shadow: none; -} - -:root[data-theme] .selected-node-symbol { - width: 38px; - height: 38px; -} - -:root[data-theme] .selected-node-symbol svg { - width: 22px; - height: 22px; -} - -:root[data-theme] .selected-node-copy small { - font-size: 8px; -} - -:root[data-theme] .selected-node-copy strong { - margin-top: 1px; - font-size: 13px; -} - -:root[data-theme] .selected-node-progress { - font-size: 9px; -} - -:root[data-theme] .selected-node-brief > button { - min-height: 44px; - box-shadow: none; -} - /* Drawer navigation appears only after the user asks ------------------- */ .tool-shelf-nav { @@ -616,14 +520,12 @@ padding: 0; } - :root[data-theme] .map-screen, :root[data-theme] .consultation-screen { height: calc(100dvh - var(--header-height)); min-height: 0; overflow: hidden; } - :root[data-theme] .map-stage, :root[data-theme] .consultation-stage { width: 100%; height: 100%; @@ -632,15 +534,10 @@ overflow-y: auto; } - :root[data-theme] .map-stage { - display: flex; - } - .stage-heading-row { gap: 10px; } - :root[data-theme] .stage-intro h1, :root[data-theme] .consultation-intro h1 { font-size: clamp(20px, 6vw, 25px); } @@ -655,31 +552,6 @@ width: auto; } - .map-workspace { - order: 4; - flex-basis: 300px; - min-height: 300px; - overflow-y: auto; - overscroll-behavior: contain; - } - - :root[data-theme] .selected-node-brief { - order: 3; - } - - .map-stage > .success-callout { - order: 2; - } - - .map-stage > .stage-heading-row { - order: 0; - } - - .map-stage > .stage-context, - .map-stage > .connection-banner { - order: 1; - } - :root[data-theme] .consultation-intro { padding: 0; } @@ -727,7 +599,6 @@ font-size: 11px; } - :root[data-theme] .map-stage, :root[data-theme] .consultation-stage { gap: 10px; padding: 12px; @@ -753,31 +624,6 @@ font-size: 11px; } - :root[data-theme] .selected-node-brief { - grid-template-columns: minmax(0, 1fr) auto; - gap: 8px; - min-height: 58px; - padding: 7px 8px; - } - - :root[data-theme] .selected-node-symbol, - :root[data-theme] .selected-node-progress { - display: none; - } - - :root[data-theme] .selected-node-brief > button { - grid-column: auto; - justify-self: end; - width: auto; - min-height: 44px; - padding-inline: 11px; - } - - .map-workspace { - flex-basis: 280px; - min-height: 280px; - } - :root[data-theme] .hypothesis-fieldset { padding-top: 16px; } diff --git a/apps/lab-guide/src/styles/disclosure-responsive.css b/apps/lab-guide/src/styles/disclosure-responsive.css index 4850b56..52ec178 100644 --- a/apps/lab-guide/src/styles/disclosure-responsive.css +++ b/apps/lab-guide/src/styles/disclosure-responsive.css @@ -1,25 +1,11 @@ /* Disclosure layout refinements ----------------------------------------- */ @media (min-width: 901px) and (max-height: 780px) { - .map-screen, .consultation-screen { display: block; grid-template: none; } - .map-stage { - padding-top: 16px; - } - - .stage-intro h1 { - margin-bottom: 4px; - font-size: clamp(23px, 1.8vw, 28px); - } - - .stage-intro p { - font-size: 13px; - } - .stage-context { min-height: 48px; margin-top: 10px; @@ -29,14 +15,6 @@ min-height: 49px; margin-top: 4px; } - - .map-toolbar { - min-height: 42px; - } - - .map-canvas { - min-height: 240px; - } } @media (max-width: 900px) { @@ -62,7 +40,6 @@ width: 218px; } - .map-screen, .consultation-screen { display: block; height: auto; @@ -70,7 +47,6 @@ overflow: visible; } - .map-stage, .consultation-stage { order: initial; width: 100%; @@ -89,17 +65,6 @@ margin-top: 12px; } - .map-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 8px; - } - - .map-toolbar h2 { - margin: 0; - } - .disclosure-pulls { margin-inline: -18px; padding-inline: 18px; @@ -187,13 +152,11 @@ font-size: 9px; } - .stage-intro h1, .consultation-intro h1 { font-size: 24px; line-height: 1.35; } - .stage-intro p, .consultation-intro p { font-size: 13px; } @@ -231,14 +194,6 @@ overflow: visible; } - .map-toolbar h2 { - font-size: 17px; - } - - .map-legend-details > summary { - padding-inline: 9px; - } - .disclosure-drawer .session-pairing { grid-template-columns: 40px minmax(0, 1fr); gap: 10px; diff --git a/apps/lab-guide/src/styles/disclosure-shell.css b/apps/lab-guide/src/styles/disclosure-shell.css index 7852e53..1f727f6 100644 --- a/apps/lab-guide/src/styles/disclosure-shell.css +++ b/apps/lab-guide/src/styles/disclosure-shell.css @@ -31,8 +31,7 @@ user-select: none; } -.theme-menu > summary::-webkit-details-marker, -.map-legend-details > summary::-webkit-details-marker { +.theme-menu > summary::-webkit-details-marker { display: none; } @@ -103,7 +102,6 @@ font-size: 11px; } -.map-screen, .consultation-screen { display: block; grid-template: none; @@ -112,7 +110,6 @@ overflow: hidden; } -.map-stage, .consultation-stage { order: initial; width: 100%; @@ -122,13 +119,6 @@ background-color: var(--canvas); } -.map-stage { - max-width: 1540px; - padding: 24px clamp(22px, 3vw, 48px) 16px; - overflow: hidden; -} - -.stage-intro h1, .consultation-intro h1 { text-wrap: balance; } @@ -265,53 +255,6 @@ font-size: 9px; } -.map-toolbar { - position: relative; - min-height: 48px; - margin-top: 2px; -} - -.map-legend-details { - position: relative; - z-index: 12; -} - -.map-legend-details > summary { - display: grid; - min-height: 40px; - padding: 0 11px; - color: var(--muted-text); - background: var(--surface); - border: 1px solid var(--border); - border-radius: 3px; - font-size: 11px; - font-weight: 680; - list-style: none; - place-items: center; - cursor: pointer; -} - -.map-legend-details[open] > summary { - color: var(--text); - border-color: var(--primary); -} - -.map-legend-details .map-legend { - position: absolute; - top: calc(100% + 7px); - right: 0; - display: grid; - gap: 9px; - width: 150px; - padding: 11px; - color: var(--text); - white-space: normal; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 3px; - box-shadow: 0 12px 34px var(--ink-shadow); -} - .disclosure-layer { position: fixed; z-index: 100; diff --git a/apps/lab-guide/src/styles/map.css b/apps/lab-guide/src/styles/map.css index 5758cff..c37a086 100644 --- a/apps/lab-guide/src/styles/map.css +++ b/apps/lab-guide/src/styles/map.css @@ -1,55 +1,223 @@ -.map-screen { - display: grid; - grid-template-columns: clamp(260px, 20.1vw, 336px) minmax(0, 1fr) clamp(300px, 22.9vw, 384px); - grid-template-rows: minmax(0, 1fr) 108px; - height: calc(100dvh - var(--header-height)); -} +/* Mission stage --------------------------------------------------------- */ -.map-screen .known-facts { - grid-row: 1 / -1; +:root[data-theme] .map-screen { + display: block; + height: calc(100dvh - var(--header-height)); + min-height: 0; + overflow: hidden; } -.map-stage { +:root[data-theme] .mission-stage { + --mission-type-state: 0.4375rem; + --mission-type-micro: 0.5rem; + --mission-type-detail: 0.5625rem; + --mission-type-action: 0.625rem; + --mission-type-objective-mobile: 0.6875rem; + --mission-type-compact: 0.75rem; + --mission-type-objective-desktop: 0.8125rem; + --mission-type-node-mobile: 0.875rem; + --mission-type-node-desktop: 0.9375rem; + --mission-type-display-desktop: clamp(30px, 3.25vw, 46px); + --mission-type-display-tablet: clamp(23px, 6.5vw, 30px); + --mission-type-display-mobile: clamp(22px, 6.4vw, 27px); display: flex; flex-direction: column; - min-width: 0; + gap: 0; + width: min(100%, 1500px); + height: 100%; min-height: 0; - padding: 30px 30px 0; + padding: clamp(14px, 2.2vh, 22px) clamp(24px, 3.4vw, 50px) 18px; + margin: 0 auto; overflow: hidden; background: var(--canvas); } -.stage-intro h1 { - margin-bottom: 8px; - font-family: var(--heading); - font-size: clamp(25px, 2vw, 31px); - font-weight: 650; - letter-spacing: 0.015em; +.mission-header { + position: relative; + z-index: 2; + flex: 0 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: clamp(24px, 4vw, 64px); + align-items: end; + padding: 0 4px 18px; } -.stage-intro p { - margin-bottom: 0; +.mission-heading { + min-width: 0; +} + +.mission-kicker { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 5px; color: var(--muted-text); - font-size: 15px; - line-height: 1.7; + font-family: var(--mono); + font-size: var(--mission-type-detail); + font-weight: 720; + letter-spacing: 0.11em; +} + +.mission-kicker > span:first-child { + color: var(--primary); +} + +.mission-heading h1 { + max-width: 21ch; + margin: 0; + font-family: var(--heading); + font-size: var(--mission-type-display-desktop); + font-weight: 820; + line-height: 1.16; + letter-spacing: -0.045em; + text-wrap: balance; +} + +.mission-objective { + display: grid; + grid-template-columns: 30px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + max-width: 66ch; + min-height: 38px; + padding: 0; + margin-top: 12px; + color: var(--text); + background: transparent; + border: 0; +} + +.mission-objective > svg { + width: 22px; + height: 22px; + color: var(--primary); +} + +.mission-objective small, +.mission-objective strong { + display: block; +} + +.mission-objective small { + margin-bottom: 1px; + color: var(--primary); + font-family: var(--mono); + font-size: var(--mission-type-micro); + font-weight: 760; + letter-spacing: 0.08em; +} + +.mission-objective strong { + overflow: hidden; + font-size: var(--mission-type-objective-desktop); + font-weight: 680; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mission-objective > [role="progressbar"] { + min-width: 58px; + padding: 0; + color: var(--primary); + font-family: var(--mono); + font-size: var(--mission-type-objective-mobile); + font-weight: 760; + text-align: center; + white-space: nowrap; + border: 0; +} + +.mission-objective > [role="progressbar"] > small { + display: inline; + margin: 0; + color: inherit; + font-size: inherit; +} + +.mission-tools { + position: relative; + z-index: 3; + align-self: end; + min-height: 44px; + margin-bottom: -25px; +} + +.mission-tools .disclosure-pull { + position: relative; + display: grid; + grid-template-columns: 19px auto; + gap: 8px; + align-items: center; + min-width: 126px; + min-height: 46px; + padding: 0 13px; + color: var(--text); + background: var(--surface); + border: 2px solid var(--text); + border-radius: 0; + box-shadow: 4px 4px 0 color-mix(in srgb, var(--text) 14%, transparent); + transition: + color 200ms var(--ease-out), + background-color 200ms var(--ease-out), + border-color 200ms var(--ease-out), + transform 200ms var(--ease-out); +} + +.mission-tools .disclosure-pull:hover { + color: var(--primary-hover); + transform: translateY(2px); +} + +.mission-tools .disclosure-pull:active { + transform: translateY(3px) scale(0.985); +} + +.mission-tools .disclosure-pull-cord { + position: absolute; + top: auto; + bottom: -21px; + left: 50%; + width: 2px; + height: 19px; + background: var(--text); + transform: translateX(-50%); +} + +.mission-tools .disclosure-pull-meta { + display: none; +} + +.mission-tools .disclosure-pull-icon, +.mission-tools .disclosure-pull-icon svg { + width: 19px; + height: 19px; +} + +.mission-tools .disclosure-pull-label { + font-size: var(--mission-type-objective-mobile); + font-weight: 760; + letter-spacing: 0.01em; + white-space: nowrap; } .success-callout { + flex: 0 0 auto; display: grid; - grid-template-columns: 38px minmax(0, 1fr); - gap: 13px; + grid-template-columns: 26px minmax(0, 1fr); + gap: 10px; align-items: center; - padding: 12px 15px; - margin-top: 16px; - color: var(--success); - background: color-mix(in srgb, var(--success) 5%, var(--surface)); - border: 1px solid color-mix(in srgb, var(--success) 36%, var(--border)); - border-radius: 6px; + min-height: 50px; + padding: 7px 4px 10px; + color: var(--text); + border-bottom: 1px solid var(--border); } .success-callout > svg { - width: 30px; - height: 30px; + width: 24px; + height: 24px; + color: var(--primary); } .success-callout strong, @@ -58,293 +226,988 @@ } .success-callout strong { - margin-bottom: 2px; - font-size: 14px; + font-size: var(--mission-type-objective-desktop); + font-weight: 760; + line-height: 1.35; } .success-callout span { + margin-top: 2px; color: var(--muted-text); - font-size: 12px; + font-size: var(--mission-type-objective-mobile); + line-height: 1.4; } -.map-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - min-height: 58px; - margin-top: 20px; -} +/* Route is the world, not a card --------------------------------------- */ -.map-toolbar h2 { +.mission-route { + position: relative; + flex: 1 1 auto; display: flex; - align-items: center; - gap: 11px; - margin: 0; - font-family: var(--heading); - font-size: 20px; + min-width: 0; + min-height: 330px; + padding: 26px 6px 4px; + overflow: hidden; + background: + radial-gradient(circle at 78% 48%, color-mix(in srgb, var(--primary) 7%, transparent), transparent 25%), + linear-gradient(90deg, color-mix(in srgb, var(--text) 3%, transparent) 1px, transparent 1px), + linear-gradient(color-mix(in srgb, var(--text) 3%, transparent) 1px, transparent 1px); + background-size: auto, 40px 40px, 40px 40px; + border-top: 1px solid color-mix(in srgb, var(--text) 42%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--text) 18%, transparent); } -.map-toolbar h2 svg { - width: 27px; - height: 27px; - color: var(--primary); +.mission-route::before { + position: absolute; + top: -1px; + left: 0; + width: clamp(120px, 15vw, 210px); + height: 3px; + content: ""; + background: var(--primary); } -.map-legend { +.route-caption { + position: absolute; + top: 9px; + left: 12px; + z-index: 2; display: flex; - align-items: center; - gap: 22px; + gap: 8px; + align-items: baseline; color: var(--muted-text); - font-size: 13px; - white-space: nowrap; + pointer-events: none; } -.map-legend span { - display: inline-flex; - align-items: center; - gap: 7px; +.route-caption span { + color: var(--primary); + font-family: var(--mono); + font-size: var(--mission-type-micro); + font-weight: 780; + letter-spacing: 0.14em; } -.map-legend svg { - width: 21px; - height: 21px; +.route-caption strong { + font-size: var(--mission-type-action); + font-weight: 680; } -.map-legend span:nth-child(1) svg { - color: var(--success); +.mission-map { + flex: 1 1 auto; + display: flex; + width: 100%; + min-width: 0; + min-height: 0; + overflow: hidden; } -.map-legend span:nth-child(2) svg { - color: var(--primary); +.mission-map .map-canvas { + flex: 1 1 auto; + width: 100%; + min-width: 0; + min-height: 0; + margin: 0; } -.map-canvas { - flex: 1; - min-height: 340px; - margin: 0 -4px; +.map-canvas .react-flow, +.map-canvas .react-flow__renderer { + background: transparent; } -.map-canvas .react-flow { - background: transparent; +.map-canvas .react-flow__background { + opacity: 0; } .react-flow__node-world { - width: 190px; + width: 220px; border: 0; } -.world-node { - display: flex; - gap: 8px; +.react-flow__node-world:has(.world-node--undiscovered) { + width: 152px; +} + +:root[data-theme] .world-node { + position: relative; + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 10px; align-items: center; - min-height: 86px; - padding: 13px 10px; + width: 100%; + min-height: 90px; + padding: 22px 14px 13px; color: var(--text); background: var(--surface); - border: 2px solid var(--success); - border-radius: 6px; - box-shadow: 0 1px 2px rgb(31 27 22 / 5%); + border: 2px solid color-mix(in srgb, var(--primary) 80%, var(--text)); + border-radius: 2px; + box-shadow: 6px 6px 0 color-mix(in srgb, var(--text) 12%, transparent); + transition: + color 220ms var(--ease-out), + background-color 220ms var(--ease-out), + border-color 220ms var(--ease-out), + box-shadow 220ms var(--ease-out), + transform 220ms var(--ease-out); } -.world-node--selected, -.world-node.is-focused { - border-color: var(--primary); - box-shadow: - 0 0 0 4px color-mix(in srgb, var(--warning) 25%, transparent), - 0 1px 2px rgb(31 27 22 / 5%); +:root[data-theme] button.world-node { + appearance: none; + text-align: left; } -.world-node--undiscovered { - justify-content: center; - color: var(--muted-text); - background: color-mix(in srgb, var(--surface) 60%, transparent); - border-color: var(--muted-text); - border-style: dashed; - box-shadow: none; +:root[data-theme] .world-node-index { + position: absolute; + top: 0; + right: auto; + left: 0; + min-width: 28px; + padding: 2px 5px; + color: var(--surface); + background: var(--text); + font-family: var(--mono); + font-size: var(--mission-type-micro); + font-weight: 760; + line-height: 1.2; + text-align: center; } -.world-node-icon { - flex: 0 0 auto; +:root[data-theme] .world-node-state { + position: absolute; + top: 6px; + right: 8px; + color: var(--primary-hover); + font-family: var(--mono); + font-size: var(--mission-type-state); + font-weight: 760; + letter-spacing: 0.08em; +} + +:root[data-theme] .world-node-icon { display: grid; - width: 30px; + width: 40px; + height: 40px; + color: var(--text); + background: color-mix(in srgb, var(--primary) 8%, var(--surface)); + border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border)); place-items: center; } -.world-node-icon svg { - width: 29px; - height: 29px; +:root[data-theme] .world-node-icon svg { + width: 24px; + height: 24px; } -.world-node-copy { +:root[data-theme] .world-node-copy { display: block; min-width: 0; } -.world-node-copy strong, -.world-node-copy small, -.world-node-copy em { +:root[data-theme] .world-node-copy strong, +:root[data-theme] .world-node-copy small, +:root[data-theme] .world-node-copy em { display: block; } -.world-node-copy strong { - font-size: 14px; - line-height: 1.4; +:root[data-theme] .world-node-copy strong { + overflow: hidden; + font-size: var(--mission-type-node-desktop); + font-weight: 760; + line-height: 1.3; + text-overflow: ellipsis; white-space: nowrap; } -.world-node-copy small { - margin-top: 2px; +:root[data-theme] .world-node-copy small { + display: -webkit-box; + margin-top: 3px; + overflow: hidden; color: var(--muted-text); - font-size: 11px; - line-height: 1.4; + font-size: var(--mission-type-action); + line-height: 1.42; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } -.world-node-copy em { +:root[data-theme] .world-node-copy em { width: fit-content; - padding: 1px 5px; + padding: 2px 5px; margin-top: 5px; - color: var(--success); - background: color-mix(in srgb, var(--success) 7%, var(--surface)); - border: 1px solid color-mix(in srgb, var(--success) 36%, var(--border)); - border-radius: 3px; + color: var(--primary-hover); + background: color-mix(in srgb, var(--primary) 8%, var(--surface)); + border: 1px solid color-mix(in srgb, var(--primary) 42%, var(--border)); font-family: var(--mono); - font-size: 9px; + font-size: var(--mission-type-micro); font-style: normal; } -.world-node .react-flow__handle { +:root[data-theme] .world-node--undiscovered { + display: grid; + grid-template-columns: 1fr; + min-height: 68px; + padding: 18px 12px 12px; + color: var(--muted-text); + background: color-mix(in srgb, var(--surface) 54%, transparent); + border: 1px dashed color-mix(in srgb, var(--muted-text) 82%, transparent); + box-shadow: none; +} + +:root[data-theme] .world-node--undiscovered .world-node-state { + display: none; +} + +:root[data-theme] .world-node--undiscovered .world-node-index { + color: var(--muted-text); + background: transparent; +} + +:root[data-theme] .world-node--undiscovered .world-node-copy { + text-align: center; +} + +:root[data-theme] .world-node--undiscovered .world-node-copy strong { + font-size: var(--mission-type-compact); + font-weight: 640; +} + +:root[data-theme] .world-node.is-focused { + z-index: 2; + color: var(--surface); + background: var(--text); + border-color: var(--signal); + box-shadow: + 0 0 0 4px var(--signal), + 8px 8px 0 color-mix(in srgb, var(--text) 18%, transparent); + transform: translateY(-3px); +} + +:root[data-theme] .world-node.is-focused .world-node-index { + color: var(--text); + background: var(--signal); +} + +:root[data-theme] .world-node.is-focused .world-node-state { + color: var(--signal); +} + +:root[data-theme] .world-node.is-focused .world-node-icon { + color: var(--signal); + background: color-mix(in srgb, var(--surface) 7%, transparent); + border-color: color-mix(in srgb, var(--surface) 30%, transparent); +} + +:root[data-theme] .world-node.is-focused .world-node-copy small { + color: color-mix(in srgb, var(--surface) 68%, transparent); +} + +:root[data-theme] .world-node.is-focused .world-node-copy em { + color: var(--signal); + background: transparent; + border-color: var(--signal); +} + +.react-flow__node-world > .react-flow__handle { width: 1px; height: 1px; opacity: 0; } +.world-node-toolbar { + padding: 0; + background: transparent; + border: 0; + box-shadow: none; +} + +.world-node-action { + display: grid; + grid-template-columns: auto 30px; + gap: 12px; + align-items: center; + min-width: 156px; + min-height: 46px; + padding: 4px 5px 4px 16px; + color: var(--text); + background: var(--signal); + border: 2px solid var(--text); + border-radius: 0; + box-shadow: 4px 4px 0 color-mix(in srgb, var(--text) 24%, transparent); + font-size: var(--mission-type-compact); + font-weight: 800; + transition: + transform 200ms var(--ease-out), + box-shadow 200ms var(--ease-out); +} + +.world-node-action > span { + display: grid; + width: 30px; + height: 30px; + background: color-mix(in srgb, var(--text) 10%, transparent); + place-items: center; +} + +.world-node-action svg { + width: 18px; + height: 18px; + transition: transform 200ms var(--ease-out); +} + +.world-node-action:hover { + box-shadow: 2px 2px 0 color-mix(in srgb, var(--text) 24%, transparent); + transform: translate(2px, 2px); +} + +.world-node-action:hover svg { + transform: translateX(2px); +} + +.world-node-action:active { + transform: translate(3px, 3px) scale(0.985); +} + .react-flow__edge-path { - stroke: var(--muted-text); - stroke-width: 1.7; + stroke: var(--text); + stroke-width: 2.2; } .world-edge--possible .react-flow__edge-path { - stroke-dasharray: 7 6; + stroke: color-mix(in srgb, var(--muted-text) 82%, transparent); + stroke-dasharray: 7 7; +} + +.react-flow__edge .react-flow__arrowhead polyline, +.react-flow__edge marker path { + fill: var(--text); + stroke: var(--text); } .react-flow__node:focus-visible { - outline: 3px solid color-mix(in srgb, var(--primary) 38%, white); - outline-offset: 4px; + outline: 3px solid var(--signal); + outline-offset: 6px; } .mobile-map { display: none; } -.investigation-rail { - min-width: 0; - padding: 34px 24px 22px; - overflow-y: auto; +/* Theme expression stays inside the route ------------------------------ */ + +:root[data-theme="play"] { + --mission-route-ink: #171611; + --mission-selected-background: #11110e; + --mission-unknown: #777166; + --mission-possible-edge: #716c62; +} + +:root[data-theme="ops"] { + --mission-node-border: #31545b; + --mission-unknown: #49636a; + --mission-route-edge: #71ddc4; + --mission-possible-edge: #879893; +} + +:root[data-theme="focus"] { + --mission-unknown: #817e75; + --mission-route-edge: #4b4944; +} + +:root[data-theme="play"] .mission-route { + background: + radial-gradient(circle at 76% 46%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 26%), + radial-gradient(circle at 16% 72%, color-mix(in srgb, var(--signal) 5%, transparent), transparent 20%), + linear-gradient(color-mix(in srgb, var(--text) 2.8%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in srgb, var(--text) 2.2%, transparent) 1px, transparent 1px); + background-size: auto, auto, 42px 42px, 42px 42px; + border-top-color: var(--mission-route-ink); +} + +:root[data-theme="play"] .world-node { + border-color: var(--primary); +} + +:root[data-theme="play"] .world-node--undiscovered { + border-color: var(--mission-unknown); +} + +:root[data-theme="play"] .world-node.is-focused { + color: var(--surface); + background: var(--mission-selected-background); + border-width: 3px; + border-color: var(--signal); + box-shadow: + 0 0 0 4px var(--signal), + 8px 8px 0 color-mix(in srgb, var(--text) 18%, transparent); + transform: translateY(-3px); +} + +:root[data-theme="play"] .world-node-copy em { + font-weight: 750; +} + +:root[data-theme="play"] .react-flow__edge-path { + stroke: var(--mission-route-ink); + stroke-width: 2.3; +} + +:root[data-theme="play"] .world-edge--possible .react-flow__edge-path { + stroke: var(--mission-possible-edge); + stroke-dasharray: 8 6; +} + +:root[data-theme="ops"] .mission-route { + background: + radial-gradient(circle at 74% 48%, color-mix(in srgb, var(--primary) 8%, transparent), transparent 26%), + linear-gradient(color-mix(in srgb, var(--primary) 4%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in srgb, var(--primary) 3%, transparent) 1px, transparent 1px); + background-size: auto, 40px 40px, 40px 40px; + border-color: var(--border); +} + +:root[data-theme="ops"] .mission-tools .disclosure-pull { + color: var(--text); + background: var(--surface); + border-color: var(--mission-node-border); + box-shadow: 4px 4px 0 rgb(0 0 0 / 28%); +} + +:root[data-theme="ops"] .mission-tools .disclosure-pull-cord { + background: var(--primary); +} + +:root[data-theme="ops"] .world-node { background: var(--surface); - border-left: 1px solid var(--border); + border-color: var(--mission-node-border); + box-shadow: 6px 6px 0 rgb(0 0 0 / 26%); +} + +:root[data-theme="ops"] .world-node-index { + color: var(--canvas); + background: var(--primary); +} + +:root[data-theme="ops"] .world-node-icon { + background: color-mix(in srgb, var(--primary) 7%, transparent); +} + +:root[data-theme="ops"] .world-node-copy small, +:root[data-theme="ops"] .world-node-copy em { + font-family: var(--mono); +} + +:root[data-theme="ops"] .world-node-copy em { + border-radius: 1px; +} + +:root[data-theme="ops"] .world-node--undiscovered { + background: color-mix(in srgb, var(--surface) 45%, transparent); + border-color: var(--mission-unknown); } -.rail-intro { - margin-bottom: 22px; +:root[data-theme="ops"] .world-node--undiscovered .world-node-index { color: var(--muted-text); - font-size: 13px; - line-height: 1.6; + background: transparent; } -.investigation-list { - display: grid; - gap: 14px; +:root[data-theme="ops"] .world-node.is-focused { + color: var(--text); + background: var(--canvas); + border-color: var(--signal); + box-shadow: + 0 0 0 3px color-mix(in srgb, var(--signal) 92%, transparent), + 0 12px 36px rgb(0 0 0 / 42%); } -.investigation-option { - display: grid; - grid-template-columns: 40px minmax(0, 1fr) 20px; - gap: 12px; - align-items: start; - width: 100%; - min-height: 112px; - padding: 18px 14px; - text-align: left; - background: var(--surface); - border: 1px solid var(--primary); - border-radius: 6px; +:root[data-theme="ops"] .world-node-action { + color: var(--canvas); + background: var(--signal); + border-color: var(--signal); + box-shadow: 4px 4px 0 rgb(0 0 0 / 34%); +} + +:root[data-theme="ops"] .react-flow__edge-path { + stroke: var(--mission-route-edge); + stroke-width: 1.5; + filter: drop-shadow(0 0 3px color-mix(in srgb, var(--primary) 35%, transparent)); +} + +:root[data-theme="ops"] .world-edge--possible .react-flow__edge-path { + stroke: var(--mission-possible-edge); + stroke-dasharray: 7 7; + animation: ops-route 2.8s linear infinite; } -.investigation-option:hover { - background: color-mix(in srgb, var(--primary) 4%, var(--surface)); - border-color: var(--primary-hover); +@keyframes ops-route { + to { + stroke-dashoffset: -28; + } } -.investigation-option > svg:first-child { - width: 34px; - height: 34px; +:root[data-theme="focus"] .mission-route { + background: + linear-gradient(color-mix(in srgb, var(--text) 4%, transparent) 1px, transparent 1px), + linear-gradient(90deg, transparent calc(100% - 1px), color-mix(in srgb, var(--text) 3%, transparent) 1px); + background-size: 100% 44px, 84px 100%; + border-top-color: var(--text); + border-bottom-color: var(--border); } -.investigation-option > svg:last-child { - align-self: center; - width: 20px; - height: 20px; +:root[data-theme="focus"] .mission-route::before { + background: var(--primary); +} + +:root[data-theme="focus"] .mission-kicker > span:first-child, +:root[data-theme="focus"] .route-caption span { color: var(--primary); } -.investigation-option strong, -.investigation-option small { - display: block; +:root[data-theme="focus"] .world-node { + background: var(--surface); + border-width: 1px; + border-color: var(--text); + box-shadow: none; } -.investigation-option strong { - margin-bottom: 6px; - font-size: 16px; - line-height: 1.5; +:root[data-theme="focus"] .world-node-index { + color: var(--surface); + background: var(--text); } -.investigation-option small { +:root[data-theme="focus"] .world-node-icon { + color: var(--accent); + background: transparent; + border-color: var(--border); +} + +:root[data-theme="focus"] .world-node-copy strong { + font-family: var(--heading); +} + +:root[data-theme="focus"] .world-node-copy em { + border-radius: 1px; +} + +:root[data-theme="focus"] .world-node--undiscovered { + background: transparent; + border-color: var(--mission-unknown); +} + +:root[data-theme="focus"] .world-node--undiscovered .world-node-index { color: var(--muted-text); - font-size: 12px; - line-height: 1.65; + background: transparent; } -.outline-action { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - width: 100%; - min-height: 52px; - margin-top: 24px; +:root[data-theme="focus"] .world-node.is-focused { + color: var(--text); background: var(--surface); - border: 1px solid var(--text); - border-radius: 4px; - font-weight: 650; + border: 3px solid var(--primary); + box-shadow: 7px 7px 0 color-mix(in srgb, var(--primary) 12%, transparent); } -.outline-action:hover { - color: var(--primary-hover); - border-color: var(--primary); +:root[data-theme="focus"] .world-node.is-focused .world-node-index { + color: var(--surface); + background: var(--primary); } -.outline-action svg { - width: 23px; - height: 23px; +:root[data-theme="focus"] .world-node.is-focused .world-node-state, +:root[data-theme="focus"] .world-node.is-focused .world-node-copy em { + color: var(--primary); } -.safety-note { - display: flex; - gap: 10px; - align-items: flex-start; - margin: 20px 4px 0; +:root[data-theme="focus"] .world-node.is-focused .world-node-icon { + color: var(--accent); + background: transparent; + border-color: var(--accent); +} + +:root[data-theme="focus"] .world-node.is-focused .world-node-copy small { color: var(--muted-text); - font-size: 11px; - line-height: 1.65; } -.safety-note svg { - flex: 0 0 auto; - width: 19px; - height: 19px; +:root[data-theme="focus"] .world-node-action { + color: var(--surface); + background: var(--text); + border-color: var(--text); + box-shadow: 4px 4px 0 color-mix(in srgb, var(--primary) 26%, transparent); +} + +:root[data-theme="focus"] .react-flow__edge-path { + stroke: var(--mission-route-edge); + stroke-width: 1.4; +} + +/* Narrow route keeps its topology ------------------------------------- */ + +@media (max-width: 900px) { + :root[data-theme] .mission-stage { + gap: 0; + padding: 12px 14px 14px; + overflow: hidden; + } + + .mission-header { + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: end; + padding: 0 0 14px; + } + + .mission-kicker { + gap: 6px; + margin-bottom: 3px; + font-size: var(--mission-type-micro); + } + + .mission-heading h1 { + max-width: 18ch; + font-size: var(--mission-type-display-tablet); + line-height: 1.2; + } + + .mission-objective { + grid-template-columns: 22px minmax(0, 1fr) auto; + gap: 7px; + min-height: 34px; + margin-top: 8px; + } + + .mission-objective > svg { + width: 18px; + height: 18px; + } + + .mission-objective small { + font-size: var(--mission-type-micro); + } + + .mission-objective strong { + display: -webkit-box; + overflow: hidden; + font-size: var(--mission-type-objective-mobile); + line-height: 1.4; + text-overflow: clip; + white-space: normal; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + } + + .mission-objective > [role="progressbar"] { + min-width: 48px; + padding: 0; + font-size: var(--mission-type-action); + } + + .mission-tools { + margin-bottom: -20px; + } + + .mission-tools .disclosure-pull { + min-width: 108px; + min-height: 44px; + padding: 0 10px; + box-shadow: 3px 3px 0 color-mix(in srgb, var(--text) 14%, transparent); + } + + .mission-tools .disclosure-pull-cord { + bottom: -17px; + height: 15px; + } + + .mission-tools .disclosure-pull-label { + font-size: var(--mission-type-action); + } + + .mission-route { + flex: 1 1 auto; + min-height: 0; + padding: 28px 0 0; + overflow: hidden; + } + + .route-caption { + top: 8px; + left: 4px; + } + + .mission-map { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + } + + .map-canvas { + display: none; + } + + .mobile-map { + position: relative; + align-self: flex-start; + display: grid; + gap: 0; + width: 100%; + min-height: max-content; + padding: 12px 8px 44px 54px; + margin: 0; + list-style: none; + counter-reset: route-step; + } + + .success-callout { + grid-template-columns: 22px minmax(0, 1fr); + gap: 8px; + min-height: 54px; + padding: 7px 2px 9px; + } + + .success-callout > svg { + width: 21px; + height: 21px; + } + + .success-callout strong { + font-size: var(--mission-type-objective-mobile); + } + + .success-callout span { + font-size: var(--mission-type-detail); + } + + .mobile-map::before { + position: absolute; + top: 34px; + bottom: 64px; + left: 28px; + width: 2px; + content: ""; + background: color-mix(in srgb, var(--text) 28%, transparent); + } + + .mobile-map > li { + position: relative; + min-width: 0; + min-height: 74px; + counter-increment: route-step; + } + + .mobile-map > li::before { + position: absolute; + top: 23px; + left: -40px; + z-index: 2; + display: grid; + width: 28px; + height: 28px; + color: var(--surface); + content: counter(route-step, decimal-leading-zero); + background: var(--text); + border: 2px solid var(--canvas); + border-radius: 50%; + font-family: var(--mono); + font-size: var(--mission-type-state); + font-weight: 760; + place-items: center; + } + + .mobile-map > li:has(.mobile-map-node--undiscovered)::before { + color: var(--muted-text); + background: var(--canvas); + border-color: var(--muted-text); + border-style: dashed; + } + + .mobile-map > li:has(.mobile-map-node.is-focused)::before { + color: var(--text); + background: var(--signal); + border-color: var(--text); + } + + :root[data-theme] .mobile-map-node { + display: block; + width: 100%; + min-height: 66px; + padding: 0; + color: var(--text); + background: transparent; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--text) 18%, transparent); + border-radius: 0; + box-shadow: none; + } + + .mobile-map-select { + display: grid; + grid-template-columns: 36px minmax(0, 1fr) 20px; + gap: 10px; + align-items: center; + width: 100%; + min-height: 66px; + padding: 8px 10px; + color: inherit; + text-align: left; + background: transparent; + border: 0; + } + + .mobile-map-select > svg:first-child { + width: 27px; + height: 27px; + } + + .mobile-map-select > svg:last-child { + width: 20px; + height: 20px; + color: var(--primary); + } + + .mobile-map-select strong, + .mobile-map-select small { + display: block; + } + + .mobile-map-select strong { + font-size: var(--mission-type-node-mobile); + font-weight: 760; + line-height: 1.3; + } + + .mobile-map-select small { + margin-top: 2px; + color: var(--muted-text); + font-size: var(--mission-type-detail); + line-height: 1.4; + } + + :root[data-theme] .mobile-map-node.is-focused { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + min-height: 78px; + margin: 4px 0 8px; + color: var(--surface); + background: var(--text); + border: 2px solid var(--signal); + box-shadow: 4px 4px 0 color-mix(in srgb, var(--text) 16%, transparent); + } + + :root[data-theme] .mobile-map-node.is-focused .mobile-map-select { + grid-template-columns: 30px minmax(0, 1fr); + min-height: 74px; + padding: 8px 5px 8px 10px; + } + + :root[data-theme] .mobile-map-node.is-focused .mobile-map-select > svg:last-child { + display: none; + } + + :root[data-theme] .mobile-map-node.is-focused .mobile-map-select small { + color: color-mix(in srgb, var(--surface) 68%, transparent); + } + + .mobile-map-action { + display: grid; + grid-template-columns: auto 18px; + gap: 6px; + align-items: center; + min-width: 104px; + min-height: 46px; + padding: 0 9px 0 12px; + margin: 8px; + color: var(--text); + background: var(--signal); + border: 0; + font-size: var(--mission-type-action); + font-weight: 800; + } + + .mobile-map-action svg { + width: 17px; + height: 17px; + } + + :root[data-theme] .mobile-map-node--undiscovered { + display: flex; + gap: 10px; + align-items: center; + min-height: 62px; + padding: 8px 10px; + color: var(--muted-text); + border-bottom-style: dashed; + } + + :root[data-theme] .mobile-map-node--undiscovered > svg { + display: none; + } + + :root[data-theme] .mobile-map-node--undiscovered span { + font-size: var(--mission-type-compact); + } + + :root[data-theme="focus"] .mobile-map-node.is-focused { + color: var(--text); + background: var(--surface); + border-color: var(--primary); + } + + :root[data-theme="ops"] .mobile-map-node.is-focused { + color: var(--text); + background: var(--canvas); + border-color: var(--signal); + } + + :root[data-theme="ops"] .mobile-map-node.is-focused .mobile-map-select small { + color: var(--muted-text); + } + + :root[data-theme="ops"] .mobile-map-action { + color: var(--canvas); + background: var(--signal); + } + + :root[data-theme="focus"] .mobile-map-node.is-focused .mobile-map-select small { + color: var(--muted-text); + } + + :root[data-theme="focus"] .mobile-map-action { + color: var(--surface); + background: var(--text); + } +} + +@media (max-width: 560px) { + .mission-header { + gap: 8px; + } + + .mission-heading h1 { + font-size: var(--mission-type-display-mobile); + } + + .mission-tools .disclosure-pull { + min-width: 102px; + padding-inline: 8px; + } + + .mobile-map { + padding-right: 4px; + padding-left: 50px; + } + + .mobile-map-node.is-focused { + grid-template-columns: minmax(0, 1fr) 98px; + } + + .mobile-map-action { + min-width: 0; + padding-inline: 8px; + margin: 6px; + } } -.map-screen .event-strip { - grid-column: 2 / -1; +@media (prefers-reduced-motion: reduce) { + .mission-tools .disclosure-pull, + .world-node, + .world-node-action, + .world-node-action svg { + transition: none; + } } diff --git a/apps/lab-guide/src/styles/responsive.css b/apps/lab-guide/src/styles/responsive.css index 6a273a3..b07a7f6 100644 --- a/apps/lab-guide/src/styles/responsive.css +++ b/apps/lab-guide/src/styles/responsive.css @@ -33,10 +33,6 @@ padding-inline: 18px; } - .map-stage { - padding-inline: 22px; - } - .session-pairing { grid-template-columns: 42px minmax(190px, 1fr) minmax(280px, 330px); gap: 11px; @@ -53,15 +49,6 @@ font-size: 14px; } - .map-legend { - gap: 12px; - font-size: 11px; - } - - .react-flow__node-world { - width: 180px; - } - .consultation-stage { padding-inline: 26px; } @@ -154,7 +141,6 @@ display: none; } - .map-screen, .consultation-screen { display: flex; flex-direction: column; @@ -162,27 +148,10 @@ min-height: calc(100dvh - var(--header-height)); } - .map-screen .known-facts, .consultation-screen .known-facts { order: 1; } - .map-screen .investigation-rail { - order: 2; - } - - .map-screen .map-stage { - order: 3; - } - - .map-screen--browse .map-stage { - order: 2; - } - - .map-screen--browse .investigation-rail { - order: 3; - } - .consultation-stage { order: 2; } @@ -229,15 +198,10 @@ display: none; } - .map-stage, .consultation-stage { overflow: visible; } - .map-stage { - padding: 28px 22px 30px; - } - .session-pairing { grid-template-columns: 46px minmax(0, 1fr); } @@ -246,68 +210,6 @@ grid-column: 1 / -1; } - .map-canvas { - display: none; - } - - .mobile-map { - display: grid; - gap: 10px; - padding: 0; - margin: 10px 0 0; - list-style: none; - } - - .mobile-map-node { - display: grid; - grid-template-columns: 34px minmax(0, 1fr) 24px; - gap: 12px; - align-items: center; - width: 100%; - min-height: 70px; - padding: 12px 14px; - text-align: left; - background: var(--surface); - border: 1px solid var(--success); - border-radius: 5px; - } - - .mobile-map-node--selected, - .mobile-map-node[aria-pressed="true"] { - border: 2px solid var(--primary); - } - - .mobile-map-node--undiscovered { - color: var(--muted-text); - border-color: var(--muted-text); - border-style: dashed; - } - - .mobile-map-node > svg { - width: 25px; - height: 25px; - } - - .mobile-map-node > svg:last-child { - color: var(--success); - } - - .mobile-map-node--selected > svg:last-child, - .mobile-map-node[aria-pressed="true"] > svg:last-child { - color: var(--primary); - } - - .mobile-map-node strong, - .mobile-map-node small { - display: block; - } - - .mobile-map-node small { - margin-top: 3px; - color: var(--muted-text); - font-size: 11px; - } - .investigation-rail, .hint-rail { padding: 28px 22px; @@ -388,7 +290,6 @@ min-height: 78px; } - .map-stage, .consultation-stage, .investigation-rail, .hint-rail { @@ -414,27 +315,11 @@ width: 100%; } - .stage-intro h1, .consultation-intro h1 { font-size: 27px; line-height: 1.45; } - .map-toolbar { - display: block; - margin-top: 18px; - } - - .map-toolbar h2 { - margin-bottom: 14px; - } - - .map-legend { - justify-content: space-between; - gap: 8px; - font-size: 10px; - } - .investigation-option { grid-template-columns: 38px minmax(0, 1fr) 20px; min-height: 100px; @@ -463,11 +348,20 @@ } .connection-banner { - grid-template-columns: 25px minmax(0, 1fr); + grid-template-columns: 25px minmax(0, 1fr) 44px; + gap: 8px; + min-height: 62px; + padding: 8px 10px; + margin-bottom: 10px; } .connection-banner button { - grid-column: 1 / -1; + grid-column: auto; + width: 44px; + min-width: 44px; + padding: 0; + justify-content: center; + font-size: 0; } .hypothesis-fieldset { diff --git a/apps/lab-guide/src/styles/shared.css b/apps/lab-guide/src/styles/shared.css index 164017e..3d6d90a 100644 --- a/apps/lab-guide/src/styles/shared.css +++ b/apps/lab-guide/src/styles/shared.css @@ -334,6 +334,7 @@ } .connection-banner { + flex: 0 0 auto; display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 12px; @@ -553,8 +554,7 @@ } .is-live-update .fact-list .fact-item:last-child, -.is-live-update .event-list li:last-child, -.is-live-update .selected-node-brief { +.is-live-update .event-list li:last-child { animation: state-arrive 220ms var(--ease-out) both; } diff --git a/apps/lab-guide/src/styles/themes.css b/apps/lab-guide/src/styles/themes.css index 21e8116..9da4a50 100644 --- a/apps/lab-guide/src/styles/themes.css +++ b/apps/lab-guide/src/styles/themes.css @@ -10,16 +10,13 @@ .app-root, .app-header, .known-facts, -.map-stage, .consultation-stage, .investigation-rail, .hint-rail, .event-strip, -.world-node, .investigation-option, .hypothesis-option, .hint-step, -.selected-node-brief, .primary-action, .secondary-action, .outline-action, @@ -120,7 +117,6 @@ margin-right: 0; } -.stage-intro::before, .consultation-intro::before { display: inline-block; margin-bottom: 9px; @@ -131,129 +127,10 @@ letter-spacing: 0.13em; } -.stage-intro::before { - content: "OPEN WORLD / ROUTE BOARD"; -} - .consultation-intro::before { content: "DECISION SUPPORT / NEXT MOVE"; } -.world-node { - position: relative; -} - -.world-node-index { - position: absolute; - top: 5px; - right: 7px; - color: var(--muted-text); - font-family: var(--mono); - font-size: 8px; - font-weight: 700; - letter-spacing: 0.08em; -} - -.world-node--undiscovered .world-node-index { - top: 50%; - right: auto; - left: 12px; - transform: translateY(-50%); -} - -.selected-node-brief { - flex: 0 0 auto; - display: grid; - grid-template-columns: 48px minmax(0, 1fr) auto auto; - gap: 14px; - align-items: center; - min-height: 80px; - padding: 12px 14px; - margin: 2px 0 16px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 6px; -} - -.selected-node-symbol { - display: grid; - width: 44px; - height: 44px; - color: var(--primary); - border: 1px solid var(--border); - border-radius: 5px; - place-items: center; -} - -.selected-node-symbol svg { - width: 25px; - height: 25px; -} - -.selected-node-copy { - display: grid; - min-width: 0; -} - -.selected-node-copy small { - color: var(--primary); - font-family: var(--mono); - font-size: 9px; - font-weight: 720; - letter-spacing: 0.08em; -} - -.selected-node-copy strong { - margin-top: 2px; - font-size: 15px; - line-height: 1.4; -} - -.selected-node-copy span { - overflow: hidden; - margin-top: 1px; - color: var(--muted-text); - font-size: 11px; - line-height: 1.45; - text-overflow: ellipsis; - white-space: nowrap; -} - -.selected-node-progress { - padding: 4px 7px; - color: var(--success); - border: 1px solid color-mix(in srgb, var(--success) 42%, var(--border)); - border-radius: 3px; - font-family: var(--mono); - font-size: 10px; - white-space: nowrap; -} - -.selected-node-brief > button { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 44px; - padding: 0 13px; - color: white; - background: var(--primary); - border: 1px solid var(--primary); - border-radius: 4px; - font-size: 12px; - font-weight: 700; - white-space: nowrap; -} - -.selected-node-brief > button:hover { - background: var(--primary-hover); - transform: translateX(2px); -} - -.selected-node-brief > button svg { - width: 17px; - height: 17px; -} - /* PLAY — Mission Deck ----------------------------------------------------- */ :root[data-theme="play"] { @@ -495,7 +372,6 @@ background: var(--accent); } -:root[data-theme="play"] .map-stage, :root[data-theme="play"] .consultation-stage { background-color: #f7f3e9; background-image: @@ -504,7 +380,6 @@ background-size: 17px 17px, 100% 42px; } -:root[data-theme="play"] .stage-intro::before, :root[data-theme="play"] .consultation-intro::before { padding: 4px 8px; color: #10100d; @@ -512,117 +387,12 @@ transform: rotate(-1deg); } -:root[data-theme="play"] .stage-intro h1, :root[data-theme="play"] .consultation-intro h1 { font-family: var(--heading); font-weight: 850; letter-spacing: -0.045em; } -:root[data-theme="play"] .map-toolbar { - padding: 0 12px; - background: #12120f; - border-left: 7px solid var(--signal); -} - -:root[data-theme="play"] .map-toolbar h2, -:root[data-theme="play"] .map-toolbar h2 svg, -:root[data-theme="play"] .map-legend { - color: #fffdf5; -} - -:root[data-theme="play"] .map-legend span:nth-child(1) svg { - color: var(--accent); -} - -:root[data-theme="play"] .map-legend span:nth-child(2) svg { - color: var(--signal); -} - -:root[data-theme="play"] .world-node { - min-height: 82px; - background: #fffdf5; - border: 2px solid var(--text); - border-radius: 3px; - box-shadow: 5px 5px 0 #d5d0c4; -} - -:root[data-theme="play"] .world-node--discovered { - border-color: var(--primary); -} - -:root[data-theme="play"] .world-node--selected, -:root[data-theme="play"] .world-node.is-focused { - background: #fff8bd; - border: 3px solid var(--text); - box-shadow: - 0 0 0 5px var(--signal), - 7px 7px 0 rgb(21 20 16 / 20%); - transform: translateY(-2px) rotate(-0.35deg); -} - -:root[data-theme="play"] .world-node--undiscovered { - background: rgb(255 253 245 / 60%); - border: 2px dashed #6d685d; - box-shadow: none; -} - -:root[data-theme="play"] .world-node-index { - padding: 1px 4px; - color: #11110e; - background: var(--signal); -} - -:root[data-theme="play"] .world-node--undiscovered .world-node-index { - color: var(--muted-text); - background: transparent; -} - -:root[data-theme="play"] .world-node-copy em { - color: #0e3d42; - background: #c8f7f4; - border-color: var(--primary); - font-weight: 750; -} - -:root[data-theme="play"] .react-flow__edge-path { - stroke: #171611; - stroke-width: 2.3; -} - -:root[data-theme="play"] .world-edge--possible .react-flow__edge-path { - stroke: #716c62; - stroke-dasharray: 8 6; -} - -:root[data-theme="play"] .selected-node-brief { - color: #fffdf5; - background: #11110e; - border: 3px solid #11110e; - border-radius: 2px; - box-shadow: 6px 6px 0 var(--signal); -} - -:root[data-theme="play"] .selected-node-symbol { - color: var(--signal); - border-color: #5d5a51; - border-radius: 2px; -} - -:root[data-theme="play"] .selected-node-copy small { - color: var(--accent); -} - -:root[data-theme="play"] .selected-node-copy span { - color: #c9c5ba; -} - -:root[data-theme="play"] .selected-node-progress { - color: var(--signal); - border-color: var(--signal); -} - -:root[data-theme="play"] .selected-node-brief > button, :root[data-theme="play"] .primary-action { color: #11110e; background: var(--signal); @@ -921,7 +691,6 @@ background: var(--primary); } -:root[data-theme="ops"] .map-stage, :root[data-theme="ops"] .consultation-stage { background-color: #071419; background-image: @@ -964,132 +733,19 @@ border-radius: 2px; } -:root[data-theme="ops"] .stage-intro::before, :root[data-theme="ops"] .consultation-intro::before { color: var(--primary); } -:root[data-theme="ops"] .stage-intro h1, :root[data-theme="ops"] .consultation-intro h1 { font-weight: 680; letter-spacing: -0.025em; } -:root[data-theme="ops"] .stage-intro p, :root[data-theme="ops"] .consultation-intro p { color: #8aa59f; } -:root[data-theme="ops"] .map-toolbar { - border-bottom: 1px solid #27464d; -} - -:root[data-theme="ops"] .map-toolbar h2 { - font-family: var(--mono); - font-size: 15px; - font-weight: 600; -} - -:root[data-theme="ops"] .map-toolbar h2 svg { - color: var(--primary); -} - -:root[data-theme="ops"] .world-node { - color: var(--text); - background: rgb(7 22 27 / 96%); - border: 1px solid var(--primary); - border-radius: 2px; - box-shadow: - inset 0 0 22px rgb(89 231 194 / 3.5%), - 0 0 14px rgb(89 231 194 / 08%); -} - -:root[data-theme="ops"] .world-node--selected, -:root[data-theme="ops"] .world-node.is-focused { - background: #151a18; - border: 2px solid var(--signal); - box-shadow: - inset 0 0 24px rgb(255 189 72 / 07%), - 0 0 0 3px rgb(255 189 72 / 10%), - 0 0 24px rgb(255 189 72 / 20%); -} - -:root[data-theme="ops"] .world-node--undiscovered { - color: #6c7f7b; - background: rgb(7 18 23 / 72%); - border-color: #60736f; -} - -:root[data-theme="ops"] .world-node-index { - color: var(--primary); - font-family: var(--mono); -} - -:root[data-theme="ops"] .world-node--selected .world-node-index, -:root[data-theme="ops"] .world-node.is-focused .world-node-index { - color: var(--signal); -} - -:root[data-theme="ops"] .world-node-copy strong { - font-weight: 570; -} - -:root[data-theme="ops"] .world-node-copy small, -:root[data-theme="ops"] .world-node-copy em { - font-family: var(--mono); -} - -:root[data-theme="ops"] .world-node-copy em { - color: var(--primary); - background: rgb(89 231 194 / 06%); - border-color: #2a665d; - border-radius: 1px; -} - -:root[data-theme="ops"] .react-flow__edge-path { - stroke: #71ddc4; - stroke-width: 1.5; - filter: drop-shadow(0 0 3px rgb(89 231 194 / 35%)); -} - -:root[data-theme="ops"] .world-edge--possible .react-flow__edge-path { - stroke: #879893; - stroke-dasharray: 7 7; - animation: ops-route 2.8s linear infinite; -} - -@keyframes ops-route { - to { - stroke-dashoffset: -28; - } -} - -:root[data-theme="ops"] .selected-node-brief { - background: rgb(7 20 25 / 96%); - border: 1px solid #2b535a; - border-left: 3px solid var(--signal); - border-radius: 1px; - box-shadow: 0 10px 28px rgb(0 0 0 / 20%); -} - -:root[data-theme="ops"] .selected-node-symbol { - color: var(--signal); - background: rgb(255 189 72 / 05%); - border-color: #6e5730; - border-radius: 1px; -} - -:root[data-theme="ops"] .selected-node-copy small, -:root[data-theme="ops"] .selected-node-progress { - color: var(--signal); - font-family: var(--mono); -} - -:root[data-theme="ops"] .selected-node-copy span { - color: #8ba59f; -} - -:root[data-theme="ops"] .selected-node-brief > button, :root[data-theme="ops"] .primary-action { color: #061015; background: var(--primary); @@ -1097,7 +753,6 @@ border-radius: 1px; } -:root[data-theme="ops"] .selected-node-brief > button:hover, :root[data-theme="ops"] .primary-action:hover:not(:disabled) { color: #061015; background: var(--primary-hover); @@ -1340,107 +995,20 @@ background: var(--primary); } -:root[data-theme="focus"] .map-stage, :root[data-theme="focus"] .consultation-stage { background: #fffefa; border-inline: 0; } -:root[data-theme="focus"] .stage-intro::before, :root[data-theme="focus"] .consultation-intro::before { color: var(--primary); } -:root[data-theme="focus"] .stage-intro h1, :root[data-theme="focus"] .consultation-intro h1 { font-weight: 690; letter-spacing: 0.01em; } -:root[data-theme="focus"] .stage-intro h1 { - max-width: 720px; - font-size: clamp(29px, 2.45vw, 40px); -} - -:root[data-theme="focus"] .map-toolbar { - border-top: 1px solid var(--text); - border-bottom: 1px solid var(--border); -} - -:root[data-theme="focus"] .map-toolbar h2 { - font-family: var(--body); - font-size: 14px; - font-weight: 700; -} - -:root[data-theme="focus"] .map-toolbar h2 svg { - color: var(--primary); -} - -:root[data-theme="focus"] .world-node { - background: #fffefa; - border: 1.5px solid var(--accent); - border-radius: 2px; - box-shadow: none; -} - -:root[data-theme="focus"] .world-node--selected, -:root[data-theme="focus"] .world-node.is-focused { - border: 2px solid var(--primary); - box-shadow: 4px 4px 0 rgb(214 43 31 / 10%); -} - -:root[data-theme="focus"] .world-node--undiscovered { - border-color: #77736b; -} - -:root[data-theme="focus"] .world-node-copy strong { - font-family: var(--heading); - font-size: 13px; -} - -:root[data-theme="focus"] .world-node-index { - color: var(--accent); -} - -:root[data-theme="focus"] .world-node--selected .world-node-index, -:root[data-theme="focus"] .world-node.is-focused .world-node-index { - color: var(--primary); -} - -:root[data-theme="focus"] .world-node-copy em { - color: var(--accent); - background: transparent; - border-color: color-mix(in srgb, var(--accent) 34%, var(--border)); - border-radius: 1px; -} - -:root[data-theme="focus"] .react-flow__edge-path { - stroke: #4b4944; - stroke-width: 1.4; -} - -:root[data-theme="focus"] .selected-node-brief { - padding-inline: 0; - background: transparent; - border-width: 1px 0; - border-radius: 0; -} - -:root[data-theme="focus"] .selected-node-symbol { - color: var(--primary); - border: 0; -} - -:root[data-theme="focus"] .selected-node-copy small { - color: var(--primary); -} - -:root[data-theme="focus"] .selected-node-copy strong { - font-family: var(--heading); -} - -:root[data-theme="focus"] .selected-node-brief > button, :root[data-theme="focus"] .primary-action { background: var(--primary); border-color: var(--primary); @@ -1558,19 +1126,6 @@ .end-session-button { padding-inline: 12px; } - - .selected-node-brief { - grid-template-columns: 42px minmax(0, 1fr) auto; - } - - .selected-node-symbol { - width: 40px; - height: 40px; - } - - .selected-node-progress { - display: none; - } } @media (max-width: 1100px) and (min-width: 901px) { @@ -1584,23 +1139,14 @@ } @media (min-width: 901px) and (max-height: 780px) { - .map-screen, .consultation-screen { grid-template-rows: minmax(0, 1fr) 80px; } - .map-screen { - grid-template-columns: 230px minmax(0, 1fr) 280px; - } - .consultation-screen { grid-template-columns: 250px minmax(0, 1fr) 300px; } - .map-stage { - padding-top: 18px; - } - .session-pairing { min-height: 68px; padding-block: 7px; @@ -1611,66 +1157,10 @@ display: none; } - .stage-intro::before, .consultation-intro::before { margin-bottom: 5px; } - .stage-intro h1 { - margin-bottom: 4px; - font-size: 24px; - } - - .stage-intro p { - font-size: 13px; - line-height: 1.5; - } - - .map-toolbar { - min-height: 46px; - margin-top: 10px; - } - - .map-canvas { - min-height: 248px; - } - - .world-node-copy strong { - font-size: 17px; - } - - .world-node-copy small { - font-size: 13px; - } - - .world-node-copy em { - font-size: 10px; - } - - .world-node-icon svg { - width: 32px; - height: 32px; - } - - .selected-node-brief { - min-height: 58px; - padding-block: 6px; - margin-bottom: 6px; - } - - .selected-node-symbol { - width: 34px; - height: 34px; - } - - .selected-node-copy span { - display: none; - } - - .selected-node-brief > button { - min-height: 40px; - } - .known-facts, .investigation-rail, .hint-rail { @@ -1825,10 +1315,6 @@ margin: 0; } - .selected-node-brief { - margin-top: 12px; - } - :root[data-theme="play"] .known-facts, :root[data-theme="play"] .investigation-rail, :root[data-theme="play"] .hint-rail { @@ -1859,26 +1345,4 @@ .theme-switcher button { min-height: 36px; } - - .selected-node-brief { - grid-template-columns: 42px minmax(0, 1fr) auto; - gap: 10px; - } - - .selected-node-copy span { - white-space: normal; - } - - .selected-node-progress { - display: none; - } - - .selected-node-brief > button { - grid-column: 1 / -1; - width: 100%; - } - - :root[data-theme="play"] .selected-node-brief { - margin-right: 5px; - } } diff --git a/design-qa.md b/design-qa.md index 0f8321b..76d493f 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,142 +1,136 @@ # Design QA — ExamServer Open World -Date: 2026-07-28 +Date: 2026-07-29 ## Design intent -The interface follows one progressive-disclosure rule: raise cognitive load only -after the learner asks for more. +The initial screen is one mission stage, not a stack of dashboard bands. +The learner reads it in one direction: -- Always visible: the current screen, current objective, world map or hypothesis - task, and the primary action. -- Available but closed: live pairing, confirmed facts, investigations or hints, - recent events, and theme choices. -- Every hidden area has a labelled pull with a useful count or current value. -- Pulling one label opens one surface. Opening another replaces it. -- Live events update the objective, map, and pull counts without opening a - surface on the learner's behalf. +`mission → objective → route → selected place → action` -Desktop uses a side drawer. Narrow screens use a bottom sheet. Drawer contents -are conditionally mounted, so closed information is absent from the tab order -and accessibility tree rather than merely covered. +Only the mission, objective, route, selected place, and primary action stay +visible. Pairing, facts, investigations, hints, events, and theme controls stay +behind one labelled tool pull or the single application menu until requested. -## Visual language - -The same disclosure hierarchy is authored in three distinct themes: +The selected place owns `次の一手`. On desktop the action uses a non-scaling +React Flow toolbar attached to the node. On mobile it is part of the selected +row in a single vertical route. -- PLAY — hard black borders, cyan and yellow signals, playful mission-map - geometry. -- OPS — midnight field, phosphor routes, amber selection, restrained mono - metadata. -- FOCUS — warm paper, thin rules, vermilion and cobalt state language, - editorial typography. - -Theme selection itself is folded under the labelled `見た目` control. Changing -theme does not reset the current map, consultation, or live state. +## Visual language -Evidence: +- PLAY — warm paper, hard black ink, cyan routes, and yellow selection. +- OPS — midnight field, restrained mint traces, and amber selection. +- FOCUS — editorial paper, thin rules, red selection, and blue utility marks. -- `docs/qa/live/disclosure-public-play-1366x768.png` -- `docs/qa/live/disclosure-public-ops-1366x768.png` -- `docs/qa/live/disclosure-public-focus-1366x768.png` -- `docs/qa/live/disclosure-consultation-focus-1366x768.png` +All three themes preserve the same order, state labels, primary action, keyboard +contract, and beginner-safe language. Mission-specific colour, type, geometry, +and alpha values are registered in `DESIGN.md` version 2.3 and consumed through +semantic CSS variables. -## Initial-load verification +## Desktop verification -Verified at `1366×768` in Chromium: +Verified at an exact `1280×720` raster: -- The world map owns the available width; the previous left, right, and bottom - information rails are not visible. -- The current objective and four labelled pulls are visible. -- Pairing input, facts, investigations, and event history are absent until - requested. -- Public browse mode makes no API request. -- No horizontal overflow and no console errors. +- Browse node union occupies `95.3%` of the route width. +- Live node union occupies `79.2%` of the route width. +- The selected-node action is approximately `46px` high and does not scale with + the graph. +- The route has no outer card or empty framing rectangle. +- No horizontal overflow occurs in PLAY, OPS, or FOCUS. Evidence: -- `docs/qa/live/disclosure-public-play-1366x768.png` +- `docs/qa/mission-stage/play-browse-1280x720.png` +- `docs/qa/mission-stage/ops-live-1280x720.png` +- `docs/qa/mission-stage/focus-browse-1280x720.png` -## On-request disclosure +## Mobile verification -Verified at `1366×768` in Chromium and `1280×720` in Firefox: +Verified at `375×844`: -- Pulling `確定した事実` opens one `480px` side drawer. -- The close control receives initial focus. -- `Tab` and `Shift+Tab` stay within the drawer. -- `Escape` and the backdrop close it. -- Closing restores focus to the pull that opened it. -- Body scrolling is locked only while the drawer is open. +- Browse and live show the mission, full objective, first route places, + selected place, and approximately `46px` primary action in the first view. +- Browse action bottom is approximately `449px`; live action bottom is + approximately `375px`. +- The graph becomes one route timeline rather than a card list. +- Only the route work area scrolls when the route is longer than the screen. +- No horizontal overflow occurs in PLAY, OPS, or FOCUS. Evidence: -- `docs/qa/live/disclosure-facts-drawer-play-1366x768.png` -- `docs/qa/live/disclosure-firefox-facts-1280x720.png` - -## Narrow-screen verification +- `docs/qa/mission-stage/play-browse-375x844.png` +- `docs/qa/mission-stage/ops-live-375x844.png` +- `docs/qa/mission-stage/focus-browse-375x844.png` -Verified at `360×800` in Chromium and Firefox: +## Consultation verification -- Document width equals viewport width; there is no horizontal overflow. -- All four pull labels remain visible as a `2×2` group before the map. -- The initial screen does not vertically stack every information panel. -- Pairing and hints open as bottom sheets, not full-page replacements. -- The pairing sheet contains only pairing controls. -- The hint sheet contains the three progressive hint levels. -- `Escape` closes the sheet and restores focus to its pull. +The consultation screen remains one decision surface with one selected +hypothesis, one explanation, and one primary action. Its supporting tools stay +closed until requested. All three themes were checked at both target sizes with +no horizontal overflow. Evidence: -- `docs/qa/live/disclosure-public-play-360x800.png` -- `docs/qa/live/disclosure-pairing-sheet-play-360x800.png` -- `docs/qa/live/disclosure-consultation-play-360x800.png` -- `docs/qa/live/disclosure-hints-sheet-play-360x800.png` -- `docs/qa/live/disclosure-firefox-pairing-360x800.png` +- `docs/qa/mission-stage/play-consultation-1280x720.png` +- `docs/qa/mission-stage/ops-consultation-375x844.png` +- `docs/qa/mission-stage/focus-consultation-1280x720.png` -## Live-state verification +## Edge-state verification -Verified with deterministic network fixtures: +Empty, loading, reconnecting, unavailable, and complete states were rendered at +both target sizes. -- Entering a valid pairing code closes the pairing drawer, removes its pull, - exposes the end-session control, and changes the connection marker to - `paired`. -- A later telemetry event changes the heading from `標的との接続を確かめる` - to `Webの入口を発見`. -- Fact and event counts move from `0` to `1`. -- The browser does not reload. -- No facts or event drawer opens automatically. -- No console errors or horizontal overflow. +- Empty shows one honest starting place and stops the route spine after it. +- Loading preserves the last useful map while announcing the update. +- Reconnecting and unavailable notices contain their refresh action without + overlapping the mission heading. +- Long objectives wrap to two lines instead of being truncated. +- Complete restores a compact, separated route summary and keeps all fourteen + places available for review. Evidence: -- `docs/qa/live/disclosure-pairing-to-live-1366x768.png` -- `docs/qa/live/disclosure-live-transition-after-1366x768.png` +- `docs/qa/mission-stage/play-empty-375x844.png` +- `docs/qa/mission-stage/play-loading-1280x720.png` +- `docs/qa/mission-stage/play-reconnecting-375x844.png` +- `docs/qa/mission-stage/play-unavailable-375x844.png` +- `docs/qa/mission-stage/play-success-1280x720.png` + +## Interaction and accessibility + +- Discovered desktop places are native buttons with `aria-pressed`. +- React Flow wrappers and edges are not keyboard stops. +- The selected action is not nested inside the place button. +- Mobile place selection remains a native button. +- Tool drawers keep the existing Escape, focus trap, and focus-return contract. +- Reduced motion removes route animation without removing state changes. +- Undiscovered labels, locked hint bodies, credentials, commands, and flags are + not exposed by the browser projection. ## Automated verification -- Repository contracts: 4/4 passed. -- World graph: 3 entrances, 3 footholds, 3 root paths, 14 flags, all 9 route - combinations. +- Repository contracts: 5/5 passed. +- World graph: 3 entrances, 3 footholds, 3 root paths, 14 flags, and all 9 + viable combinations. - Telemetry: 42 passed, 1 Linux-only skip. - Bridge: 19/19 passed. -- Platform: 52 passed, 2 Linux/WSL-only skips. +- Platform: 54 passed, 2 Linux/WSL-only skips. - Operator: 7/7 passed. -- Guide Vitest: 27/27 passed. +- Guide Vitest: 28/28 passed. - Guide Node suites: 19/19 passed. - TypeScript: passed. - Vite production build: passed. -- Chromium and Firefox console errors: none. +- `git diff --check`: passed. ## External verification boundary -The repository and browser implementation pass. The canonical production -surface is ; the former standalone -Sites surface is retired. Production is accepted only after the `/api/lab` -smoke passes: `create 200 → pair 200 → replay 404 → waiting state 200 → -snapshot 204 → live state 200 → SSE 200`. +Repository, automated, and local browser gates pass. Production is accepted only +after the exact saved version is deployed and both the deployed route and +canonical ExamServer `/lab` route are opened successfully. -The physical Kali-to-Debian connection, dual-boot installation, and all -physical recovery gates remain **NOT RUN** until tested on the designated -hardware. +The physical Kali-to-Debian connection, dual-boot installation, recovery image, +and hardware isolation gates remain **NOT RUN** until tested on the designated +machines. -final result: passed +final result: local pass; production pending diff --git a/docs/qa/mission-stage/focus-browse-1280x720.png b/docs/qa/mission-stage/focus-browse-1280x720.png new file mode 100644 index 0000000..599218f Binary files /dev/null and b/docs/qa/mission-stage/focus-browse-1280x720.png differ diff --git a/docs/qa/mission-stage/focus-browse-375x844.png b/docs/qa/mission-stage/focus-browse-375x844.png new file mode 100644 index 0000000..7bca4e7 Binary files /dev/null and b/docs/qa/mission-stage/focus-browse-375x844.png differ diff --git a/docs/qa/mission-stage/focus-consultation-1280x720.png b/docs/qa/mission-stage/focus-consultation-1280x720.png new file mode 100644 index 0000000..bc4be99 Binary files /dev/null and b/docs/qa/mission-stage/focus-consultation-1280x720.png differ diff --git a/docs/qa/mission-stage/focus-consultation-375x844.png b/docs/qa/mission-stage/focus-consultation-375x844.png new file mode 100644 index 0000000..60324ff Binary files /dev/null and b/docs/qa/mission-stage/focus-consultation-375x844.png differ diff --git a/docs/qa/mission-stage/focus-live-1280x720.png b/docs/qa/mission-stage/focus-live-1280x720.png new file mode 100644 index 0000000..e3401eb Binary files /dev/null and b/docs/qa/mission-stage/focus-live-1280x720.png differ diff --git a/docs/qa/mission-stage/focus-live-375x844.png b/docs/qa/mission-stage/focus-live-375x844.png new file mode 100644 index 0000000..a1870dd Binary files /dev/null and b/docs/qa/mission-stage/focus-live-375x844.png differ diff --git a/docs/qa/mission-stage/ops-browse-1280x720.png b/docs/qa/mission-stage/ops-browse-1280x720.png new file mode 100644 index 0000000..274a3b7 Binary files /dev/null and b/docs/qa/mission-stage/ops-browse-1280x720.png differ diff --git a/docs/qa/mission-stage/ops-browse-375x844.png b/docs/qa/mission-stage/ops-browse-375x844.png new file mode 100644 index 0000000..8f5b6a1 Binary files /dev/null and b/docs/qa/mission-stage/ops-browse-375x844.png differ diff --git a/docs/qa/mission-stage/ops-consultation-1280x720.png b/docs/qa/mission-stage/ops-consultation-1280x720.png new file mode 100644 index 0000000..8066fe2 Binary files /dev/null and b/docs/qa/mission-stage/ops-consultation-1280x720.png differ diff --git a/docs/qa/mission-stage/ops-consultation-375x844.png b/docs/qa/mission-stage/ops-consultation-375x844.png new file mode 100644 index 0000000..1b5586d Binary files /dev/null and b/docs/qa/mission-stage/ops-consultation-375x844.png differ diff --git a/docs/qa/mission-stage/ops-live-1280x720.png b/docs/qa/mission-stage/ops-live-1280x720.png new file mode 100644 index 0000000..db87eb4 Binary files /dev/null and b/docs/qa/mission-stage/ops-live-1280x720.png differ diff --git a/docs/qa/mission-stage/ops-live-375x844.png b/docs/qa/mission-stage/ops-live-375x844.png new file mode 100644 index 0000000..1b368dd Binary files /dev/null and b/docs/qa/mission-stage/ops-live-375x844.png differ diff --git a/docs/qa/mission-stage/play-browse-1280x720.png b/docs/qa/mission-stage/play-browse-1280x720.png new file mode 100644 index 0000000..92536cc Binary files /dev/null and b/docs/qa/mission-stage/play-browse-1280x720.png differ diff --git a/docs/qa/mission-stage/play-browse-375x844.png b/docs/qa/mission-stage/play-browse-375x844.png new file mode 100644 index 0000000..95f5502 Binary files /dev/null and b/docs/qa/mission-stage/play-browse-375x844.png differ diff --git a/docs/qa/mission-stage/play-consultation-1280x720.png b/docs/qa/mission-stage/play-consultation-1280x720.png new file mode 100644 index 0000000..4cb8b8d Binary files /dev/null and b/docs/qa/mission-stage/play-consultation-1280x720.png differ diff --git a/docs/qa/mission-stage/play-consultation-375x844.png b/docs/qa/mission-stage/play-consultation-375x844.png new file mode 100644 index 0000000..0117721 Binary files /dev/null and b/docs/qa/mission-stage/play-consultation-375x844.png differ diff --git a/docs/qa/mission-stage/play-empty-1280x720.png b/docs/qa/mission-stage/play-empty-1280x720.png new file mode 100644 index 0000000..d9c059b Binary files /dev/null and b/docs/qa/mission-stage/play-empty-1280x720.png differ diff --git a/docs/qa/mission-stage/play-empty-375x844.png b/docs/qa/mission-stage/play-empty-375x844.png new file mode 100644 index 0000000..c248440 Binary files /dev/null and b/docs/qa/mission-stage/play-empty-375x844.png differ diff --git a/docs/qa/mission-stage/play-live-1280x720.png b/docs/qa/mission-stage/play-live-1280x720.png new file mode 100644 index 0000000..1f82cca Binary files /dev/null and b/docs/qa/mission-stage/play-live-1280x720.png differ diff --git a/docs/qa/mission-stage/play-live-375x844.png b/docs/qa/mission-stage/play-live-375x844.png new file mode 100644 index 0000000..c09188d Binary files /dev/null and b/docs/qa/mission-stage/play-live-375x844.png differ diff --git a/docs/qa/mission-stage/play-loading-1280x720.png b/docs/qa/mission-stage/play-loading-1280x720.png new file mode 100644 index 0000000..bce4fd2 Binary files /dev/null and b/docs/qa/mission-stage/play-loading-1280x720.png differ diff --git a/docs/qa/mission-stage/play-loading-375x844.png b/docs/qa/mission-stage/play-loading-375x844.png new file mode 100644 index 0000000..a714d3c Binary files /dev/null and b/docs/qa/mission-stage/play-loading-375x844.png differ diff --git a/docs/qa/mission-stage/play-reconnecting-1280x720.png b/docs/qa/mission-stage/play-reconnecting-1280x720.png new file mode 100644 index 0000000..c293c92 Binary files /dev/null and b/docs/qa/mission-stage/play-reconnecting-1280x720.png differ diff --git a/docs/qa/mission-stage/play-reconnecting-375x844.png b/docs/qa/mission-stage/play-reconnecting-375x844.png new file mode 100644 index 0000000..9d49702 Binary files /dev/null and b/docs/qa/mission-stage/play-reconnecting-375x844.png differ diff --git a/docs/qa/mission-stage/play-success-1280x720.png b/docs/qa/mission-stage/play-success-1280x720.png new file mode 100644 index 0000000..a689e96 Binary files /dev/null and b/docs/qa/mission-stage/play-success-1280x720.png differ diff --git a/docs/qa/mission-stage/play-success-375x844.png b/docs/qa/mission-stage/play-success-375x844.png new file mode 100644 index 0000000..1a24f46 Binary files /dev/null and b/docs/qa/mission-stage/play-success-375x844.png differ diff --git a/docs/qa/mission-stage/play-unavailable-1280x720.png b/docs/qa/mission-stage/play-unavailable-1280x720.png new file mode 100644 index 0000000..49f28e4 Binary files /dev/null and b/docs/qa/mission-stage/play-unavailable-1280x720.png differ diff --git a/docs/qa/mission-stage/play-unavailable-375x844.png b/docs/qa/mission-stage/play-unavailable-375x844.png new file mode 100644 index 0000000..74311f6 Binary files /dev/null and b/docs/qa/mission-stage/play-unavailable-375x844.png differ