Compare commits

..

1 Commits

Author SHA1 Message Date
mimo-v2-pro
8057e7d00c fix: [PORTALS] Add destination preview cards with description, purpose, and readiness (closes #715)
Some checks failed
CI / test (pull_request) Failing after 11s
CI / validate (pull_request) Failing after 14s
Review Approval Gate / verify-review (pull_request) Failing after 6s
2026-04-10 20:18:38 -04:00
4 changed files with 344 additions and 377 deletions

153
app.js
View File

@@ -2405,7 +2405,18 @@ function checkPortalProximity() {
activePortal = closest;
const hint = document.getElementById('portal-hint');
if (activePortal) {
document.getElementById('portal-hint-name').textContent = activePortal.config.name;
const cfg = activePortal.config;
document.getElementById('portal-hint-name').textContent = cfg.name;
document.getElementById('portal-hint-desc').textContent = cfg.description || '';
document.getElementById('portal-hint-purpose').textContent = cfg.purpose || cfg.description || '\u2014';
document.getElementById('portal-hint-access').textContent = (cfg.access_mode || 'open').toUpperCase();
document.getElementById('portal-hint-interaction').textContent = cfg.meaningful_interaction || '\u2014';
const readinessEl = document.getElementById('portal-hint-readiness');
const readiness = cfg.readiness || cfg.status || 'online';
readinessEl.textContent = readiness.toUpperCase();
readinessEl.className = 'portal-preview-readiness readiness-' + readiness;
hint.style.display = 'flex';
} else {
hint.style.display = 'none';
@@ -2422,10 +2433,20 @@ function activatePortal(portal) {
const timerDisplay = document.getElementById('portal-timer');
const statusDot = document.getElementById('portal-status-dot');
nameDisplay.textContent = portal.config.name.toUpperCase();
descDisplay.textContent = portal.config.description;
statusDot.style.background = portal.config.color;
statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
const cfg = portal.config;
nameDisplay.textContent = cfg.name.toUpperCase();
descDisplay.textContent = cfg.description;
statusDot.style.background = cfg.color;
statusDot.style.boxShadow = `0 0 10px ${cfg.color}`;
// Populate destination preview details
document.getElementById('portal-purpose-display').textContent = cfg.purpose || cfg.description || '\u2014';
const readinessEl = document.getElementById('portal-readiness-display');
const readiness = cfg.readiness || cfg.status || 'online';
readinessEl.textContent = readiness.toUpperCase();
readinessEl.className = 'portal-readiness readiness-' + readiness;
document.getElementById('portal-access-display').textContent = (cfg.access_mode || 'open').toUpperCase();
document.getElementById('portal-interaction-display').textContent = cfg.meaningful_interaction || '\u2014';
overlay.style.display = 'flex';
@@ -2516,48 +2537,38 @@ function populateAtlas() {
const grid = document.getElementById('atlas-grid');
grid.innerHTML = '';
// Counters by status
let onlineCount = 0;
let standbyCount = 0;
let rebuildingCount = 0;
let localCount = 0;
let blockedCount = 0;
// Group portals by environment
const byEnv = { production: [], staging: [], local: [] };
portals.forEach(portal => {
const config = portal.config;
const status = config.status || 'online';
const env = config.environment || 'production';
if (config.status === 'online') onlineCount++;
if (config.status === 'standby') standbyCount++;
// Count statuses
if (status === 'online' || status === 'active') onlineCount++;
else if (status === 'standby') standbyCount++;
else if (status === 'rebuilding') rebuildingCount++;
else if (status === 'local-only') localCount++;
else if (status === 'blocked') blockedCount++;
// Group by environment
if (byEnv[env]) {
byEnv[env].push({ config, status });
} else {
byEnv['production'].push({ config, status });
}
// Create atlas card
const card = document.createElement('div');
card.className = 'atlas-card';
card.style.setProperty('--portal-color', config.color);
const statusClass = `status-${status}`;
const statusClass = `status-${config.status || 'online'}`;
card.innerHTML = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${status.toUpperCase()}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
${config.purpose ? `<div class="atlas-card-row"><span class="atlas-card-label">PURPOSE</span> ${config.purpose}</div>` : ''}
<div class="atlas-card-meta">
<div class="atlas-card-meta-item">
<span class="atlas-card-label">READINESS</span>
<span class="atlas-card-readiness readiness-${config.readiness || config.status || 'online'}">${(config.readiness || config.status || 'online').toUpperCase()}</span>
</div>
<div class="atlas-card-meta-item">
<span class="atlas-card-label">ACCESS</span>
<span>${(config.access_mode || 'open').toUpperCase()}</span>
</div>
</div>
${config.meaningful_interaction ? `<div class="atlas-card-row"><span class="atlas-card-label">INTERACTION</span> ${config.meaningful_interaction}</div>` : ''}
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
@@ -2572,18 +2583,9 @@ function populateAtlas() {
grid.appendChild(card);
});
// Update footer counts
document.getElementById('atlas-online-count').textContent = onlineCount;
document.getElementById('atlas-standby-count').textContent = standbyCount;
document.getElementById('atlas-rebuilding-count').textContent = rebuildingCount;
document.getElementById('atlas-local-count').textContent = localCount;
document.getElementById('atlas-blocked-count').textContent = blockedCount;
// Populate status wall by environment
populateStatusWallEnv('production', byEnv.production);
populateStatusWallEnv('staging', byEnv.staging);
populateStatusWallEnv('local', byEnv.local);
// Update Bannerlord HUD status
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
if (bannerlord) {
@@ -2592,75 +2594,6 @@ function populateAtlas() {
}
}
function populateStatusWallEnv(envName, portalList) {
const container = document.getElementById(`${envName}-portals`);
const summary = document.getElementById(`${envName}-summary`);
container.innerHTML = '';
if (portalList.length === 0) {
container.innerHTML = '<div class="status-portal-row"><span class="status-portal-name" style="font-style: italic; color: rgba(160,184,208,0.4);">No worlds</span></div>';
summary.textContent = 'No portals in this environment';
return;
}
// Count statuses in this environment
const statusCounts = {};
portalList.forEach(({ config, status }) => {
statusCounts[status] = (statusCounts[status] || 0) + 1;
});
// Create portal rows
portalList.forEach(({ config, status }) => {
const row = document.createElement('div');
row.className = 'status-portal-row';
const indicator = document.createElement('span');
indicator.className = `status-portal-indicator status-dot ${status}`;
const nameSpan = document.createElement('span');
nameSpan.className = 'status-portal-name';
nameSpan.textContent = config.name;
const statusSpan = document.createElement('span');
statusSpan.style.fontSize = '9px';
statusSpan.style.textTransform = 'uppercase';
statusSpan.style.marginLeft = '8px';
statusSpan.style.color = getStatusColor(status);
statusSpan.textContent = status;
row.appendChild(nameSpan);
row.appendChild(statusSpan);
row.appendChild(indicator);
container.appendChild(row);
});
// Create summary
const summaryParts = Object.entries(statusCounts).map(([status, count]) =>
`${count} ${status}`
);
summary.textContent = summaryParts.join(' · ');
}
function getStatusColor(status) {
switch (status) {
case 'online':
case 'active':
return 'var(--color-primary)';
case 'standby':
return 'var(--color-gold)';
case 'rebuilding':
return '#ffa500';
case 'local-only':
return '#00ff88';
case 'blocked':
return '#ff0000';
case 'offline':
return 'var(--color-danger)';
default:
return 'var(--color-text-muted)';
}
}
function focusPortal(portal) {
// Teleport player to a position in front of the portal
const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));

View File

@@ -264,57 +264,11 @@
<div class="atlas-grid" id="atlas-grid">
<!-- Portals will be injected here -->
</div>
<!-- Portal Status Wall -->
<div class="atlas-status-wall">
<div class="status-wall-header">
<span class="status-wall-title">WORLD STATUS WALL</span>
<span class="status-wall-subtitle">Real-time portal health</span>
</div>
<div class="status-wall-grid">
<div class="status-wall-env" id="status-wall-production">
<div class="status-env-header">
<span class="status-env-dot production"></span>
<span class="status-env-label">PRODUCTION</span>
</div>
<div class="status-env-portals" id="production-portals"></div>
<div class="status-env-summary" id="production-summary"></div>
</div>
<div class="status-wall-env" id="status-wall-staging">
<div class="status-env-header">
<span class="status-env-dot staging"></span>
<span class="status-env-label">STAGING</span>
</div>
<div class="status-env-portals" id="staging-portals"></div>
<div class="status-env-summary" id="staging-summary"></div>
</div>
<div class="status-wall-env" id="status-wall-local">
<div class="status-env-header">
<span class="status-env-dot local"></span>
<span class="status-env-label">LOCAL</span>
</div>
<div class="status-env-portals" id="local-portals"></div>
<div class="status-env-summary" id="local-summary"></div>
</div>
</div>
<div class="status-wall-legend">
<div class="legend-item"><span class="status-dot online"></span> Online</div>
<div class="legend-item"><span class="status-dot rebuilding"></span> Rebuilding</div>
<div class="legend-item"><span class="status-dot local-only"></span> Local-only</div>
<div class="legend-item"><span class="status-dot blocked"></span> Blocked</div>
<div class="legend-item"><span class="status-dot offline"></span> Offline</div>
</div>
</div>
<div class="atlas-footer">
<div class="atlas-status-summary">
<span class="status-indicator online"></span> <span id="atlas-online-count">0</span> ONLINE
&nbsp;&nbsp;
<span class="status-indicator standby"></span> <span id="atlas-standby-count">0</span> STANDBY
&nbsp;&nbsp;
<span class="status-indicator rebuilding"></span> <span id="atlas-rebuilding-count">0</span> REBUILDING
&nbsp;&nbsp;
<span class="status-indicator local-only"></span> <span id="atlas-local-count">0</span> LOCAL
&nbsp;&nbsp;
<span class="status-indicator blocked"></span> <span id="atlas-blocked-count">0</span> BLOCKED
</div>
<div class="atlas-hint">Click a portal to focus or teleport</div>
</div>

View File

@@ -4,27 +4,44 @@
"name": "Morrowind",
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
"status": "online",
"environment": "production",
"color": "#ff6600",
"position": { "x": 15, "y": 0, "z": -10 },
"rotation": { "y": -0.5 },
"position": {
"x": 15,
"y": 0,
"z": -10
},
"rotation": {
"y": -0.5
},
"destination": {
"url": "https://morrowind.timmy.foundation",
"type": "harness",
"params": { "world": "vvardenfell" }
}
"params": {
"world": "vvardenfell"
}
},
"purpose": "Game world \u2014 exploration, combat, and role-playing in Vvardenfell",
"meaningful_interaction": "Autonomous questing, combat encounters, conversation with NPCs via agent harness",
"access_mode": "open",
"readiness": "online"
},
{
"id": "bannerlord",
"name": "Bannerlord",
"description": "Calradia battle harness. Massive armies, tactical command.",
"status": "online",
"environment": "production",
"status": "active",
"color": "#ffd700",
"position": { "x": -15, "y": 0, "z": -10 },
"rotation": { "y": 0.5 },
"position": {
"x": -15,
"y": 0,
"z": -10
},
"rotation": {
"y": 0.5
},
"portal_type": "game-world",
"world_category": "strategy-rpg",
"environment": "production",
"access_mode": "operator",
"readiness_state": "active",
"telemetry_source": "hermes-harness:bannerlord",
@@ -35,97 +52,142 @@
"url": "https://bannerlord.timmy.foundation",
"type": "harness",
"action_label": "Enter Calradia",
"params": { "world": "calradia" }
}
"params": {
"world": "calradia"
}
},
"purpose": "Strategy RPG \u2014 tactical army command and battlefield control",
"meaningful_interaction": "Agent-driven campaign, diplomacy, real-time battle command",
"readiness": "active"
},
{
"id": "workshop",
"name": "Workshop",
"description": "The creative harness. Build, script, and manifest.",
"status": "online",
"environment": "production",
"color": "#4af0c0",
"position": { "x": 0, "y": 0, "z": -20 },
"rotation": { "y": 0 },
"position": {
"x": 0,
"y": 0,
"z": -20
},
"rotation": {
"y": 0
},
"destination": {
"url": "https://workshop.timmy.foundation",
"type": "harness",
"params": { "mode": "creative" }
}
"params": {
"mode": "creative"
}
},
"purpose": "Creative sandbox \u2014 build tools, scripts, and artifacts",
"meaningful_interaction": "Code execution, file creation, prototype building with agent assistance",
"access_mode": "open",
"readiness": "online"
},
{
"id": "archive",
"name": "Archive",
"description": "The repository of all knowledge. History, logs, and ancient data.",
"status": "online",
"environment": "production",
"color": "#0066ff",
"position": { "x": 25, "y": 0, "z": 0 },
"rotation": { "y": -1.57 },
"position": {
"x": 25,
"y": 0,
"z": 0
},
"rotation": {
"y": -1.57
},
"destination": {
"url": "https://archive.timmy.foundation",
"type": "harness",
"params": { "mode": "read" }
}
"params": {
"mode": "read"
}
},
"purpose": "Knowledge repository \u2014 logs, history, and stored data",
"meaningful_interaction": "Search, retrieve, analyze historical records and documents",
"access_mode": "read-only",
"readiness": "online"
},
{
"id": "chapel",
"name": "Chapel",
"description": "A sanctuary for reflection and digital peace.",
"status": "online",
"environment": "production",
"color": "#ffd700",
"position": { "x": -25, "y": 0, "z": 0 },
"rotation": { "y": 1.57 },
"position": {
"x": -25,
"y": 0,
"z": 0
},
"rotation": {
"y": 1.57
},
"destination": {
"url": "https://chapel.timmy.foundation",
"type": "harness",
"params": { "mode": "meditation" }
}
"params": {
"mode": "meditation"
}
},
"purpose": "Sanctuary \u2014 digital peace and reflection space",
"meaningful_interaction": "Meditation interface, contemplative atmosphere, no active tasks",
"access_mode": "open",
"readiness": "online"
},
{
"id": "courtyard",
"name": "Courtyard",
"description": "The open nexus. A place for agents to gather and connect.",
"status": "online",
"environment": "production",
"color": "#4af0c0",
"position": { "x": 15, "y": 0, "z": 10 },
"rotation": { "y": -2.5 },
"position": {
"x": 15,
"y": 0,
"z": 10
},
"rotation": {
"y": -2.5
},
"destination": {
"url": "https://courtyard.timmy.foundation",
"type": "harness",
"params": { "mode": "social" }
}
"params": {
"mode": "social"
}
},
"purpose": "Social nexus \u2014 agent gathering and connection point",
"meaningful_interaction": "Agent presence, inter-agent communication, shared context",
"access_mode": "open",
"readiness": "online"
},
{
"id": "gate",
"name": "Gate",
"description": "The transition point. Entry and exit from the Nexus core.",
"status": "standby",
"environment": "staging",
"color": "#ff4466",
"position": { "x": -15, "y": 0, "z": 10 },
"rotation": { "y": 2.5 },
"position": {
"x": -15,
"y": 0,
"z": 10
},
"rotation": {
"y": 2.5
},
"destination": {
"url": "https://gate.timmy.foundation",
"type": "harness",
"params": { "mode": "transit" }
}
},
{
"id": "dev",
"name": "Dev Sandbox",
"description": "Local development world. Unstable, experimental, honest.",
"status": "local-only",
"environment": "local",
"color": "#00ff88",
"position": { "x": 0, "y": 0, "z": 20 },
"rotation": { "y": 0 },
"destination": {
"url": "http://localhost:3000",
"type": "local",
"params": { "mode": "dev" }
}
"params": {
"mode": "transit"
}
},
"purpose": "Transit point \u2014 entry and exit from Nexus core",
"meaningful_interaction": "System transit, routing, session management",
"access_mode": "open",
"readiness": "standby"
}
]
]

356
style.css
View File

@@ -365,11 +365,7 @@ canvas#nexus-canvas {
}
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-rebuilding { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
.status-local-only { background: rgba(0, 255, 136, 0.2); color: #00ff88; border: 1px solid #00ff88; }
.status-blocked { background: rgba(255, 0, 0, 0.2); color: #ff0000; border: 1px solid #ff0000; }
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
.atlas-card-desc {
@@ -387,6 +383,52 @@ canvas#nexus-canvas {
font-size: 10px;
color: rgba(160, 184, 208, 0.6);
}
.atlas-card-row {
font-size: 12px;
color: rgba(224, 240, 255, 0.65);
margin-bottom: 8px;
line-height: 1.4;
}
.atlas-card-label {
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
font-weight: 600;
color: var(--portal-color, var(--color-primary));
letter-spacing: 0.1em;
margin-right: 6px;
opacity: 0.8;
}
.atlas-card-meta {
display: flex;
gap: 20px;
margin-bottom: 10px;
}
.atlas-card-meta-item {
font-size: 11px;
color: rgba(224, 240, 255, 0.6);
}
.atlas-card-readiness {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
letter-spacing: 0.06em;
}
.atlas-card-readiness.readiness-online,
.atlas-card-readiness.readiness-active {
background: rgba(74, 240, 192, 0.12);
color: #4af0c0;
}
.atlas-card-readiness.readiness-standby {
background: rgba(255, 215, 0, 0.1);
color: #ffd700;
}
.atlas-card-readiness.readiness-offline {
background: rgba(255, 68, 102, 0.1);
color: #ff4466;
}
.atlas-footer {
padding: 15px 30px;
@@ -414,165 +456,6 @@ canvas#nexus-canvas {
font-style: italic;
}
/* Portal Status Wall */
.atlas-status-wall {
padding: 20px 30px;
border-top: 1px solid var(--color-border);
background: rgba(10, 15, 40, 0.5);
}
.status-wall-header {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 16px;
}
.status-wall-title {
font-family: var(--font-display);
font-size: 14px;
font-weight: 700;
color: var(--color-primary);
letter-spacing: 1px;
}
.status-wall-subtitle {
font-family: var(--font-body);
font-size: 11px;
color: var(--color-text-muted);
}
.status-wall-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 16px;
}
.status-wall-env {
background: rgba(20, 30, 60, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
padding: 12px;
border-radius: 4px;
}
.status-env-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.status-env-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-env-dot.production {
background: var(--color-primary);
box-shadow: 0 0 5px var(--color-primary);
}
.status-env-dot.staging {
background: var(--color-gold);
box-shadow: 0 0 5px var(--color-gold);
}
.status-env-dot.local {
background: #00ff88;
box-shadow: 0 0 5px #00ff88;
}
.status-env-label {
font-family: var(--font-display);
font-size: 11px;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
}
.status-env-portals {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 10px;
max-height: 120px;
overflow-y: auto;
}
.status-portal-row {
display: flex;
align-items: center;
justify-content: space-between;
font-family: var(--font-body);
font-size: 11px;
color: var(--color-text-muted);
padding: 4px 0;
}
.status-portal-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-portal-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
margin-left: 8px;
flex-shrink: 0;
}
.status-env-summary {
font-family: var(--font-body);
font-size: 10px;
color: var(--color-text-muted);
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.status-wall-legend {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-body);
font-size: 10px;
color: var(--color-text-muted);
}
.status-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-dot.online { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
.status-dot.active { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
.status-dot.standby { background: var(--color-gold); box-shadow: 0 0 4px var(--color-gold); }
.status-dot.rebuilding { background: #ffa500; box-shadow: 0 0 4px #ffa500; }
.status-dot.local-only { background: #00ff88; box-shadow: 0 0 4px #00ff88; }
.status-dot.blocked { background: #ff0000; box-shadow: 0 0 4px #ff0000; }
.status-dot.offline { background: var(--color-danger); box-shadow: 0 0 4px var(--color-danger); }
/* Additional status indicators for footer */
.status-indicator.rebuilding { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
.status-indicator.local-only { background: #00ff88; box-shadow: 0 0 5px #00ff88; }
.status-indicator.blocked { background: #ff0000; box-shadow: 0 0 5px #ff0000; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
@@ -586,12 +469,6 @@ canvas#nexus-canvas {
.atlas-content {
max-height: 90vh;
}
.status-wall-grid {
grid-template-columns: 1fr;
}
.atlas-status-wall {
padding: 15px 20px;
}
}
/* Debug overlay */
@@ -822,6 +699,95 @@ canvas#nexus-canvas {
border-radius: 4px;
animation: hint-float 2s ease-in-out infinite;
}
/* Portal Preview Card */
.portal-preview-card {
background: rgba(10, 15, 30, 0.95);
border: 1px solid var(--portal-color, var(--color-primary));
border-radius: 6px;
padding: 16px 20px;
min-width: 300px;
max-width: 400px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
}
.portal-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.portal-preview-name {
font-family: 'Orbitron', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--portal-color, var(--color-primary));
letter-spacing: 0.1em;
}
.portal-preview-readiness {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.08em;
}
.portal-preview-readiness.readiness-online,
.portal-preview-readiness.readiness-active {
background: rgba(74, 240, 192, 0.15);
color: #4af0c0;
border: 1px solid rgba(74, 240, 192, 0.3);
}
.portal-preview-readiness.readiness-standby {
background: rgba(255, 215, 0, 0.12);
color: #ffd700;
border: 1px solid rgba(255, 215, 0, 0.3);
}
.portal-preview-readiness.readiness-offline {
background: rgba(255, 68, 102, 0.12);
color: #ff4466;
border: 1px solid rgba(255, 68, 102, 0.3);
}
.portal-preview-desc {
font-size: 13px;
color: rgba(224, 240, 255, 0.7);
margin-bottom: 12px;
line-height: 1.4;
}
.portal-preview-meta {
font-size: 12px;
color: rgba(224, 240, 255, 0.6);
margin-bottom: 6px;
line-height: 1.4;
}
.portal-preview-label {
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
font-weight: 600;
color: var(--portal-color, var(--color-primary));
letter-spacing: 0.1em;
margin-right: 6px;
opacity: 0.8;
}
.portal-preview-footer {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
font-size: 12px;
color: rgba(224, 240, 255, 0.5);
}
.portal-hint-key {
background: var(--portal-color, var(--color-primary));
color: var(--color-bg);
font-weight: 700;
font-size: 11px;
padding: 2px 8px;
border-radius: 3px;
}
@keyframes hint-float {
0%, 100% { transform: translate(-50%, 100px); }
50% { transform: translate(-50%, 90px); }
@@ -1011,6 +977,58 @@ canvas#nexus-canvas {
text-align: center;
padding: var(--space-8);
}
.portal-overlay-details {
text-align: left;
margin: 16px auto;
max-width: 400px;
padding: 12px 16px;
background: rgba(10, 15, 30, 0.5);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
}
.portal-overlay-detail-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 6px 0;
font-size: 13px;
color: rgba(224, 240, 255, 0.7);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.portal-overlay-detail-row:last-child {
border-bottom: none;
}
.portal-overlay-detail-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 600;
color: var(--color-primary);
letter-spacing: 0.1em;
opacity: 0.8;
min-width: 90px;
}
.portal-readiness {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
letter-spacing: 0.06em;
}
.portal-readiness.readiness-online,
.portal-readiness.readiness-active {
background: rgba(74, 240, 192, 0.12);
color: #4af0c0;
}
.portal-readiness.readiness-standby {
background: rgba(255, 215, 0, 0.1);
color: #ffd700;
}
.portal-readiness.readiness-offline {
background: rgba(255, 68, 102, 0.1);
color: #ff4466;
}
.portal-overlay-header {
display: flex;
align-items: center;