diff --git a/apps/web/src/features/game/GamePage.tsx b/apps/web/src/features/game/GamePage.tsx index f5e7e49..b8e4514 100644 --- a/apps/web/src/features/game/GamePage.tsx +++ b/apps/web/src/features/game/GamePage.tsx @@ -8,6 +8,7 @@ import { CombatPanel } from "./components/CombatPanel"; import { CardsPanel } from "./components/CardsPanel"; import { BattleLogPanel } from "./components/BattleLogPanel"; import { OnboardingTour, shouldShowOnboarding } from "./components/OnboardingTour"; +import { formatPhaseLabel, formatTerritoryLabel } from "./utils/labels"; interface GamePageProps { credentials: SessionCredentials; @@ -56,6 +57,11 @@ export function GamePage({ credentials, onLeaveSession }: GamePageProps) { [credentials.playerId, privateState], ); + const myPublicPlayer = useMemo( + () => publicState?.players.find((player) => player.id === credentials.playerId), + [credentials.playerId, publicState], + ); + async function handleSubmitAction(action: Record) { await submitAction(credentials, action as RiskAction); } @@ -97,41 +103,63 @@ export function GamePage({ credentials, onLeaveSession }: GamePageProps) { } return ( -
-
-
-

Lobby {credentials.lobbyCode}

-

- {isMyTurn ? "Your move." : "Waiting for another commander..."} ·{" "} - Phase: {privateState.currentPhase} -

-
-
- - -
-
- -
+
+
-
-
- -
+
+ +
+ +

Legal Action Hints

{legalActions.length === 0 ? ( -

No legal actions right now.

+

No legal actions available for the current phase.

) : (
    {legalActions.map((hint) => ( diff --git a/apps/web/src/features/game/components/BoardMap.tsx b/apps/web/src/features/game/components/BoardMap.tsx index 8508698..634aa71 100644 --- a/apps/web/src/features/game/components/BoardMap.tsx +++ b/apps/web/src/features/game/components/BoardMap.tsx @@ -1,5 +1,6 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { GameStatePublic } from "@risk/shared-types"; +import { formatTerritoryLabel } from "../utils/labels"; import { TerritoryLayer } from "./TerritoryLayer"; interface BoardMapProps { @@ -30,26 +31,30 @@ export function BoardMap({ } const root = containerRef.current; - const svgElement = root.querySelector("svg"); + const svgElement = root.querySelector("svg"); if (!svgElement) { return; } const updateCenters = () => { - const svgRect = svgElement.getBoundingClientRect(); + const containerRect = root.getBoundingClientRect(); const nextCenters: Record = {}; for (const territoryId of Object.keys(state.territories)) { - const path = svgElement.querySelector( - `#${CSS.escape(territoryId)}`, - ); + const path = getTerritoryNode(svgElement, territoryId); if (!path) { continue; } const rect = path.getBoundingClientRect(); nextCenters[territoryId] = { - x: ((rect.left + rect.width / 2 - svgRect.left) / svgRect.width) * 100, - y: ((rect.top + rect.height / 2 - svgRect.top) / svgRect.height) * 100, + x: + ((rect.left + rect.width / 2 - containerRect.left) / + containerRect.width) * + 100, + y: + ((rect.top + rect.height / 2 - containerRect.top) / + containerRect.height) * + 100, }; } setCenters(nextCenters); @@ -72,85 +77,202 @@ export function BoardMap({ } const root = containerRef.current; - const svgElement = root.querySelector("svg"); + const svgElement = root.querySelector("svg"); if (!svgElement) { return; } + const territoryIds = Object.keys(state.territories); const handlers: Array<{ - node: SVGPathElement; - listener: EventListener; + node: SVGGraphicsElement; + clickListener: EventListener; + keydownListener: EventListener; }> = []; - for (const territoryId of Object.keys(state.territories)) { - const path = svgElement.querySelector( - `#${CSS.escape(territoryId)}`, - ); + for (const territoryId of territoryIds) { + const path = getTerritoryNode(svgElement, territoryId); if (!path) { continue; } - const listener = () => onSelectTerritory(territoryId); - path.addEventListener("click", listener); + const clickListener = () => onSelectTerritory(territoryId); + const keydownListener = (event: Event) => { + const keyboardEvent = event as KeyboardEvent; + if (keyboardEvent.key === "Enter" || keyboardEvent.key === " ") { + keyboardEvent.preventDefault(); + onSelectTerritory(territoryId); + } + }; + + path.classList.add("territory-region"); + path.setAttribute("tabindex", "0"); + path.setAttribute("role", "button"); + path.setAttribute( + "aria-label", + `Select territory ${formatTerritoryLabel(territoryId)}`, + ); + path.style.pointerEvents = "all"; + path.addEventListener("click", clickListener); + path.addEventListener("keydown", keydownListener); path.style.cursor = "pointer"; - handlers.push({ node: path, listener }); + handlers.push({ node: path, clickListener, keydownListener }); } return () => { for (const handler of handlers) { - handler.node.removeEventListener("click", handler.listener); + handler.node.removeEventListener("click", handler.clickListener); + handler.node.removeEventListener("keydown", handler.keydownListener); } }; - }, [svgMarkup, state.territories, onSelectTerritory]); + }); + + useEffect(() => { + if (!containerRef.current) { + return; + } + + const root = containerRef.current; + const svgElement = root.querySelector("svg"); + if (!svgElement) { + return; + } - const territoryStyles = useMemo(() => { const playerById = new Map( state.players.map((player) => [player.id, player]), ); - const styles = Object.entries(state.territories).map( - ([territoryId, territory]) => { - const color = territory.ownerId - ? playerById.get(territory.ownerId)?.color ?? "#666" - : "#5d5d5d"; - const stroke = - selectedTerritoryId === territoryId ? "#fff8d6" : "#1f1f1f"; - const strokeWidth = selectedTerritoryId === territoryId ? "3px" : "1.4px"; - - return ` - #${CSS.escape(territoryId)} { - fill: ${color} !important; - fill-opacity: ${territory.ownerId ? 0.7 : 0.28} !important; - stroke: ${stroke} !important; - stroke-width: ${strokeWidth} !important; - transition: fill 0.2s ease, stroke 0.2s ease; - } - `; - }, - ); + for (const [territoryId, territory] of Object.entries(state.territories)) { + const path = getTerritoryNode(svgElement, territoryId); + if (!path) { + continue; + } + + const color = territory.ownerId + ? playerById.get(territory.ownerId)?.color ?? "#666" + : "#5d5d5d"; + const stroke = + selectedTerritoryId === territoryId ? "#f6dd9d" : "rgba(13, 22, 29, 0.88)"; + const strokeWidth = selectedTerritoryId === territoryId ? "3.2px" : "1.5px"; + const fillOpacity = selectedTerritoryId === territoryId + ? 0.88 + : territory.ownerId + ? 0.66 + : 0.28; + + path.style.fill = color; + path.style.fillOpacity = String(fillOpacity); + path.style.stroke = stroke; + path.style.strokeWidth = strokeWidth; + path.style.transition = + "fill 0.2s ease, stroke 0.2s ease, fill-opacity 0.2s ease, filter 0.2s ease"; + path.style.filter = + selectedTerritoryId === territoryId + ? "drop-shadow(0 0 7px rgba(246, 221, 157, 0.36))" + : "none"; + } + }); - return styles.join("\n"); - }, [selectedTerritoryId, state.players, state.territories]); + const selectedTerritory = selectedTerritoryId + ? state.territories[selectedTerritoryId] + : null; + const selectedOwner = selectedTerritory?.ownerId + ? state.players.find((player) => player.id === selectedTerritory.ownerId) + : null; + const territoryCount = Object.keys(state.territories).length; + const claimedTerritoryCount = Object.values(state.territories).filter( + (territory) => territory.ownerId, + ).length; + const troopsOnBoard = Object.values(state.territories).reduce( + (sum, territory) => sum + territory.troops, + 0, + ); + const latestBattleEvent = state.battleLog.length + ? state.battleLog[state.battleLog.length - 1] + : null; return ( -
    -
    -

    World Map

    -
    +
    {svgMarkup ? ( <> -
    - + ) : (
    Loading board...
    )} + +
    +

    Strategic Theater

    +

    World Command Map

    +

    Drive every decision directly from the battlefield.

    +
    + +
    +
    +
    + {state.players.length} + Commanders +
    +
    + {claimedTerritoryCount}/{territoryCount} + Claimed +
    +
    + {troopsOnBoard} + Total Troops +
    +
    + Round {state.round} + Current Cycle +
    +
    +
    + +
    +

    + {selectedTerritoryId + ? formatTerritoryLabel(selectedTerritoryId) + : "Select a Territory"} +

    + {selectedTerritory ? ( +

    + {selectedOwner + ? `${selectedOwner.name} controls this position with ${selectedTerritory.troops} troops.` + : `This position is unclaimed with ${selectedTerritory.troops} troops.`} +

    + ) : ( +

    Click the map to inspect troop counts and launch commands.

    + )} + {latestBattleEvent ? ( +

    Latest Event: {latestBattleEvent.message}

    + ) : null} +
    ); } + +function getTerritoryNode(svgElement: SVGSVGElement, territoryId: string) { + const candidateIds = [territoryId, ...(TERRITORY_ID_ALIASES[territoryId] ?? [])]; + for (const candidateId of candidateIds) { + const node = svgElement.querySelector( + `#${CSS.escape(candidateId)}`, + ); + if (node) { + return node; + } + } + return null; +} + +const TERRITORY_ID_ALIASES: Record = { + yakutsk: ["yakursk"], +}; diff --git a/apps/web/src/features/game/components/TerritoryLayer.tsx b/apps/web/src/features/game/components/TerritoryLayer.tsx index 6777566..53240a7 100644 --- a/apps/web/src/features/game/components/TerritoryLayer.tsx +++ b/apps/web/src/features/game/components/TerritoryLayer.tsx @@ -1,11 +1,22 @@ +import type { CSSProperties } from "react"; import type { GameStatePublic } from "@risk/shared-types"; +import { formatTerritoryLabel } from "../utils/labels"; interface TerritoryLayerProps { state: GameStatePublic; centers: Record; + selectedTerritoryId: string | null; } -export function TerritoryLayer({ state, centers }: TerritoryLayerProps) { +export function TerritoryLayer({ + state, + centers, + selectedTerritoryId, +}: TerritoryLayerProps) { + const playerColorById = new Map( + state.players.map((player) => [player.id, player.color]), + ); + return (
    {Object.entries(state.territories).map(([territoryId, territory]) => { @@ -14,15 +25,23 @@ export function TerritoryLayer({ state, centers }: TerritoryLayerProps) { return null; } + const ownerColor = territory.ownerId + ? playerColorById.get(territory.ownerId) ?? "#7f8f9f" + : "#5d6975"; + const style = { + left: `${center.x}%`, + top: `${center.y}%`, + "--badge-accent": ownerColor, + } as CSSProperties; + return (
    {territory.troops}
    diff --git a/apps/web/src/features/game/components/TurnPanel.tsx b/apps/web/src/features/game/components/TurnPanel.tsx index a5b1500..d7c471c 100644 --- a/apps/web/src/features/game/components/TurnPanel.tsx +++ b/apps/web/src/features/game/components/TurnPanel.tsx @@ -1,4 +1,5 @@ import type { GameStatePrivate, GameStatePublic } from "@risk/shared-types"; +import { formatPhaseLabel } from "../utils/labels"; interface TurnPanelProps { publicState: GameStatePublic; @@ -6,21 +7,28 @@ interface TurnPanelProps { } export function TurnPanel({ publicState, privateState }: TurnPanelProps) { + const currentPlayer = publicState.players.find( + (player) => player.id === publicState.currentPlayerId, + ); + return (
    -

    Turn Status

    -

    - Round: {publicState.round} -

    -

    - Phase: {formatPhase(publicState.currentPhase)} -

    -

    - Current Player:{" "} - {publicState.players.find((player) => player.id === publicState.currentPlayerId)?.name} -

    +
    +

    Turn Status

    + {formatPhaseLabel(publicState.currentPhase)} +
    +
    +
    + Round + {publicState.round} +
    +
    + Reinforcements + {privateState.me.reinforcementPool} +
    +

    - Your Reinforcements: {privateState.me.reinforcementPool} + Current Player: {currentPlayer?.name ?? "Unknown"}

    {publicState.players.map((player) => ( @@ -39,10 +47,3 @@ export function TurnPanel({ publicState, privateState }: TurnPanelProps) {
    ); } - -function formatPhase(phase: string) { - return phase - .split("_") - .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) - .join(" "); -} diff --git a/apps/web/src/features/game/utils/labels.ts b/apps/web/src/features/game/utils/labels.ts new file mode 100644 index 0000000..200ba6a --- /dev/null +++ b/apps/web/src/features/game/utils/labels.ts @@ -0,0 +1,12 @@ +export function formatPhaseLabel(phase: string): string { + return phase + .split("_") + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(" "); +} + +export function formatTerritoryLabel(territoryId: string): string { + return territoryId + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, (character) => character.toUpperCase()); +} diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 8a5a5bb..eed674b 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -1,11 +1,14 @@ :root { - --bg: #171312; - --panel: rgba(23, 21, 18, 0.86); - --panel-border: rgba(255, 243, 208, 0.22); - --text: #f1ebdc; - --muted: #c9baa0; - --accent: #d2a854; - --error: #ff6f6f; + --bg: #071019; + --panel: rgba(10, 20, 31, 0.82); + --panel-strong: rgba(6, 14, 23, 0.92); + --panel-border: rgba(149, 194, 226, 0.25); + --text: #eaf4ff; + --muted: #9eb2c5; + --accent: #79cfff; + --accent-warm: #f6d08b; + --error: #ff788b; + --shadow-heavy: 0 18px 55px rgba(3, 8, 14, 0.65); } * { @@ -18,11 +21,19 @@ body { color: var(--text); min-height: 100vh; background: - radial-gradient(circle at 20% 10%, rgba(255, 221, 151, 0.15), transparent 35%), - url("/assets/textures/table-texture.svg"), + radial-gradient(circle at 20% -8%, rgba(104, 193, 255, 0.24), transparent 32%), + radial-gradient(circle at 80% 0%, rgba(255, 205, 132, 0.16), transparent 28%), + linear-gradient(180deg, #0a1522 0%, #050d16 60%, #050c14 100%), var(--bg); } +h1, +h2, +h3, +p { + margin: 0; +} + button, input, select { @@ -31,43 +42,61 @@ select { input, select { - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; + border: 1px solid rgba(154, 193, 221, 0.35); + border-radius: 10px; padding: 0.5rem 0.7rem; - background: rgba(0, 0, 0, 0.25); + background: rgba(7, 14, 22, 0.78); color: var(--text); } +input:focus-visible, +select:focus-visible, +button:focus-visible { + outline: 2px solid rgba(121, 207, 255, 0.6); + outline-offset: 1px; +} + button { - border: 1px solid rgba(255, 255, 255, 0.22); - border-radius: 8px; - background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(160, 195, 221, 0.4); + border-radius: 10px; + background: linear-gradient(180deg, rgba(52, 82, 109, 0.78), rgba(31, 48, 64, 0.78)); color: var(--text); padding: 0.5rem 0.8rem; cursor: pointer; + transition: transform 0.16s ease, border-color 0.16s ease, filter 0.16s ease; +} + +button:hover { + border-color: rgba(175, 213, 240, 0.65); + filter: brightness(1.08); +} + +button:active { + transform: translateY(1px); } button.primary { - background: linear-gradient(180deg, #d8b36a 0%, #a67829 100%); - border-color: rgba(0, 0, 0, 0.55); - color: #1d1307; + background: linear-gradient(180deg, #f2ca84 0%, #c3843f 100%); + border-color: rgba(26, 16, 6, 0.7); + color: #1f1204; font-weight: 700; } button:disabled { - opacity: 0.55; + opacity: 0.56; cursor: not-allowed; + transform: none; } .panel { background: - linear-gradient(165deg, rgba(255, 244, 214, 0.06), rgba(0, 0, 0, 0.1)), - url("/assets/textures/parchment-texture.svg"), + linear-gradient(170deg, rgba(150, 205, 241, 0.08), rgba(11, 22, 33, 0.4)), var(--panel); border: 1px solid var(--panel-border); border-radius: 14px; padding: 1rem; - backdrop-filter: blur(2px); + backdrop-filter: blur(8px); + box-shadow: inset 0 1px rgba(255, 255, 255, 0.04); } .stack { @@ -104,8 +133,14 @@ button:disabled { } .hero h1 { - margin: 0 0 0.5rem; - font-size: clamp(1.6rem, 2.8vw, 2.5rem); + margin-bottom: 0.4rem; + font-size: clamp(1.65rem, 2.8vw, 2.6rem); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.hero p { + color: var(--muted); } .lobby-grid { @@ -118,13 +153,14 @@ button:disabled { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.75rem; + margin-top: 0.75rem; } .lobby-card { - border: 1px solid rgba(255, 255, 255, 0.15); + border: 1px solid rgba(160, 205, 234, 0.2); border-radius: 10px; padding: 0.8rem; - background: rgba(0, 0, 0, 0.23); + background: rgba(7, 17, 28, 0.56); } .error-banner { @@ -137,72 +173,335 @@ button:disabled { margin: 1rem auto; } -.game-layout { - width: min(1600px, 98vw); - margin: 0.8rem auto 1.2rem; - display: grid; - gap: 0.8rem; +.war-room-layout { + width: min(1920px, 99.3vw); + margin: 0.35rem auto 0.7rem; } -.game-main-grid { - display: grid; - grid-template-columns: minmax(700px, 1fr) minmax(330px, 420px); - gap: 0.8rem; - align-items: start; +.war-room-topbar h1 { + font-size: clamp(1.35rem, 2.3vw, 2rem); + text-transform: uppercase; + letter-spacing: 0.08em; } -.side-column { - max-height: calc(100vh - 180px); +.war-room-topbar p { + color: var(--muted); +} + +.war-room-kicker { + text-transform: uppercase; + letter-spacing: 0.17em; + font-size: 0.72rem; + color: var(--accent); + font-weight: 600; +} + +.war-room-overlay-stage { + position: relative; + min-height: calc(100vh - 0.9rem); + border-radius: 20px; + overflow: hidden; + box-shadow: 0 18px 44px rgba(3, 8, 13, 0.55); +} + +.overlay-topbar { + position: absolute; + top: 0.8rem; + left: 0.8rem; + right: 0.8rem; + z-index: 12; + pointer-events: auto; + background: + linear-gradient(170deg, rgba(185, 226, 255, 0.18), rgba(7, 15, 23, 0.66)), + rgba(4, 10, 17, 0.78); + border-color: rgba(182, 221, 247, 0.36); +} + +.overlay-commander-panel { + position: absolute; + top: 7.15rem; + left: 1rem; + width: min(340px, 27vw); + z-index: 11; + pointer-events: auto; +} + +.overlay-command-stack { + position: absolute; + top: 7.15rem; + right: 1rem; + width: min(390px, 31vw); + max-height: calc(100vh - 18rem); overflow: auto; + padding-right: 0.1rem; + z-index: 11; + pointer-events: auto; } -.board-shell { - min-height: 72vh; +.overlay-battle-log { + position: absolute; + left: 1rem; + bottom: 1rem; + width: min(500px, 42vw); + z-index: 11; + pointer-events: auto; +} + +.overlay-legal-intel { + position: absolute; + right: 1rem; + bottom: 1rem; + width: min(390px, 31vw); + max-height: 30vh; + overflow: auto; + z-index: 11; + pointer-events: auto; } -.board-title-row h2 { - margin: 0 0 0.5rem; +.overlay-command-stack .panel, +.overlay-commander-panel, +.overlay-battle-log .panel, +.overlay-legal-intel { + background: + linear-gradient(170deg, rgba(192, 230, 255, 0.12), rgba(5, 14, 22, 0.7)), + var(--panel-strong); + border-color: rgba(177, 215, 241, 0.34); + box-shadow: 0 10px 28px rgba(2, 7, 12, 0.54); +} + +.board-shell { + min-height: 100%; } .board-container { position: relative; width: 100%; - border-radius: 12px; + min-height: calc(100vh - 0.9rem); + border-radius: 20px; + overflow: hidden; + background: + radial-gradient(circle at 34% 20%, rgba(72, 120, 156, 0.32), rgba(9, 17, 27, 0.96) 70%), + linear-gradient(180deg, rgba(8, 16, 26, 1), rgba(6, 13, 20, 1)); + border: 1px solid rgba(162, 202, 232, 0.26); + box-shadow: var(--shadow-heavy); +} + +.board-loading { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: #d7e8f6; + font-weight: 600; + z-index: 4; +} + +.board-container::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at center, transparent 40%, rgba(3, 7, 11, 0.58) 100%), + linear-gradient(180deg, rgba(3, 7, 12, 0.22), rgba(3, 7, 12, 0.5)); + pointer-events: none; + z-index: 3; +} + +.board-container::after { + content: ""; + position: absolute; + inset: 0; + background-image: url("/assets/textures/table-texture.svg"); + opacity: 0.08; + mix-blend-mode: soft-light; + pointer-events: none; + z-index: 2; +} + +.risk-board-svg { + position: absolute; + inset: 0; + z-index: 1; overflow: hidden; - background: radial-gradient(circle at top left, rgba(245, 232, 196, 0.18), transparent 35%); - border: 1px solid rgba(255, 245, 222, 0.15); } .risk-board-svg svg { - width: 100%; - height: auto; + position: absolute; + left: 50%; + top: 50.5%; + width: 126%; + height: 126%; + max-width: none; + max-height: none; display: block; - filter: saturate(1.02) contrast(1.08); + transform: translate(-50%, -50%); + filter: saturate(1.18) contrast(1.14) brightness(0.95); +} + +.territory-region { + transition: filter 0.2s ease, fill-opacity 0.2s ease; +} + +.territory-region:hover, +.territory-region:focus-visible { + filter: drop-shadow(0 0 9px rgba(184, 226, 255, 0.28)); + outline: none; } .territory-layer { position: absolute; inset: 0; pointer-events: none; + z-index: 5; } .troop-badge { position: absolute; transform: translate(-50%, -50%); - min-width: 1.55rem; + min-width: 1.75rem; text-align: center; border-radius: 999px; - background: rgba(7, 7, 7, 0.85); - border: 1px solid rgba(255, 255, 255, 0.4); - color: #fff; - font-weight: 700; + background: + radial-gradient(circle at 35% 28%, rgba(255, 255, 255, 0.25), transparent 58%), + rgba(8, 12, 18, 0.9); + border: 1px solid rgba(255, 255, 255, 0.58); + outline: 1px solid var(--badge-accent, rgba(150, 185, 211, 0.8)); + color: #f5fbff; + font-weight: 800; + letter-spacing: 0.02em; + font-size: 0.72rem; + padding: 0.2rem 0.4rem; + box-shadow: 0 0 14px rgba(5, 10, 16, 0.75); + transition: transform 0.18s ease, box-shadow 0.18s ease; +} + +.troop-badge.selected { + transform: translate(-50%, -50%) scale(1.14); + box-shadow: + 0 0 0 2px rgba(246, 208, 139, 0.62), + 0 0 20px rgba(246, 208, 139, 0.55); +} + +.map-hud { + position: absolute; + z-index: 6; + pointer-events: none; + max-width: min(420px, 90%); +} + +.map-hud.panel { + background: + linear-gradient(170deg, rgba(198, 231, 255, 0.13), rgba(7, 16, 25, 0.56)), + var(--panel-strong); + border-color: rgba(180, 216, 241, 0.34); + box-shadow: 0 12px 28px rgba(2, 6, 10, 0.52); +} + +.map-hud-top-left { + display: none; +} + +.map-hud-top-right { + top: 7.4rem; + right: calc(min(31vw, 390px) + 1.8rem); + width: min(260px, 21vw); +} + +.map-hud-bottom-left { + display: none; +} + +.hud-kicker { + text-transform: uppercase; + letter-spacing: 0.12em; + color: #b5dcf7; font-size: 0.72rem; - padding: 0.2rem 0.38rem; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); + font-weight: 600; +} + +.map-hud h2, +.map-hud h3 { + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 1rem; +} + +.map-hud p { + color: #b9cde0; + font-size: 0.88rem; +} + +.hud-stat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.45rem; +} + +.hud-stat-card { + border: 1px solid rgba(175, 214, 241, 0.2); + border-radius: 10px; + padding: 0.5rem; + background: rgba(6, 14, 22, 0.62); + display: grid; + gap: 0.15rem; +} + +.hud-stat-card strong { + color: #f4fbff; + font-size: 0.95rem; +} + +.hud-stat-card span { + color: #acc2d4; + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.latest-event { + margin-top: 0.2rem; + color: #d4e6f5; + font-size: 0.8rem; } .turn-panel .player-list { - margin-top: 0.3rem; + margin-top: 0.2rem; +} + +.phase-pill { + border: 1px solid rgba(175, 217, 247, 0.38); + border-radius: 999px; + padding: 0.2rem 0.55rem; + font-size: 0.72rem; + color: #d4ecff; + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.turn-highlights { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.45rem; +} + +.turn-highlight-card { + border: 1px solid rgba(167, 207, 233, 0.2); + border-radius: 10px; + padding: 0.5rem; + display: grid; + gap: 0.2rem; + background: rgba(6, 14, 22, 0.62); +} + +.turn-highlight-card span { + color: #9bb4c8; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.turn-highlight-card strong { + font-size: 1.05rem; } .player-chip { @@ -210,12 +509,16 @@ button:disabled { grid-template-columns: 14px 1fr auto; align-items: center; gap: 0.5rem; - background: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 8px; + background: rgba(6, 15, 24, 0.6); + border: 1px solid rgba(162, 206, 233, 0.2); + border-radius: 9px; padding: 0.45rem 0.55rem; } +.player-chip small { + color: #abc0d0; +} + .player-color { width: 12px; height: 12px; @@ -238,65 +541,160 @@ button:disabled { } .card-chip.selected { - border-color: #f7d38b; - box-shadow: 0 0 0 1px rgba(247, 211, 139, 0.8); -} - -.bottom-grid { - display: grid; - grid-template-columns: 1fr 380px; - gap: 0.8rem; + border-color: #f6d08b; + box-shadow: 0 0 0 1px rgba(246, 208, 139, 0.74); } .battle-log-panel { - min-height: 230px; + min-height: 240px; } .battle-log-list { display: grid; - gap: 0.4rem; - max-height: 220px; + gap: 0.45rem; + max-height: 240px; overflow: auto; + padding-right: 0.15rem; } .log-entry { - border: 1px solid rgba(255, 255, 255, 0.16); - border-radius: 8px; - padding: 0.45rem 0.55rem; - background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(166, 205, 231, 0.2); + border-radius: 10px; + padding: 0.5rem 0.6rem; + background: rgba(6, 13, 21, 0.66); +} + +.log-entry p { + margin-top: 0.35rem; + color: #b2c6d7; +} + +.legal-intel-panel { + min-height: 240px; +} + +.overlay-battle-log .battle-log-panel { + min-height: 24vh; + max-height: 30vh; + overflow: hidden; +} + +.overlay-battle-log .battle-log-list { + max-height: calc(30vh - 4rem); +} + +.overlay-legal-intel.legal-intel-panel { + min-height: 0; } .legal-list { margin: 0; padding-left: 1rem; display: grid; - gap: 0.35rem; + gap: 0.4rem; +} + +.legal-list li { + color: #c4d8e8; } .onboarding-backdrop { position: fixed; inset: 0; - background: rgba(4, 4, 4, 0.7); + background: rgba(3, 8, 13, 0.78); display: grid; place-items: center; z-index: 100; + pointer-events: none; } .onboarding-card { width: min(560px, 90vw); + pointer-events: auto; } .winner-banner { - color: #ffe4a9; + color: #ffdca8; font-weight: 700; } -@media (max-width: 1180px) { - .game-main-grid { - grid-template-columns: 1fr; +@media (max-width: 1480px) { + .overlay-commander-panel { + width: min(320px, 29vw); + } + + .overlay-command-stack, + .overlay-legal-intel { + width: min(360px, 32vw); + } + + .overlay-battle-log { + width: min(460px, 42vw); + } + + .map-hud-top-right { + right: calc(min(32vw, 360px) + 1.8rem); + width: min(230px, 20vw); + } +} + +@media (max-width: 1220px) { + .war-room-overlay-stage { + display: grid; + gap: 0.75rem; + min-height: unset; + overflow: visible; + box-shadow: none; + } + + .overlay-topbar, + .overlay-commander-panel, + .overlay-command-stack, + .overlay-battle-log, + .overlay-legal-intel { + position: static; + width: 100%; + max-height: unset; + } + + .overlay-command-stack, + .overlay-battle-log, + .overlay-legal-intel { + overflow: visible; + } + + .board-container { + min-height: 68vh; + border-radius: 18px; + } + + .risk-board-svg svg { + width: 116%; + height: 116%; + } + + .map-hud-top-right { + display: none; + } +} + +@media (max-width: 820px) { + .lobby-layout, + .war-room-layout { + width: min(96vw, 100%); + } + + .war-room-topbar { + gap: 0.7rem; + flex-direction: column; + align-items: flex-start; + } + + .map-hud { + max-width: calc(100% - 2rem); } - .bottom-grid { - grid-template-columns: 1fr; + .board-container { + min-height: 60vh; } }