Vibe galaxy map

This commit is contained in:
2026-09-29 18:21:21 +00:00
committed by JonathanMcCaffrey
parent eaffd00137
commit ab07b38c98
12 changed files with 1844 additions and 69 deletions
@@ -0,0 +1,182 @@
---
sessionId: session-260928-131223-1ueu
---
# Requirements
### Overview & Goals
The goal of this initiative is to elevate the UI and user experience of `ZeroSpace.Web` and introduce an interactive **Galaxy Map** page. The Galaxy Map will visually display all planetary nodes positioned by their coordinates `(X, Y)`, render connection lines between interconnected systems, and provide instant hover information popups detailing yields, occupation buffs, masteries, and faction classifications.
### Scope
#### In Scope
- **Galaxy Page (`/galaxy`)**: Interactive star map rendering all 78 star systems from `StarSystems.All` with their actual `(X, Y)` spatial coordinates.
- **Connection Lines**: Drawing connection paths between systems based on `StarSystemData.ConnectedSystems`.
- **Hover Information Popup**: Dynamic floating tooltip/card showing system name, kind, occupation buffs, system masteries, yield bonuses, and connections.
- **Click Navigation**: Direct navigation from galaxy nodes to `/StarSystem/{Id}` details page.
- **UI & Aesthetic Overhaul**:
- Updating `NavMenu.razor` with a dedicated Galaxy Map entry and modern styling.
- Revamping `Home.razor` from default template placeholder to an engaging sci-fi overview dashboard.
- Enhancing theme colors, badge indicators, cards, and typography in `app.css`.
#### Out of Scope
- Backend database schema modifications (uses existing static/generated `ZeroSpace.Model` records).
- Real-time multiplayer synchronization or editing capabilities (map editing remains in `ZeroSpace.MapEditor`).
### User Stories
- **As a player / wiki reader**, I want to view a visual galaxy map with all planetary nodes and connections so that I can understand system layout and routes across the galaxy.
- **As a player**, I want to hover over any planetary node to immediately see its occupation buff, yields, and system mastery without having to navigate to separate pages.
- **As a user**, I want to click on a star system node on the map to navigate directly to its detailed page.
- **As a user**, I want a sleek, cohesive dark sci-fi web interface with clear navigation and a functional dashboard.
### Functional Requirements
1. **Galaxy Map Rendering**:
- The map shall render an SVG canvas encompassing coordinate bounds (0 to 1000) preserving aspect ratio.
- Lines must connect every system with its entries in `ConnectedSystems` without duplicate edge rendering.
- Planetary nodes shall be rendered as circular nodes styled and colored according to `StarSystemKind`:
- *Homeworld*: Vibrant Emerald / Green (`#43a047` / `#10b981`)
- *Core*: Vibrant Amber / Orange (`#fb8c00` / `#f59e0b`)
- *Side*: Vibrant Sky Blue / Cyan (`#4a90d9` / `#38bdf8`)
2. **Hover Information Popup**:
- Hovering over a planet node triggers a floating information popup.
- Popup content includes:
- Planet Name & Kind badge
- Occupation buff (e.g. `+50 Starting Hexite`)
- System mastery name (if present)
- Higher yield indicators (Influence, Credits, Energy, Items)
- Count and list of connected star systems
- Tooltip disappears smoothly when the cursor leaves the node.
3. **Map Interaction**:
- Hovering a node highlights the node (glow ring) and highlights incoming/outgoing connection lines.
- Clicking a node navigates to `/StarSystem/{Id}`.
4. **App Navigation & Home Page**:
- `NavMenu.razor` includes a link to `/galaxy` labeled "Galaxy Map" with an appropriate icon.
- `Home.razor` provides quick-launch cards to Galaxy Map, Star Systems, Masteries, and Units with summary counts.
### Non-Functional Requirements
- **Performance**: Instant SVG rendering with zero lag for 78 nodes and ~200 connections; pure CSS/Blazor event handling with no external heavy mapping dependencies.
- **Responsiveness**: Map scales smoothly inside its container across desktop and mobile screens.
- **Accessibility**: Clear contrast ratios, readable tooltips, and keyboard/touch support for node selection.
# Technical Design
### Current Implementation
- `ZeroSpace.Model.StarSystems.All`: Contains all 78 star systems with records containing `Id`, `Name`, `Kind`, `X`, `Y`, `ConnectedSystems`, `OccupationBuff`, `SystemMastery`, and yield flags.
- `ZeroSpace.Web`: Blazor WebAssembly application using Bootstrap 5 and Telerik Blazor components.
- Existing pages include `StarSystemIndex.razor`, `StarSystemDetail.razor`, `SystemMasteryIndex.razor`, and `Units.razor`.
- `Home.razor` is currently a default "Hello World" template placeholder.
### Key Decisions
1. **SVG Vector Graphics for Galaxy Map**:
- *Chosen Approach*: Native Blazor SVG rendering (`<svg viewBox="0 0 1000 1000">`).
- *Rationale*: SVG offers vector sharpness at all display resolutions, reactive Blazor event binding (`@onmouseenter`, `@onmouseleave`, `@onclick`), lightweight footprint, and zero external JS dependency overhead.
2. **Dynamic Overlay Tooltip**:
- *Chosen Approach*: HTML/CSS floating glassmorphism card positioned relative to the hovered node/cursor with smooth fade transition.
- *Rationale*: Allows rich formatting (badges, icons, multi-column yields) and styling that matches the dark sci-fi aesthetic better than native SVG `<title>` or basic browser tooltips.
3. **Connection Line Optimization**:
- *Chosen Approach*: Precompute unique undirected edge pairs `(From, To)` on initialization.
- *Rationale*: Prevents duplicate overlapping lines between reciprocal connections `A -> B` and `B -> A`, improving render performance and visual clarity.
### Proposed Architecture & Flow
```mermaid
graph TD
Nav[NavMenu.razor] -->|Navigate /galaxy| GalaxyPage[Galaxy.razor]
Home[Home.razor] -->|Quick Link| GalaxyPage
subgraph Galaxy.razor
Model[StarSystems.All] --> EdgeBuilder[Connection Deduplicator]
EdgeBuilder --> SVGCanvas[SVG Canvas ViewBox 0 0 1000 1000]
Model --> NodeRenderer[Node Elements & Coordinates]
SVGCanvas --> Lines[SVG Lines: Connection Paths]
SVGCanvas --> Nodes[SVG Circles & Glows: Planets]
Nodes -->|@onmouseenter| HoverState[Hovered System State]
Nodes -->|@onclick| NavAction[NavigationManager.NavigateTo /StarSystem/Id]
HoverState --> Popup[Floating Planet Info Card]
end
```
### Components & File Structure
- **New Files**:
- `ZeroSpace.Web/Pages/Galaxy.razor`: Main Galaxy map page containing the SVG viewport, control header, node filtering/legend, and popup overlay.
- `ZeroSpace.Web/Pages/Galaxy.razor.css`: Scoped styles for map container, SVG glow effects, connection styling, and animated hover card.
- **Modified Files**:
- `ZeroSpace.Web/Layout/NavMenu.razor`: Add "Galaxy Map" item with navigation icon.
- `ZeroSpace.Web/Pages/Home.razor`: Overhaul into a sci-fi hub with hero banner and feature cards.
- `ZeroSpace.Web/wwwroot/css/app.css`: Dark sci-fi styling improvements, badge colors, and glassmorphism utilities.
### Data Models / Contracts
```csharp
public sealed record ConnectionEdge(
StarSystemData From,
StarSystemData To);
public sealed class GalaxyMapState
{
public IReadOnlyList<StarSystemData> Systems { get; set; } = StarSystems.All;
public IReadOnlyList<ConnectionEdge> Connections { get; set; } = [];
public StarSystemData? HoveredSystem { get; set; }
public double TooltipX { get; set; }
public double TooltipY { get; set; }
public string? SelectedKindFilter { get; set; }
}
```
### Risks & Mitigations
- **Tooltip overflow near screen edges**: Clamp calculated tooltip coordinates within container boundaries to prevent viewport clipping.
- **Coordinate overlap on small screens**: Ensure viewBox preserves 1:1 aspect ratio (`preserveAspectRatio="xMidYMid meet"`) and allow zooming/scaling via standard CSS/viewport responsiveness.
# Testing
### Validation Approach
Verification will be performed through build checks, component markup analysis, and interactive scenario validation in the Blazor runtime.
### Key Scenarios
1. **Galaxy Map Rendering**:
- Verify that all 78 star systems are rendered at their designated `(X, Y)` positions.
- Verify that connection lines accurately connect linked systems without orphaned or broken lines.
- Verify node color coding matches `StarSystemKind` (Homeworld, Core, Side).
2. **Hover Information Popup**:
- Hovering over a planet node displays the popup immediately.
- Verify all fields render accurately (Occupation buff, System mastery, Yields, Connected systems list).
- Moving the mouse away dismisses the popup cleanly.
3. **Navigation & Interactivity**:
- Clicking a node navigates to the corresponding `/StarSystem/{Id}` route.
- Navigating via the "Galaxy Map" link in `NavMenu.razor` loads the page properly.
4. **UI & Responsiveness**:
- Verify `Home.razor` renders updated dashboard cards and quick links.
- Verify dark-theme sci-fi visual style across desktop and smaller browser viewports.
### Regressions & Edge Cases
- Star systems without occupation buffs or masteries display clean fallback dashes (`—`) without layout breaks.
- Rapid mouse movements over adjacent nodes update the hover popup smoothly without flickering.
# Delivery Steps
### ✓ Step 1: Implement Galaxy Map SVG rendering and connection lines
A functional SVG-based Galaxy Map page (`Galaxy.razor`) renders all star system nodes and connection lines based on their world coordinates.
- Create `ZeroSpace.Web/Pages/Galaxy.razor` with route `@page "/galaxy"` and navigation title.
- Parse `StarSystems.All` to extract unique undirected connection pairs and planetary node coordinate models.
- Render connection lines (`<line>`) in SVG with dark-steel futuristic line styling (`#3a4d6b`, opacity, glow).
- Render planetary nodes (`<circle>` / `<g>`) positioned at `(system.X, system.Y)` using color coding based on `StarSystemKind` (Homeworld = green, Core = orange/amber, Side = blue/cyan).
- Add SVG viewBox scaling (e.g. `0 0 1000 1000`) and responsive container layout to ensure crisp vector rendering on all screen sizes.
- Implement click handlers on nodes to navigate directly to `/StarSystem/{Id}`.
### ✓ Step 2: Implement dynamic planetary hover popup and interactive highlights
Interactive hover tooltip overlays display rich star system details when the user hovers over any planet node on the galaxy map.
- Design a floating glassmorphism hover card component / markup positioned dynamically relative to the active node or cursor.
- Display star system metadata: system name, `Kind` badge, `OccupationBuff`, `SystemMastery` link, and `Yield` bonuses (Influence, Credits, Energy, Items).
- Add highlight visual effects when hovering over a node (pulsing glow radius, highlight connected lines).
- Handle edge cases such as tooltip boundary clamping near viewport edges and debounced mouse enter/leave events.
### ✓ Step 3: Refine global UI, navigation menu, and dashboard landing page
ZeroSpace.Web navigation, Home landing dashboard, and global UI styles are updated to match a polished sci-fi space aesthetic.
- Update `ZeroSpace.Web/Layout/NavMenu.razor` with a dedicated "Galaxy Map" navigation link with custom star/galaxy icon.
- Transform `ZeroSpace.Web/Pages/Home.razor` into an overview hub showcasing quick links and summary stats for Galaxy Map, Star Systems, System Masteries, and Units.
- Enhance `ZeroSpace.Web/wwwroot/css/app.css` and component stylesheets with refined dark-theme colors, glassmorphism cards, glowing badges, and smooth hover transitions.
- Verify solution builds cleanly and all navigation flows between the Galaxy map, system details, and other pages work seamlessly.
@@ -2,21 +2,32 @@
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh;
background-color: #080c14;
background-image:
radial-gradient(circle at 15% 20%, rgba(56, 189, 248, 0.04) 0%, transparent 40%),
radial-gradient(circle at 85% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%);
} }
main { main {
flex: 1; flex: 1;
min-width: 0;
} }
.sidebar { .sidebar {
background-image: linear-gradient(180deg, #0d1117 0%, #161b22 70%); background-color: #0b0f19;
background-image: linear-gradient(180deg, #0d131f 0%, #080c14 100%);
border-right: 1px solid rgba(56, 189, 248, 0.12);
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.35);
z-index: 100;
} }
.top-row { .top-row {
background-color: #f7f7f7; background-color: rgba(13, 19, 31, 0.8);
border-bottom: 1px solid #d6d5d5; backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(56, 189, 248, 0.15);
justify-content: flex-end; justify-content: flex-end;
height: 3.5rem; height: 3.75rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }
@@ -25,10 +36,12 @@ main {
white-space: nowrap; white-space: nowrap;
margin-left: 1.5rem; margin-left: 1.5rem;
text-decoration: none; text-decoration: none;
color: #38bdf8;
} }
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline; text-decoration: underline;
color: #7dd3fc;
} }
.top-row ::deep a:first-child { .top-row ::deep a:first-child {
@@ -56,6 +69,7 @@ main {
height: 100vh; height: 100vh;
position: sticky; position: sticky;
top: 0; top: 0;
flex-shrink: 0;
} }
.top-row { .top-row {
@@ -70,8 +84,10 @@ main {
width: 0; width: 0;
} }
.top-row, article { article {
padding-left: 2rem !important; padding-left: 2rem !important;
padding-right: 1.5rem !important; padding-right: 2rem !important;
padding-top: 1.5rem !important;
padding-bottom: 2.5rem !important;
} }
} }
+20 -6
View File
@@ -1,6 +1,9 @@
<div class="top-row ps-3 navbar navbar-dark"> <div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="">ZeroSpace</a> <a class="navbar-brand d-flex align-items-center" href="">
<span class="brand-glow me-2">✦</span>
<span class="brand-text">ZeroSpace</span>
</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@@ -11,22 +14,33 @@
<nav class="nav flex-column"> <nav class="nav flex-column">
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span>
<span>Home</span>
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="units"> <NavLink class="nav-link" href="galaxy">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Units <span class="bi bi-galaxy-nav-menu" aria-hidden="true"></span>
<span>Galaxy Map</span>
<span class="badge badge-nav-live ms-auto">MAP</span>
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="StarSystem"> <NavLink class="nav-link" href="StarSystem">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Star Systems <span class="bi bi-star-systems-nav-menu" aria-hidden="true"></span>
<span>Star Systems</span>
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="SystemMastery"> <NavLink class="nav-link" href="SystemMastery">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Masteries <span class="bi bi-masteries-nav-menu" aria-hidden="true"></span>
<span>Masteries</span>
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="units">
<span class="bi bi-units-nav-menu" aria-hidden="true"></span>
<span>Units</span>
</NavLink> </NavLink>
</div> </div>
</nav> </nav>
@@ -1,14 +1,44 @@
.navbar-toggler { .navbar-toggler {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.25);
} }
.top-row { .top-row {
min-height: 3.5rem; min-height: 3.75rem;
background-color: rgba(0,0,0,0.4); background: linear-gradient(180deg, rgba(13, 19, 31, 0.95) 0%, rgba(9, 13, 22, 0.95) 100%);
border-bottom: 1px solid rgba(56, 189, 248, 0.15);
} }
.navbar-brand { .navbar-brand {
font-size: 1.1rem; font-size: 1.15rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #f0f6fc;
}
.brand-glow {
color: #38bdf8;
text-shadow: 0 0 10px rgba(56, 189, 248, 0.8);
font-size: 1.25rem;
}
.brand-text {
background: linear-gradient(135deg, #ffffff 30%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.badge-nav-live {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.05em;
padding: 0.2rem 0.45rem;
border-radius: 4px;
background: rgba(56, 189, 248, 0.2);
color: #38bdf8;
border: 1px solid rgba(56, 189, 248, 0.4);
box-shadow: 0 0 6px rgba(56, 189, 248, 0.3);
} }
.bi { .bi {
@@ -18,24 +48,34 @@
height: 1.25rem; height: 1.25rem;
margin-right: 0.75rem; margin-right: 0.75rem;
top: -1px; top: -1px;
background-size: cover; background-size: contain;
background-repeat: no-repeat;
background-position: center;
} }
.bi-house-door-fill-nav-menu { .bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2394a3b8' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
} }
.bi-plus-square-fill-nav-menu { .bi-galaxy-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2338bdf8' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='2.5' fill='%2338bdf8'/%3E%3Ccircle cx='2.5' cy='4' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='13.5' cy='5' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='4' cy='13' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='12' cy='12' r='1.5' fill='%2338bdf8'/%3E%3Cpath stroke='%2338bdf8' stroke-width='1.2' stroke-linecap='round' d='M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4' opacity='.65'/%3E%3C/svg%3E");
} }
.bi-list-nested-nav-menu { .bi-star-systems-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23f59e0b' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3.5' fill='%23f59e0b'/%3E%3Cpath stroke='%23f59e0b' stroke-width='1.2' stroke-linecap='round' d='M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8' opacity='.75'/%3E%3C/svg%3E");
}
.bi-masteries-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23a78bfa' viewBox='0 0 16 16'%3E%3Cpath d='M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z' fill='%23a78bfa'/%3E%3C/svg%3E");
}
.bi-units-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2310b981' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' stroke='%2310b981' stroke-width='1.3' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2310b981'/%3E%3Cpath stroke='%2310b981' stroke-width='1.3' d='M8 0v4M8 12v4M0 8h4M12 8h4'/%3E%3C/svg%3E");
} }
.nav-item { .nav-item {
font-size: 0.9rem; font-size: 0.92rem;
padding-bottom: 0.5rem; padding-bottom: 0.4rem;
} }
.nav-item:first-of-type { .nav-item:first-of-type {
@@ -47,22 +87,30 @@
} }
.nav-item ::deep a { .nav-item ::deep a {
color: #d7d7d7; color: #94a3b8;
border-radius: 4px; border-radius: 8px;
height: 3rem; height: 2.75rem;
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 3rem; line-height: 2.75rem;
padding: 0 0.85rem;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
} }
.nav-item ::deep a.active { .nav-item ::deep a.active {
background-color: rgba(88, 166, 255, 0.2); background: linear-gradient(90deg, rgba(56, 189, 248, 0.15) 0%, rgba(56, 189, 248, 0.05) 100%);
color: #58a6ff; color: #38bdf8;
border-color: rgba(56, 189, 248, 0.35);
box-shadow: inset 0 0 12px rgba(56, 189, 248, 0.12), 0 0 8px rgba(56, 189, 248, 0.1);
font-weight: 600;
} }
.nav-item ::deep a:hover { .nav-item ::deep a:hover:not(.active) {
background-color: rgba(88, 166, 255, 0.1); background-color: rgba(56, 189, 248, 0.08);
color: #58a6ff; color: #e2e8f0;
border-color: rgba(56, 189, 248, 0.15);
} }
@media (min-width: 641px) { @media (min-width: 641px) {
@@ -77,7 +125,7 @@
.nav-scrollable { .nav-scrollable {
/* Allow sidebar to scroll for tall menus */ /* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem); height: calc(100vh - 3.75rem);
overflow-y: auto; overflow-y: auto;
} }
} }
+390
View File
@@ -0,0 +1,390 @@
@page "/galaxy"
@using System.Globalization
@inject NavigationManager Navigation
<PageTitle>Galaxy Map</PageTitle>
<div class="galaxy-page">
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
<div>
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and
hyperlane routes.</p>
</div>
<div class="d-flex align-items-center gap-3">
<div class="map-legend d-flex align-items-center gap-3 px-3 py-2 rounded">
<div class="legend-item d-flex align-items-center gap-1">
<span class="legend-dot dot-homeworld"></span>
<span class="legend-text">Homeworld</span>
</div>
<div class="legend-item d-flex align-items-center gap-1">
<span class="legend-dot dot-core"></span>
<span class="legend-text">Core</span>
</div>
<div class="legend-item d-flex align-items-center gap-1">
<span class="legend-dot dot-side"></span>
<span class="legend-text">Side</span>
</div>
</div>
<div class="map-stats text-secondary small px-3 py-2 rounded">
<span><strong>@_systems.Count</strong> Systems</span>
<span class="mx-1">•</span>
<span><strong>@_connections.Count</strong> Hyperlanes</span>
</div>
</div>
</div>
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Background Grid Pattern -->
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
</pattern>
<!-- Center Core Glow -->
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
</radialGradient>
<!-- Glow filters -->
<filter id="glow-homeworld" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-core" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-side" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Map Background & Grid -->
<rect width="1000" height="1000" class="map-bg"/>
<rect width="1000" height="1000" fill="url(#space-grid)"/>
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
<!-- Connection Lines (Hyperlanes) -->
<g class="hyperlanes">
@foreach (var edge in _connections)
{
var isHighlighted = _hoveredSystem is not null &&
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
var isDimmed = _hoveredSystem is not null && !isHighlighted;
var lineClass = isHighlighted ? "hyperlane-line hyperlane-highlighted" :
isDimmed ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
<line x1="@SvgNumber(edge.From.X)"
y1="@SvgNumber(edge.From.Y)"
x2="@SvgNumber(edge.To.X)"
y2="@SvgNumber(edge.To.Y)"
class="@lineClass"/>
}
</g>
<!-- Planetary Nodes -->
<g class="planetary-nodes">
@foreach (var system in _systems)
{
var kindClass = GetKindCssClass(system.Kind);
var radius = GetNodeRadius(system.Kind);
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
var isDimmed = _hoveredSystem is not null && !isHovered && !isNeighbor;
var stateClass = isHovered ? "is-hovered" :
isNeighbor ? "is-neighbor" :
isDimmed ? "is-dimmed" : "";
<g class="node-group @kindClass @stateClass"
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
@onclick="() => OnNodeClick(system.Id)"
@onmouseenter="() => OnNodeMouseEnter(system)"
@onmouseleave="() => OnNodeMouseLeave(system)"
@onfocus="() => OnNodeFocus(system)"
@onblur="OnNodeBlur"
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
tabindex="0"
role="button"
aria-label="@system.Name (@system.Kind)">
<!-- Outer aura/glow circle -->
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
<!-- Main Node Circle -->
<circle r="@SvgNumber(radius)" class="node-circle"/>
<!-- Center core dot for homeworlds and core systems -->
@if (system.Kind != StarSystemKind.Side)
{
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
}
<!-- System Name Label -->
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
</g>
}
</g>
</svg>
<!-- Floating Glassmorphism Hover Card -->
@if (_hoveredSystem is not null)
{
var kindClass = GetKindCssClass(_hoveredSystem.Kind);
var higherYields = GetHigherYields(_hoveredSystem);
<div class="planet-hover-card @kindClass"
style="@GetTooltipStyle(_hoveredSystem)"
role="tooltip"
aria-live="polite">
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<span class="system-kind-indicator"></span>
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
</div>
<span class="badge kind-badge">@_hoveredSystem.Kind</span>
</div>
<div class="card-details-grid">
<div class="detail-row">
<span class="detail-label">Occupation Buff</span>
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
@(_hoveredSystem.OccupationBuff ?? "—")
</span>
</div>
<div class="detail-row">
<span class="detail-label">System Mastery</span>
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
@(_hoveredSystem.SystemMastery ?? "—")
</span>
</div>
<div class="detail-row">
<span class="detail-label">Yield Bonuses</span>
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
@if (higherYields.Count == 0)
{
<span class="yield-badge yield-standard">Standard</span>
}
else
{
@foreach (var y in higherYields)
{
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
}
}
</div>
</div>
<div class="detail-row">
<div class="d-flex align-items-center justify-content-between">
<span class="detail-label">Connected Systems</span>
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
</div>
<div class="connected-systems-list text-secondary small mt-1">
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
</div>
</div>
</div>
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
<span class="hint-text small">Click system to view full details →</span>
</div>
</div>
}
</div>
</div>
@code {
public sealed record ConnectionEdge(StarSystemData From, StarSystemData To);
private IReadOnlyList<StarSystemData> _systems = [];
private IReadOnlyList<ConnectionEdge> _connections = [];
private StarSystemData? _hoveredSystem;
private readonly HashSet<string> _hoveredConnectedIds = new(StringComparer.OrdinalIgnoreCase);
protected override void OnInitialized()
{
_systems = StarSystems.All;
_connections = BuildUniqueConnections(_systems);
}
private static IReadOnlyList<ConnectionEdge> BuildUniqueConnections(IReadOnlyList<StarSystemData> allSystems)
{
var edges = new List<ConnectionEdge>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var system in allSystems)
{
foreach (var targetName in system.ConnectedSystems)
{
if (!StarSystems.ByName.TryGetValue(targetName, out var target))
{
continue;
}
var edgeKey = string.Compare(system.Id, target.Id, StringComparison.OrdinalIgnoreCase) < 0
? $"{system.Id}::{target.Id}"
: $"{target.Id}::{system.Id}";
if (seen.Add(edgeKey))
{
edges.Add(new ConnectionEdge(system, target));
}
}
}
return edges;
}
private void SetHoveredSystem(StarSystemData? system)
{
_hoveredSystem = system;
_hoveredConnectedIds.Clear();
if (system is not null)
{
foreach (var connName in system.ConnectedSystems)
{
if (StarSystems.ByName.TryGetValue(connName, out var target))
{
_hoveredConnectedIds.Add(target.Id);
}
}
}
}
private void OnNodeMouseEnter(StarSystemData system)
{
SetHoveredSystem(system);
}
private void OnNodeMouseLeave(StarSystemData system)
{
if (_hoveredSystem?.Id == system.Id)
{
SetHoveredSystem(null);
}
}
private void OnNodeFocus(StarSystemData system)
{
SetHoveredSystem(system);
}
private void OnNodeBlur()
{
SetHoveredSystem(null);
}
private void OnContainerMouseLeave()
{
SetHoveredSystem(null);
}
private void OnNodeKeyDown(KeyboardEventArgs e, string systemId)
{
if (e.Key is "Enter" or " ")
{
OnNodeClick(systemId);
}
}
private void OnNodeClick(string systemId)
{
Navigation.NavigateTo($"/StarSystem/{systemId}");
}
private static IReadOnlyList<string> GetHigherYields(StarSystemData system)
{
var yields = new List<string>(4);
if (system.HigherInfluenceYield) yields.Add("Influence");
if (system.HigherCreditYield) yields.Add("Credits");
if (system.HigherEnergyYield) yields.Add("Energy");
if (system.HigherChanceForItems) yields.Add("Items");
return yields;
}
private static string GetTooltipStyle(StarSystemData system)
{
var posX = system.X / 10.0;
var posY = system.Y / 10.0;
string transform;
if (posX > 65.0)
{
if (posY < 20.0)
{
transform = "translate(calc(-100% - 16px), 0%)";
}
else if (posY > 80.0)
{
transform = "translate(calc(-100% - 16px), -100%)";
}
else
{
transform = "translate(calc(-100% - 16px), -50%)";
}
}
else if (posX < 35.0)
{
if (posY < 20.0)
{
transform = "translate(16px, 0%)";
}
else if (posY > 80.0)
{
transform = "translate(16px, -100%)";
}
else
{
transform = "translate(16px, -50%)";
}
}
else
{
if (posY > 50.0)
{
transform = "translate(-50%, calc(-100% - 16px))";
}
else
{
transform = "translate(-50%, 16px)";
}
}
return $"left: {SvgNumber(posX)}%; top: {SvgNumber(posY)}%; transform: {transform};";
}
private static string GetKindCssClass(StarSystemKind kind) => kind switch
{
StarSystemKind.Homeworld => "kind-homeworld",
StarSystemKind.Core => "kind-core",
_ => "kind-side"
};
private static double GetNodeRadius(StarSystemKind kind) => kind switch
{
StarSystemKind.Homeworld => 7.0,
StarSystemKind.Core => 6.0,
_ => 4.5
};
private static string SvgNumber(double value) => value.ToString(CultureInfo.InvariantCulture);
}
@@ -0,0 +1,390 @@
.galaxy-page {
width: 100%;
margin-bottom: 2rem;
}
.map-legend,
.map-stats {
background: rgba(22, 27, 34, 0.85);
border: 1px solid rgba(56, 189, 248, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.dot-homeworld {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.7);
}
.dot-core {
background-color: #f59e0b;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.7);
}
.dot-side {
background-color: #38bdf8;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.7);
}
.legend-text {
font-size: 0.85rem;
color: #c9d1d9;
}
.galaxy-map-container {
position: relative;
width: 100%;
max-width: 1100px;
margin: 0 auto;
border-radius: 12px;
overflow: hidden;
background: #080b10;
border: 1px solid rgba(56, 189, 248, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 20px rgba(56, 189, 248, 0.05);
}
.galaxy-svg {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
user-select: none;
}
.map-bg {
fill: #080b10;
}
/* Hyperlanes (Connection Lines) */
.hyperlane-line {
stroke: #3a4d6b;
stroke-width: 1.5;
stroke-opacity: 0.65;
stroke-linecap: round;
transition: stroke 0.2s ease, stroke-opacity 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease;
}
.hyperlane-line.hyperlane-highlighted {
stroke: #38bdf8;
stroke-width: 2.75;
stroke-opacity: 1;
filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.85));
}
.hyperlane-line.hyperlane-dimmed {
stroke-opacity: 0.15;
}
/* Node Groups */
.node-group {
cursor: pointer;
outline: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.node-circle {
stroke-width: 1.5;
transition: all 0.2s ease;
}
.node-aura {
opacity: 0.35;
transform-origin: 0 0;
transition: opacity 0.2s ease, r 0.2s ease;
}
.node-core-dot {
fill: #ffffff;
opacity: 0.9;
pointer-events: none;
}
.node-label {
fill: #8b949e;
font-size: 8.5px;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
font-weight: 500;
pointer-events: none;
letter-spacing: 0.2px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
transition: fill 0.2s ease, font-weight 0.2s ease, font-size 0.2s ease;
}
/* Kinds */
.kind-homeworld .node-circle {
fill: #10b981;
stroke: #34d399;
}
.kind-homeworld .node-aura {
fill: #10b981;
filter: url(#glow-homeworld);
}
.kind-core .node-circle {
fill: #f59e0b;
stroke: #fbbf24;
}
.kind-core .node-aura {
fill: #f59e0b;
filter: url(#glow-core);
}
.kind-side .node-circle {
fill: #38bdf8;
stroke: #7dd3fc;
}
.kind-side .node-aura {
fill: #38bdf8;
filter: url(#glow-side);
}
/* Node State Styles */
.node-group.is-hovered .node-circle,
.node-group:focus-visible .node-circle {
stroke: #ffffff;
stroke-width: 2.75;
filter: brightness(1.35) drop-shadow(0 0 6px rgba(255, 255, 255, 0.7));
}
.node-group.is-hovered .node-aura,
.node-group:focus-visible .node-aura {
opacity: 0.95;
animation: pulseAura 1.4s infinite alternate ease-in-out;
}
.node-group.is-hovered .node-label,
.node-group:focus-visible .node-label {
fill: #ffffff;
font-weight: 700;
font-size: 9.5px;
text-shadow: 0 0 6px rgba(56, 189, 248, 0.9), 0 2px 4px rgba(0, 0, 0, 1);
}
.node-group.is-neighbor .node-aura {
opacity: 0.7;
}
.node-group.is-neighbor .node-label {
fill: #f0f6fc;
font-weight: 600;
}
.node-group.is-neighbor .node-circle {
stroke-width: 2;
filter: brightness(1.15);
}
.node-group.is-dimmed {
opacity: 0.35;
}
/* Pulse Aura Animation */
@keyframes pulseAura {
0% {
transform: scale(1);
opacity: 0.75;
}
100% {
transform: scale(1.35);
opacity: 1;
}
}
/* Floating Glassmorphism Hover Card */
.planet-hover-card {
position: absolute;
z-index: 100;
min-width: 260px;
max-width: 310px;
padding: 14px 16px;
background: rgba(13, 17, 23, 0.94);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-radius: 10px;
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.35);
pointer-events: none;
animation: popupFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.12s ease-out, left 0.12s ease-out, top 0.12s ease-out;
}
@keyframes popupFadeIn {
from {
opacity: 0;
scale: 0.96;
}
to {
opacity: 1;
scale: 1;
}
}
.system-kind-indicator {
width: 9px;
height: 9px;
border-radius: 50%;
display: inline-block;
}
.system-title {
font-size: 1.05rem;
letter-spacing: 0.3px;
}
.kind-badge {
font-size: 0.75rem;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
}
/* Kind Variant Themes for Hover Card */
.planet-hover-card.kind-homeworld {
border-color: rgba(16, 185, 129, 0.45);
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(16, 185, 129, 0.18);
}
.planet-hover-card.kind-homeworld .kind-badge {
background: rgba(16, 185, 129, 0.2);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.4);
}
.planet-hover-card.kind-homeworld .system-kind-indicator {
background-color: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
}
.planet-hover-card.kind-core {
border-color: rgba(245, 158, 11, 0.45);
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(245, 158, 11, 0.18);
}
.planet-hover-card.kind-core .kind-badge {
background: rgba(245, 158, 11, 0.2);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.4);
}
.planet-hover-card.kind-core .system-kind-indicator {
background-color: #f59e0b;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.8);
}
.planet-hover-card.kind-side {
border-color: rgba(56, 189, 248, 0.45);
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(56, 189, 248, 0.18);
}
.planet-hover-card.kind-side .kind-badge {
background: rgba(56, 189, 248, 0.2);
color: #7dd3fc;
border: 1px solid rgba(56, 189, 248, 0.4);
}
.planet-hover-card.kind-side .system-kind-indicator {
background-color: #38bdf8;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.8);
}
/* Card Details Grid */
.card-details-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
font-size: 0.82rem;
}
.detail-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #8b949e;
margin-bottom: 2px;
display: block;
}
.detail-value {
font-size: 0.85rem;
display: block;
line-height: 1.35;
}
.text-buff {
color: #34d399;
font-weight: 600;
}
.text-mastery {
color: #60a5fa;
font-weight: 600;
}
/* Yield Badges */
.yield-badges {
margin-top: 3px;
}
.yield-badge {
font-size: 0.72rem;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
display: inline-flex;
align-items: center;
}
.yield-influence {
background: rgba(168, 85, 247, 0.18);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.4);
}
.yield-credits {
background: rgba(234, 179, 8, 0.18);
color: #facc15;
border: 1px solid rgba(234, 179, 8, 0.4);
}
.yield-energy {
background: rgba(6, 182, 212, 0.18);
color: #22d3ee;
border: 1px solid rgba(6, 182, 212, 0.4);
}
.yield-items {
background: rgba(16, 185, 129, 0.18);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.4);
}
.yield-standard {
background: rgba(100, 116, 139, 0.15);
color: #94a3b8;
border: 1px solid rgba(100, 116, 139, 0.3);
}
.connected-systems-list {
line-height: 1.4;
font-size: 0.76rem;
color: #8b949e;
word-break: break-word;
}
.card-footer-hint {
margin-top: 4px;
}
.hint-text {
color: #38bdf8;
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.2px;
}
+302 -4
View File
@@ -1,7 +1,305 @@
@page "/" @page "/"
@using ZeroSpace.Model
<PageTitle>Home</PageTitle> <PageTitle>ZeroSpace Database & Command Hub</PageTitle>
<h1>Hello, world!</h1> <div class="home-container">
<!-- Hero Banner -->
<div class="zs-hero p-4 p-md-5 mb-4 position-relative overflow-hidden">
<div class="hero-glow-orb"></div>
<div class="position-relative" style="z-index: 1;">
<div class="d-flex align-items-center gap-2 mb-2">
<span class="badge badge-pill-scifi badge-side">
<span class="hero-status-pulse"></span> SYSTEM ONLINE
</span>
<span class="text-dim fs-7">ZEROSPACE INTEL ARCHIVE v1.0</span>
</div>
<h1 class="display-6 fw-bold mb-3 hero-title">ZeroSpace Database & Command Hub</h1>
<p class="lead text-muted mb-4 hero-subtitle">
Comprehensive tactical star map diagnostics, planetary sector dossiers, doctrine masteries, and unit combat intelligence.
</p>
<div class="d-flex flex-wrap gap-3">
<a href="/galaxy" class="btn btn-scifi-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="2.5"/>
<circle cx="2.5" cy="4" r="1.5"/>
<circle cx="13.5" cy="5" r="1.5"/>
<circle cx="4" cy="13" r="1.5"/>
<circle cx="12" cy="12" r="1.5"/>
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4" opacity=".65"/>
</svg>
<span>Launch Galaxy Map</span>
</a>
<a href="/StarSystem" class="btn btn-scifi-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="3.5"/>
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8" opacity=".75"/>
</svg>
<span>Browse Systems</span>
</a>
<a href="/units" class="btn btn-scifi-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
<circle cx="8" cy="8" r="2"/>
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
</svg>
<span>Unit Intel</span>
</a>
</div>
</div>
</div>
Welcome to your new app. <!-- Quick Stats Bar -->
<div class="row g-3 mb-4">
<div class="col-6 col-lg-3">
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
<div class="stat-icon-wrapper icon-cyan">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 8 8A8.01 8.01 0 0 0 8 0zm0 14.5a6.5 6.5 0 1 1 6.5-6.5 6.51 6.51 0 0 1-6.5 6.5z"/>
<circle cx="8" cy="8" r="3.5"/>
</svg>
</div>
<div>
<div class="fs-4 fw-bold text-white lh-1">@totalSystemsCount</div>
<div class="text-dim fs-7 text-uppercase fw-semibold">Star Systems</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
<div class="stat-icon-wrapper icon-emerald">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2 14L8 2l6 12M4 10h8"/>
</svg>
</div>
<div>
<div class="fs-4 fw-bold text-white lh-1">@totalHyperlanesCount+</div>
<div class="text-dim fs-7 text-uppercase fw-semibold">Hyperlanes</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
<div class="stat-icon-wrapper icon-purple">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z"/>
</svg>
</div>
<div>
<div class="fs-4 fw-bold text-white lh-1">@totalMasteriesCount</div>
<div class="text-dim fs-7 text-uppercase fw-semibold">Masteries</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
<div class="stat-icon-wrapper icon-amber">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
<circle cx="8" cy="8" r="2"/>
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
</svg>
</div>
<div>
<div class="fs-4 fw-bold text-white lh-1">@totalUnitsCount</div>
<div class="text-dim fs-7 text-uppercase fw-semibold">Combat Units</div>
</div>
</div>
</div>
</div>
<!-- Feature Hub Cards (2x2 Grid) -->
<div class="section-header d-flex align-items-center mb-3">
<h2 class="h4 mb-0 text-white">Database Modules</h2>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
<div class="row g-4 mb-4">
<!-- Card 1: Galaxy Map -->
<div class="col-md-6">
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="feature-icon-badge icon-cyan">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="2.5"/>
<circle cx="2.5" cy="4" r="1.5"/>
<circle cx="13.5" cy="5" r="1.5"/>
<circle cx="4" cy="13" r="1.5"/>
<circle cx="12" cy="12" r="1.5"/>
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4" opacity=".65"/>
</svg>
</div>
<span class="badge badge-pill-scifi badge-side">INTERACTIVE SVG</span>
</div>
<h3 class="h5 fw-bold text-white mb-2">Interactive Galaxy Map</h3>
<p class="text-muted fs-6 mb-4 flex-grow-1">
Visual SVG star chart rendering all 78 star systems and hyperlane connection networks. Features instant hover intel cards, planetary buff diagnostics, and direct dossier navigation.
</p>
<div class="feature-meta mb-3 p-2 rounded">
<span class="badge badge-homeworld me-1">@homeworldsCount Homeworlds</span>
<span class="badge badge-core me-1">@coreCount Core</span>
<span class="badge badge-side">@sideCount Side</span>
</div>
<a href="/galaxy" class="btn btn-scifi-primary w-100 justify-content-center">
Launch Galaxy Map
<span class="arrow-icon">→</span>
</a>
</div>
</div>
<!-- Card 2: Star Systems Directory -->
<div class="col-md-6">
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="feature-icon-badge icon-amber">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="3.5"/>
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8" opacity=".75"/>
</svg>
</div>
<span class="badge badge-pill-scifi badge-core">78 DOSSIERS</span>
</div>
<h3 class="h5 fw-bold text-white mb-2">Star Systems Directory</h3>
<p class="text-muted fs-6 mb-4 flex-grow-1">
Complete catalog of planetary systems. Review sector classifications, starting hexite buffs, system masteries, item drop chances, and resource yield bonuses.
</p>
<div class="feature-meta mb-3 p-2 rounded">
<span class="badge badge-pill-scifi badge-side me-1">@buffsCount Occupation Buffs</span>
<span class="badge badge-pill-scifi badge-mastery">@higherYieldCount Yield Modifiers</span>
</div>
<a href="/StarSystem" class="btn btn-scifi-outline w-100 justify-content-center">
Browse Star Systems
<span class="arrow-icon">→</span>
</a>
</div>
</div>
<!-- Card 3: System Masteries -->
<div class="col-md-6">
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="feature-icon-badge icon-purple">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z"/>
</svg>
</div>
<span class="badge badge-pill-scifi badge-mastery">@totalMasteriesCount DOCTRINES</span>
</div>
<h3 class="h5 fw-bold text-white mb-2">System Masteries</h3>
<p class="text-muted fs-6 mb-4 flex-grow-1">
Strategic progression doctrines offering tiered battle bonuses, economic multipliers, and global faction enhancements with system availability matrices.
</p>
<div class="feature-meta mb-3 p-2 rounded text-dim fs-7">
<span>Includes 3-Tier Multiplier Progression per Doctrine</span>
</div>
<a href="/SystemMastery" class="btn btn-scifi-outline w-100 justify-content-center">
Explore Masteries
<span class="arrow-icon">→</span>
</a>
</div>
</div>
<!-- Card 4: Combat Units -->
<div class="col-md-6">
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="feature-icon-badge icon-emerald">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
<circle cx="8" cy="8" r="2"/>
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
</svg>
</div>
<span class="badge badge-pill-scifi badge-homeworld">@totalUnitsCount COMBAT UNITS</span>
</div>
<h3 class="h5 fw-bold text-white mb-2">Unit Roster & Intel</h3>
<p class="text-muted fs-6 mb-4 flex-grow-1">
Tactical combat unit encyclopedia with detailed Hexite/Flux cost breakdowns, health, DPS-per-cost efficiency metrics, armor classes, and faction hotkeys.
</p>
<div class="feature-meta mb-3 p-2 rounded text-dim fs-7">
<span>Interactive Sortable & Filterable Data Grid</span>
</div>
<a href="/units" class="btn btn-scifi-outline w-100 justify-content-center">
View Unit Intel
<span class="arrow-icon">→</span>
</a>
</div>
</div>
</div>
<!-- Galaxy Classification Breakdown -->
<div class="zs-glass-card p-4 mb-4">
<h3 class="h5 fw-bold text-white mb-3 d-flex align-items-center gap-2">
<span class="text-cyan">◈</span>
<span>Galaxy System Classification</span>
</h3>
<div class="row g-3">
<div class="col-md-4">
<div class="classification-box p-3 rounded border-homeworld">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge badge-homeworld">Homeworlds</span>
<span class="fw-bold text-emerald">@homeworldsCount systems</span>
</div>
<small class="text-muted">Primary faction command centers with critical strategic value.</small>
</div>
</div>
<div class="col-md-4">
<div class="classification-box p-3 rounded border-core">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge badge-core">Core Systems</span>
<span class="fw-bold text-amber">@coreCount systems</span>
</div>
<small class="text-muted">High-density central systems connecting major hyperlane corridors.</small>
</div>
</div>
<div class="col-md-4">
<div class="classification-box p-3 rounded border-side">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge badge-side">Side Sectors</span>
<span class="fw-bold text-cyan">@sideCount systems</span>
</div>
<small class="text-muted">Outer rim border systems offering specialized tactical yields.</small>
</div>
</div>
</div>
</div>
</div>
@code {
private int totalSystemsCount;
private int homeworldsCount;
private int coreCount;
private int sideCount;
private int totalHyperlanesCount;
private int totalMasteriesCount;
private int totalUnitsCount;
private int buffsCount;
private int higherYieldCount;
protected override void OnInitialized()
{
totalSystemsCount = StarSystems.All.Count;
homeworldsCount = StarSystems.All.Count(s => s.Kind == StarSystemKind.Homeworld);
coreCount = StarSystems.All.Count(s => s.Kind == StarSystemKind.Core);
sideCount = StarSystems.All.Count(s => s.Kind == StarSystemKind.Side);
// Calculate unique hyperlanes
var edges = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var s in StarSystems.All)
{
foreach (var connected in s.ConnectedSystems)
{
var edgeKey = string.Compare(s.Id, connected, StringComparison.OrdinalIgnoreCase) < 0
? $"{s.Id}--{connected}"
: $"{connected}--{s.Id}";
edges.Add(edgeKey);
}
}
totalHyperlanesCount = edges.Count;
totalMasteriesCount = SystemMasteries.All.Count;
totalUnitsCount = ZeroSpace.Model.Units.All.Count;
buffsCount = StarSystems.All.Count(s => !string.IsNullOrWhiteSpace(s.OccupationBuff));
higherYieldCount = StarSystems.All.Count(s => s.HigherCreditYield || s.HigherInfluenceYield || s.HigherEnergyYield || s.HigherChanceForItems);
}
}
@@ -0,0 +1,182 @@
.home-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.hero-glow-orb {
position: absolute;
width: 350px;
height: 350px;
top: -100px;
right: -50px;
background: radial-gradient(circle, rgba(56, 189, 248, 0.18) 0%, rgba(16, 185, 129, 0.05) 50%, transparent 70%);
border-radius: 50%;
filter: blur(40px);
pointer-events: none;
}
.hero-title {
background: linear-gradient(135deg, #ffffff 40%, #7dd3fc 80%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.hero-subtitle {
max-width: 750px;
line-height: 1.6;
}
.hero-status-pulse {
display: inline-block;
width: 7px;
height: 7px;
background-color: #38bdf8;
border-radius: 50%;
box-shadow: 0 0 6px #38bdf8;
animation: statusPulse 2s infinite;
}
@keyframes statusPulse {
0% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 6px rgba(56, 189, 248, 0);
}
100% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0);
}
}
.stat-card {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon-wrapper {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-cyan {
background: rgba(56, 189, 248, 0.15);
color: #38bdf8;
border: 1px solid rgba(56, 189, 248, 0.3);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
}
.icon-emerald {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
box-shadow: 0 0 10px rgba(16, 185, 129, 0.2);
}
.icon-amber {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.3);
box-shadow: 0 0 10px rgba(245, 158, 11, 0.2);
}
.icon-purple {
background: rgba(167, 139, 250, 0.15);
color: #a78bfa;
border: 1px solid rgba(167, 139, 250, 0.3);
box-shadow: 0 0 10px rgba(167, 139, 250, 0.2);
}
.feature-card {
position: relative;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.7), 0 0 20px rgba(56, 189, 248, 0.15);
border-color: rgba(56, 189, 248, 0.4);
}
.feature-icon-badge {
width: 52px;
height: 52px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.feature-meta {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.arrow-icon {
font-size: 1.1rem;
line-height: 1;
transition: transform 0.2s ease;
}
.feature-card:hover .arrow-icon {
transform: translateX(4px);
}
.classification-box {
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
}
.classification-box:hover {
background: rgba(0, 0, 0, 0.5);
transform: translateY(-2px);
}
.border-homeworld {
border-left: 3px solid #10b981;
}
.border-core {
border-left: 3px solid #f59e0b;
}
.border-side {
border-left: 3px solid #38bdf8;
}
.text-cyan {
color: #38bdf8;
}
.text-emerald {
color: #10b981;
}
.text-amber {
color: #f59e0b;
}
.text-purple {
color: #a78bfa;
}
.text-dim {
color: #64748b;
}
.fs-7 {
font-size: 0.78rem;
}
@@ -12,8 +12,11 @@ else
{ {
<div class="section-header d-flex align-items-center mb-4"> <div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">@system.Name</h1> <h1 class="mb-0">@system.Name</h1>
<span class="badge bg-primary ms-3">@system.Kind</span> <span class="badge @KindBadgeClass(system.Kind) ms-3">@system.Kind</span>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div> <div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
<a href="/galaxy" class="btn btn-sm btn-scifi-outline ms-2 d-none d-md-inline-flex">
<span>View on Map</span>
</a>
</div> </div>
<dl class="row"> <dl class="row">
@@ -83,6 +86,14 @@ else
system = StarSystems.All.FirstOrDefault(s => s.Id.Equals(Id, StringComparison.OrdinalIgnoreCase)); system = StarSystems.All.FirstOrDefault(s => s.Id.Equals(Id, StringComparison.OrdinalIgnoreCase));
} }
private static string KindBadgeClass(StarSystemKind kind) => kind switch
{
StarSystemKind.Homeworld => "badge-homeworld",
StarSystemKind.Core => "badge-core",
StarSystemKind.Side => "badge-side",
_ => "badge-side"
};
private static string YieldText(StarSystemData s) private static string YieldText(StarSystemData s)
{ {
var yields = new List<string>(); var yields = new List<string>();
@@ -5,18 +5,45 @@
<div class="section-header d-flex align-items-center mb-4"> <div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">Star Systems</h1> <h1 class="mb-0">Star Systems</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div> <div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
<a href="/galaxy" class="btn btn-sm btn-scifi-primary ms-3">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="2.5"/>
<circle cx="2.5" cy="4" r="1.5"/>
<circle cx="13.5" cy="5" r="1.5"/>
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3" opacity=".65"/>
</svg>
<span>Galaxy Map</span>
</a>
</div> </div>
@foreach (var kind in Enum.GetValues<StarSystemKind>()) @foreach (var kind in Enum.GetValues<StarSystemKind>())
{ {
var systems = StarSystems.All.Where(s => s.Kind == kind).OrderBy(s => s.Name).ToList(); var systems = StarSystems.All.Where(s => s.Kind == kind).OrderBy(s => s.Name).ToList();
<h2>@kind <span class="text-muted fs-5">(@systems.Count)</span></h2> var badgeClass = kind switch {
<ul class="list-unstyled row mb-4"> StarSystemKind.Homeworld => "badge-homeworld",
@foreach (var system in systems) StarSystemKind.Core => "badge-core",
{ StarSystemKind.Side => "badge-side",
<li class="col-md-4 col-lg-3 mb-1"> _ => "badge-side"
<a href="/StarSystem/@system.Id">@system.Name</a> };
</li>
} <div class="mb-4">
</ul> <div class="d-flex align-items-center gap-2 mb-3">
<h2 class="h4 mb-0 text-white">@kind Systems</h2>
<span class="badge @badgeClass">@systems.Count</span>
</div>
<ul class="list-unstyled row g-2">
@foreach (var system in systems)
{
<li class="col-md-4 col-lg-3">
<a class="d-block p-2 rounded zs-glass-card text-decoration-none" href="/StarSystem/@system.Id">
<span class="fw-semibold text-white">@system.Name</span>
@if (!string.IsNullOrWhiteSpace(system.OccupationBuff))
{
<div class="small text-dim text-truncate" title="@system.OccupationBuff">@system.OccupationBuff</div>
}
</a>
</li>
}
</ul>
</div>
} }
@@ -4,15 +4,19 @@
<div class="section-header d-flex align-items-center mb-4"> <div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">System Masteries</h1> <h1 class="mb-0">System Masteries</h1>
<span class="badge badge-mastery ms-3">@SystemMasteries.All.Count Doctrines</span>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div> <div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div> </div>
<div class="list-group"> <div class="list-group gap-2">
@foreach (var mastery in SystemMasteries.All.OrderBy(m => m.Name)) @foreach (var mastery in SystemMasteries.All.OrderBy(m => m.Name))
{ {
<a class="list-group-item list-group-item-action" href="/SystemMastery/@mastery.Id"> <a class="list-group-item list-group-item-action rounded border p-3 zs-glass-card text-decoration-none" href="/SystemMastery/@mastery.Id">
<div class="fw-bold">@mastery.Name</div> <div class="d-flex justify-content-between align-items-center mb-1">
<small class="text-muted">@mastery.Levels[0] · available on @mastery.AvailableSystems.Count systems</small> <span class="fw-bold text-white fs-5">@mastery.Name</span>
<span class="badge badge-pill-scifi badge-side">@mastery.AvailableSystems.Count systems</span>
</div>
<div class="text-muted small">@mastery.Levels[0]</div>
</a> </a>
} }
</div> </div>
+230 -17
View File
@@ -1,7 +1,25 @@
:root {
--zs-bg-base: #080c14;
--zs-bg-surface: #0f172a;
--zs-bg-card: rgba(15, 23, 42, 0.75);
--zs-bg-card-hover: rgba(30, 41, 59, 0.85);
--zs-border-subtle: rgba(56, 189, 248, 0.15);
--zs-border-glow: rgba(56, 189, 248, 0.45);
--zs-cyan: #38bdf8;
--zs-emerald: #10b981;
--zs-amber: #f59e0b;
--zs-purple: #a78bfa;
--zs-rose: #f43f5e;
--zs-text-main: #f0f6fc;
--zs-text-muted: #94a3b8;
--zs-text-dim: #64748b;
}
html, body { html, body {
font-family: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #0d1117; background-color: var(--zs-bg-base);
color: #c9d1d9; color: var(--zs-text-main);
min-height: 100vh;
} }
h1:focus { h1:focus {
@@ -9,25 +27,210 @@ h1:focus {
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
color: #e6edf3; color: #ffffff;
font-weight: 700;
letter-spacing: -0.01em;
} }
a, .btn-link { a, .btn-link {
color: #58a6ff; color: var(--zs-cyan);
text-decoration: none;
transition: color 0.15s ease, text-shadow 0.15s ease;
}
a:hover, .btn-link:hover {
color: #7dd3fc;
text-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
} }
p { p {
color: #8b949e; color: var(--zs-text-muted);
line-height: 1.6;
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #080c14;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 4px;
border: 1px solid rgba(56, 189, 248, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: #334155;
border-color: rgba(56, 189, 248, 0.4);
}
/* Sci-Fi Glass Panels & Cards */
.zs-glass-card {
background: var(--zs-bg-card);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--zs-border-subtle);
border-radius: 12px;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.zs-glass-card:hover {
border-color: var(--zs-border-glow);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.8), 0 0 20px rgba(56, 189, 248, 0.15);
}
.zs-hero {
background: radial-gradient(ellipse at top right, rgba(56, 189, 248, 0.12) 0%, transparent 60%),
radial-gradient(ellipse at bottom left, rgba(16, 185, 129, 0.08) 0%, transparent 60%),
rgba(15, 23, 42, 0.85);
backdrop-filter: blur(16px);
border: 1px solid rgba(56, 189, 248, 0.25);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* Sci-Fi Badges */
.badge-homeworld {
background: rgba(16, 185, 129, 0.18);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.45);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.25);
font-weight: 600;
}
.badge-core {
background: rgba(245, 158, 11, 0.18);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.45);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.25);
font-weight: 600;
}
.badge-side {
background: rgba(56, 189, 248, 0.18);
color: #7dd3fc;
border: 1px solid rgba(56, 189, 248, 0.45);
box-shadow: 0 0 8px rgba(56, 189, 248, 0.25);
font-weight: 600;
}
.badge-mastery {
background: rgba(167, 139, 250, 0.18);
color: #c4b5fd;
border: 1px solid rgba(167, 139, 250, 0.45);
box-shadow: 0 0 8px rgba(167, 139, 250, 0.25);
font-weight: 600;
}
.badge-pill-scifi {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.65rem;
border-radius: 9999px;
font-size: 0.75rem;
letter-spacing: 0.03em;
}
/* Sci-Fi Buttons */
.btn-scifi-primary {
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
color: #ffffff;
border: 1px solid #38bdf8;
box-shadow: 0 0 16px rgba(56, 189, 248, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.2);
font-weight: 600;
letter-spacing: 0.04em;
padding: 0.6rem 1.4rem;
border-radius: 8px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-scifi-primary:hover {
background: linear-gradient(135deg, #0369a1 0%, #075985 100%);
color: #ffffff;
box-shadow: 0 0 24px rgba(56, 189, 248, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.btn-scifi-outline {
background: rgba(15, 23, 42, 0.6);
color: var(--zs-cyan);
border: 1px solid rgba(56, 189, 248, 0.4);
box-shadow: 0 0 8px rgba(56, 189, 248, 0.15);
font-weight: 600;
letter-spacing: 0.04em;
padding: 0.6rem 1.4rem;
border-radius: 8px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-scifi-outline:hover {
background: rgba(56, 189, 248, 0.15);
color: #ffffff;
border-color: #38bdf8;
box-shadow: 0 0 16px rgba(56, 189, 248, 0.35);
transform: translateY(-1px);
}
/* List group dark theme styling */
.list-group-item {
background-color: rgba(15, 23, 42, 0.7);
border-color: rgba(56, 189, 248, 0.12);
color: var(--zs-text-main);
transition: all 0.2s ease;
}
.list-group-item-action:hover, .list-group-item-action:focus {
background-color: rgba(30, 41, 59, 0.8);
border-color: rgba(56, 189, 248, 0.3);
color: #38bdf8;
transform: translateX(4px);
}
/* Tables in dark theme */
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--zs-text-main);
--bs-table-border-color: rgba(56, 189, 248, 0.15);
}
.table th {
color: var(--zs-cyan);
font-weight: 600;
border-bottom: 2px solid rgba(56, 189, 248, 0.3);
}
/* Section Header Cyber Line */
.section-header h1 {
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.section-header .border-bottom {
border-color: rgba(56, 189, 248, 0.2) !important;
} }
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: #1b6ec2; background-color: #0284c7;
border-color: #1861ac; border-color: #0369a1;
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #0284c7;
} }
.content { .content {
@@ -43,14 +246,16 @@ p {
} }
.validation-message { .validation-message {
color: red; color: #f43f5e;
} }
#blazor-error-ui { #blazor-error-ui {
color-scheme: light only; color-scheme: dark only;
background: lightyellow; background: #1e1b4b;
color: #f87171;
border-top: 1px solid rgba(244, 63, 94, 0.4);
bottom: 0; bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.5);
box-sizing: border-box; box-sizing: border-box;
display: none; display: none;
left: 0; left: 0;
@@ -65,10 +270,11 @@ p {
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
color: #f87171;
} }
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #881337;
padding: 1rem 1rem 1rem 3.7rem; padding: 1rem 1rem 1rem 3.7rem;
color: white; color: white;
} }
@@ -88,16 +294,17 @@ p {
.loading-progress circle { .loading-progress circle {
fill: none; fill: none;
stroke: #e0e0e0; stroke: #1e293b;
stroke-width: 0.6rem; stroke-width: 0.6rem;
transform-origin: 50% 50%; transform-origin: 50% 50%;
transform: rotate(-90deg); transform: rotate(-90deg);
} }
.loading-progress circle:last-child { .loading-progress circle:last-child {
stroke: #1b6ec2; stroke: #38bdf8;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out; transition: stroke-dasharray 0.05s ease-in-out;
filter: drop-shadow(0 0 6px #38bdf8);
} }
.loading-progress-text { .loading-progress-text {
@@ -105,6 +312,8 @@ p {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem; inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
color: #38bdf8;
letter-spacing: 0.05em;
} }
.loading-progress-text:after { .loading-progress-text:after {
@@ -112,7 +321,11 @@ p {
} }
code { code {
color: #c02d76; color: #f43f5e;
background: rgba(244, 63, 94, 0.1);
padding: 0.15rem 0.35rem;
border-radius: 4px;
font-size: 0.875em;
} }
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { .form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {