Compare commits
1 Commits
2a7dbef0c9
...
d2706b18c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2706b18c2 |
307
app.js
307
app.js
@@ -34,8 +34,11 @@ let debugOverlay;
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let sessionPower = 1.0; // 0.0–1.0
|
||||
let sessionPowerMeter = null; // { group, fillMat, orbMat, ringMats, pointLight }
|
||||
|
||||
// Session power state
|
||||
let sessionPower = 1.0; // 0.0 → 1.0
|
||||
const SESSION_CREDITS_MAX = 10000;
|
||||
let sessionPowerMeter = null; // { fillMat, glowMat, orbMesh, orb, lightRef }
|
||||
|
||||
// ═══ INIT ═══
|
||||
function init() {
|
||||
@@ -790,53 +793,54 @@ function createAmbientStructures() {
|
||||
scene.add(pedestal);
|
||||
}
|
||||
|
||||
// ═══ SESSION POWER METER (3D) ═══
|
||||
// ═══ SESSION POWER METER ═══
|
||||
function createSessionPowerMeter() {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(-9, 0, 5);
|
||||
group.name = 'session-power-meter';
|
||||
// Place to the left, slightly in front — visible from spawn at (0,2,12)
|
||||
group.position.set(-9, 0, 4);
|
||||
group.name = 'power-meter-group';
|
||||
|
||||
const METER_HEIGHT = 4;
|
||||
const METER_RADIUS = 0.35;
|
||||
const METER_H = 4.0;
|
||||
const METER_R = 0.35;
|
||||
|
||||
// Outer glass housing
|
||||
const glassGeo = new THREE.CylinderGeometry(METER_RADIUS + 0.06, METER_RADIUS + 0.06, METER_HEIGHT, 32, 1, true);
|
||||
// --- Outer glass shell ---
|
||||
const glassGeo = new THREE.CylinderGeometry(METER_R + 0.08, METER_R + 0.08, METER_H, 32, 1, true);
|
||||
const glassMat = new THREE.MeshPhysicalMaterial({
|
||||
color: 0x88ddff,
|
||||
color: 0x88ccff,
|
||||
transparent: true,
|
||||
opacity: 0.18,
|
||||
opacity: 0.12,
|
||||
roughness: 0,
|
||||
metalness: 0,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const glass = new THREE.Mesh(glassGeo, glassMat);
|
||||
glass.position.y = METER_HEIGHT / 2;
|
||||
group.add(glass);
|
||||
group.add(new THREE.Mesh(glassGeo, glassMat));
|
||||
|
||||
// Glass cap top
|
||||
const capGeo = new THREE.CircleGeometry(METER_RADIUS + 0.06, 32);
|
||||
const capMat = new THREE.MeshPhysicalMaterial({ color: 0x88ddff, transparent: true, opacity: 0.22, roughness: 0 });
|
||||
const capTop = new THREE.Mesh(capGeo, capMat);
|
||||
capTop.rotation.x = -Math.PI / 2;
|
||||
capTop.position.y = METER_HEIGHT;
|
||||
group.add(capTop);
|
||||
const capBot = new THREE.Mesh(capGeo, capMat.clone());
|
||||
capBot.rotation.x = Math.PI / 2;
|
||||
capBot.position.y = 0;
|
||||
group.add(capBot);
|
||||
// Glass rim rings (top + bottom)
|
||||
for (const yOff of [METER_H / 2, -METER_H / 2]) {
|
||||
const rimGeo = new THREE.TorusGeometry(METER_R + 0.1, 0.04, 8, 32);
|
||||
const rimMat = new THREE.MeshStandardMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 1.2,
|
||||
roughness: 0.2,
|
||||
metalness: 0.8,
|
||||
});
|
||||
const rim = new THREE.Mesh(rimGeo, rimMat);
|
||||
rim.rotation.x = Math.PI / 2;
|
||||
rim.position.y = yOff;
|
||||
group.add(rim);
|
||||
}
|
||||
|
||||
// Animated energy fill — clips by UV.y against uFill
|
||||
const fillGeo = new THREE.CylinderGeometry(METER_RADIUS, METER_RADIUS, METER_HEIGHT, 32, 64, false);
|
||||
// --- Energy fill (shader-driven, clips at uPower) ---
|
||||
const fillGeo = new THREE.CylinderGeometry(METER_R - 0.02, METER_R - 0.02, METER_H, 32, 64, true);
|
||||
const fillMat = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
side: THREE.DoubleSide,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uFill: { value: 1.0 },
|
||||
uColorA: { value: new THREE.Color(0x4af0c0) },
|
||||
uColorB: { value: new THREE.Color(0x7b5cff) },
|
||||
uTime: { value: 0 },
|
||||
uPower: { value: 1.0 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
@@ -847,104 +851,141 @@ function createSessionPowerMeter() {
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform float uTime;
|
||||
uniform float uFill;
|
||||
uniform vec3 uColorA;
|
||||
uniform vec3 uColorB;
|
||||
uniform float uPower;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
if (vUv.y > uFill) discard;
|
||||
// vUv.y: 0 = bottom, 1 = top
|
||||
if (vUv.y > uPower) discard;
|
||||
|
||||
// Gradient from bottom (colorA) to top (colorB)
|
||||
vec3 col = mix(uColorA, uColorB, vUv.y / max(uFill, 0.01));
|
||||
float t = (uPower > 0.001) ? (vUv.y / uPower) : 0.0;
|
||||
|
||||
// Scan lines rippling upward
|
||||
float scan = sin(vUv.y * 40.0 - uTime * 3.0) * 0.5 + 0.5;
|
||||
col += col * scan * 0.25;
|
||||
// Teal → purple gradient based on remaining power color
|
||||
vec3 colLow = vec3(1.0, 0.27, 0.40); // red danger
|
||||
vec3 colMid = vec3(0.29, 0.94, 0.75); // teal
|
||||
vec3 colHigh = vec3(0.48, 0.36, 1.00); // purple
|
||||
|
||||
// Bright band at the fill edge
|
||||
float edge = smoothstep(0.0, 0.04, uFill - vUv.y);
|
||||
float glow = (1.0 - edge) * 2.0;
|
||||
col += col * glow;
|
||||
// Blend between danger and normal based on power level
|
||||
float powerBlend = smoothstep(0.0, 0.25, uPower);
|
||||
vec3 baseBot = mix(colLow, colMid, powerBlend);
|
||||
vec3 baseTop = mix(colLow, colHigh, powerBlend);
|
||||
vec3 col = mix(baseBot, baseTop, t);
|
||||
|
||||
// Pulse
|
||||
float pulse = 0.75 + 0.25 * sin(uTime * 2.5);
|
||||
float alpha = (0.55 + 0.2 * scan) * pulse;
|
||||
// Scan lines
|
||||
float scan = sin(vUv.y * 120.0 - uTime * 3.0) * 0.5 + 0.5;
|
||||
col += col * scan * 0.08;
|
||||
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
// Bright band at fill edge
|
||||
float edge = smoothstep(0.0, 0.04, uPower - vUv.y);
|
||||
col += vec3(1.0) * (1.0 - edge) * 0.6;
|
||||
|
||||
// Pulse glow
|
||||
float pulse = sin(uTime * 2.5) * 0.5 + 0.5;
|
||||
col += col * pulse * 0.15;
|
||||
|
||||
float alpha = mix(0.55, 0.85, t) * edge;
|
||||
gl_FragColor = vec4(col, alpha + 0.1);
|
||||
}
|
||||
`,
|
||||
});
|
||||
const fill = new THREE.Mesh(fillGeo, fillMat);
|
||||
fill.position.y = METER_HEIGHT / 2;
|
||||
group.add(fill);
|
||||
const fillMesh = new THREE.Mesh(fillGeo, fillMat);
|
||||
group.add(fillMesh);
|
||||
|
||||
// Accent rings
|
||||
const ringMats = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ringGeo = new THREE.TorusGeometry(METER_RADIUS + 0.09, 0.025, 8, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: i === 0 ? 0x4af0c0 : i === 1 ? 0x7b5cff : 0x4af0c0,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.position.y = METER_HEIGHT * (0.25 + i * 0.25);
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
ring.name = 'spm-ring-' + i;
|
||||
group.add(ring);
|
||||
ringMats.push({ mat: ringMat, baseY: METER_HEIGHT * (0.25 + i * 0.25) });
|
||||
}
|
||||
|
||||
// Floating indicator orb at fill level
|
||||
// --- Floating orb that tracks fill height ---
|
||||
const orbGeo = new THREE.SphereGeometry(0.12, 16, 16);
|
||||
const orbMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x4af0c0,
|
||||
emissive: 0x4af0c0,
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 3,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
});
|
||||
const orb = new THREE.Mesh(orbGeo, orbMat);
|
||||
orb.name = 'spm-orb';
|
||||
orb.position.y = METER_HEIGHT; // starts at top, updated in loop
|
||||
orb.position.set(0, METER_H / 2 - METER_H * (1.0 - sessionPower) - METER_H / 2, 0);
|
||||
group.add(orb);
|
||||
|
||||
// Dynamic point light that tracks the orb
|
||||
const powerLight = new THREE.PointLight(0x4af0c0, 2.5, 8, 1.8);
|
||||
powerLight.position.set(0, METER_HEIGHT, 0);
|
||||
powerLight.name = 'spm-light';
|
||||
// --- Dynamic point light at orb position ---
|
||||
const powerLight = new THREE.PointLight(NEXUS.colors.primary, 2.5, 8, 2);
|
||||
powerLight.position.copy(orb.position);
|
||||
group.add(powerLight);
|
||||
|
||||
// Canvas label
|
||||
const labelCanvas = document.createElement('canvas');
|
||||
labelCanvas.width = 512;
|
||||
labelCanvas.height = 80;
|
||||
const lctx = labelCanvas.getContext('2d');
|
||||
lctx.font = 'bold 28px "Orbitron", sans-serif';
|
||||
// --- 3 decorative rings (spin, dim above fill) ---
|
||||
const rings = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const rGeo = new THREE.TorusGeometry(METER_R + 0.25 + i * 0.12, 0.025, 6, 32);
|
||||
const rMat = new THREE.MeshBasicMaterial({
|
||||
color: NEXUS.colors.secondary,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
});
|
||||
const ring = new THREE.Mesh(rGeo, rMat);
|
||||
ring.position.y = -METER_H / 2 + (i + 1) * (METER_H / 4);
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
group.add(ring);
|
||||
rings.push({ mesh: ring, baseY: ring.position.y, index: i });
|
||||
}
|
||||
|
||||
// --- Canvas label ---
|
||||
const lc = document.createElement('canvas');
|
||||
lc.width = 256; lc.height = 80;
|
||||
const lctx = lc.getContext('2d');
|
||||
lctx.font = 'bold 20px "Orbitron", sans-serif';
|
||||
lctx.fillStyle = '#4af0c0';
|
||||
lctx.textAlign = 'center';
|
||||
lctx.fillText('⚡ SESSION POWER', 256, 32);
|
||||
lctx.font = '18px "JetBrains Mono", monospace';
|
||||
lctx.fillStyle = '#5a8888';
|
||||
lctx.fillText('Fund once · Ask many', 256, 60);
|
||||
const labelTex = new THREE.CanvasTexture(labelCanvas);
|
||||
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide, depthWrite: false });
|
||||
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(3, 0.5), labelMat);
|
||||
labelMesh.position.y = METER_HEIGHT + 0.9;
|
||||
group.add(labelMesh);
|
||||
|
||||
// Base pedestal
|
||||
const baseGeo = new THREE.CylinderGeometry(0.55, 0.7, 0.25, 8);
|
||||
const baseMat = new THREE.MeshStandardMaterial({ color: 0x0a0f1a, roughness: 0.4, metalness: 0.9, emissive: 0x0a1a2a, emissiveIntensity: 0.4 });
|
||||
const base = new THREE.Mesh(baseGeo, baseMat);
|
||||
base.position.y = -0.12;
|
||||
base.castShadow = true;
|
||||
group.add(base);
|
||||
lctx.fillText('SESSION POWER', 128, 28);
|
||||
lctx.font = '13px "JetBrains Mono", monospace';
|
||||
lctx.fillStyle = '#7b5cff';
|
||||
lctx.fillText('Fund once · Ask many', 128, 56);
|
||||
const lTex = new THREE.CanvasTexture(lc);
|
||||
const lMat = new THREE.MeshBasicMaterial({ map: lTex, transparent: true, side: THREE.DoubleSide, depthWrite: false });
|
||||
const label = new THREE.Mesh(new THREE.PlaneGeometry(2.2, 0.7), lMat);
|
||||
label.position.y = METER_H / 2 + 0.65;
|
||||
group.add(label);
|
||||
|
||||
scene.add(group);
|
||||
|
||||
sessionPowerMeter = { group, fillMat, orbMat, ringMats, powerLight, orb, METER_HEIGHT };
|
||||
sessionPowerMeter = { fillMat, orb, powerLight, rings, group, METER_H };
|
||||
}
|
||||
|
||||
function updateSessionPowerMeter(elapsed) {
|
||||
if (!sessionPowerMeter) return;
|
||||
const { fillMat, orb, powerLight, rings, METER_H } = sessionPowerMeter;
|
||||
|
||||
fillMat.uniforms.uTime.value = elapsed;
|
||||
fillMat.uniforms.uPower.value = sessionPower;
|
||||
|
||||
// Orb tracks the fill top edge
|
||||
const fillTopY = -METER_H / 2 + sessionPower * METER_H;
|
||||
orb.position.y = fillTopY + Math.sin(elapsed * 3.0) * 0.06;
|
||||
orb.material.emissive.setHex(sessionPower < 0.2 ? NEXUS.colors.danger : NEXUS.colors.primary);
|
||||
powerLight.position.y = orb.position.y;
|
||||
powerLight.color.setHex(sessionPower < 0.2 ? NEXUS.colors.danger : NEXUS.colors.primary);
|
||||
powerLight.intensity = 1.5 + Math.sin(elapsed * 2.5) * 0.5;
|
||||
|
||||
// Rings: dim those above current fill level, spin at different rates
|
||||
rings.forEach(({ mesh, baseY, index }) => {
|
||||
const inFill = baseY < fillTopY;
|
||||
mesh.material.opacity = inFill ? 0.7 : 0.15;
|
||||
mesh.rotation.z = elapsed * (0.4 + index * 0.25);
|
||||
});
|
||||
|
||||
// HUD update
|
||||
const pct = Math.round(sessionPower * 100);
|
||||
const credits = Math.round(sessionPower * SESSION_CREDITS_MAX).toLocaleString();
|
||||
const pmPct = document.getElementById('pm-pct');
|
||||
const pmBar = document.getElementById('pm-bar');
|
||||
const pmCredits = document.getElementById('pm-credits');
|
||||
const pmWarn = document.getElementById('pm-warn');
|
||||
if (pmPct) pmPct.textContent = pct + '%';
|
||||
if (pmCredits) pmCredits.textContent = credits + ' CR';
|
||||
if (pmBar) {
|
||||
pmBar.style.width = pct + '%';
|
||||
const isLow = sessionPower < 0.2;
|
||||
pmBar.style.background = isLow
|
||||
? 'linear-gradient(90deg, #ff2244, #ff6644)'
|
||||
: 'linear-gradient(90deg, #4af0c0, #7b5cff)';
|
||||
}
|
||||
if (pmWarn) pmWarn.style.display = sessionPower < 0.2 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// ═══ CONTROLS ═══
|
||||
@@ -1101,6 +1142,10 @@ function gameLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Drain session power slowly (full drain in ~16 min for demo)
|
||||
sessionPower = Math.max(0, sessionPower - delta * 0.001);
|
||||
updateSessionPowerMeter(elapsed);
|
||||
|
||||
// Animate nexus core
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) {
|
||||
@@ -1110,64 +1155,6 @@ function gameLoop() {
|
||||
core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
||||
}
|
||||
|
||||
// Animate session power meter
|
||||
if (sessionPowerMeter) {
|
||||
const { fillMat, orbMat, ringMats, powerLight, orb, METER_HEIGHT } = sessionPowerMeter;
|
||||
|
||||
// Slowly drain power over time (demo — ~1% per 5 seconds)
|
||||
sessionPower = Math.max(0, sessionPower - delta * 0.002);
|
||||
|
||||
// Update 3D fill shader
|
||||
if (fillMat.uniforms) {
|
||||
fillMat.uniforms.uTime.value = elapsed;
|
||||
fillMat.uniforms.uFill.value = sessionPower;
|
||||
// Switch to danger colors at low power
|
||||
if (sessionPower < 0.2) {
|
||||
fillMat.uniforms.uColorA.value.set(0xff4466);
|
||||
fillMat.uniforms.uColorB.value.set(0xff8844);
|
||||
} else {
|
||||
fillMat.uniforms.uColorA.value.set(0x4af0c0);
|
||||
fillMat.uniforms.uColorB.value.set(0x7b5cff);
|
||||
}
|
||||
}
|
||||
|
||||
// Orb tracks fill height + subtle bob
|
||||
const orbY = sessionPower * METER_HEIGHT + Math.sin(elapsed * 3) * 0.06;
|
||||
orb.position.y = orbY;
|
||||
orb.rotation.y = elapsed * 2;
|
||||
powerLight.position.y = orbY;
|
||||
powerLight.intensity = 1.5 + sessionPower + Math.sin(elapsed * 3) * 0.4;
|
||||
powerLight.color.set(sessionPower < 0.2 ? 0xff4466 : 0x4af0c0);
|
||||
|
||||
// Dim rings above fill level
|
||||
ringMats.forEach(({ mat, baseY }) => {
|
||||
const ringNorm = baseY / METER_HEIGHT;
|
||||
mat.opacity = ringNorm <= sessionPower ? 0.8 : 0.15;
|
||||
});
|
||||
|
||||
// Update HUD
|
||||
const hudEl = document.getElementById('session-power-hud');
|
||||
const fill = document.getElementById('spm-fill');
|
||||
const tip = document.getElementById('spm-tip');
|
||||
const pct = document.getElementById('spm-pct');
|
||||
const cred = document.getElementById('spm-credits');
|
||||
const warn = document.getElementById('spm-warn');
|
||||
if (hudEl && fill && pct) {
|
||||
const pctVal = Math.round(sessionPower * 100);
|
||||
fill.style.width = pctVal + '%';
|
||||
pct.textContent = pctVal + '%';
|
||||
if (cred) {
|
||||
const credVal = Math.round(sessionPower * 10000).toLocaleString();
|
||||
cred.textContent = credVal + ' / 10,000';
|
||||
}
|
||||
const lowPower = sessionPower < 0.2;
|
||||
hudEl.classList.toggle('low-power', lowPower);
|
||||
if (warn) warn.style.display = lowPower ? 'block' : 'none';
|
||||
// Tip position tracks fill percentage
|
||||
if (tip) tip.style.right = (100 - pctVal) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Render
|
||||
composer.render();
|
||||
|
||||
|
||||
33
index.html
33
index.html
@@ -95,28 +95,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Power Meter HUD -->
|
||||
<div id="session-power-hud" class="session-power-hud">
|
||||
<div class="spm-header">
|
||||
<span class="spm-icon">⚡</span>
|
||||
<span class="spm-title">SESSION POWER</span>
|
||||
<!-- Session Power Meter HUD (top-right) -->
|
||||
<div id="power-meter-hud" class="power-meter-hud">
|
||||
<div class="pm-header">
|
||||
<span class="pm-icon">⚡</span>
|
||||
<span class="pm-title">SESSION POWER</span>
|
||||
<span class="pm-badge">SOVEREIGN</span>
|
||||
</div>
|
||||
<div class="spm-bar-wrap">
|
||||
<div id="spm-bar" class="spm-bar">
|
||||
<div id="spm-fill" class="spm-fill"></div>
|
||||
<div id="spm-tip" class="spm-tip"></div>
|
||||
</div>
|
||||
<span id="spm-pct" class="spm-pct">100%</span>
|
||||
<div class="pm-bar-track">
|
||||
<div class="pm-bar" id="pm-bar"></div>
|
||||
</div>
|
||||
<div class="spm-credits-row">
|
||||
<span class="spm-label">Credits</span>
|
||||
<span id="spm-credits" class="spm-credits-val">10,000 / 10,000</span>
|
||||
<div class="pm-stats">
|
||||
<span id="pm-pct" class="pm-pct">100%</span>
|
||||
<span id="pm-credits" class="pm-credits">10,000 CR</span>
|
||||
</div>
|
||||
<div class="spm-tier-row">
|
||||
<span id="spm-tier" class="spm-tier-badge">SOVEREIGN</span>
|
||||
<div id="pm-warn" class="pm-warn" style="display:none;">
|
||||
<span class="pm-warn-icon">⚠</span>
|
||||
<span>Low power — top up session</span>
|
||||
</div>
|
||||
<div class="spm-tagline">Fund once · Ask many models</div>
|
||||
<div id="spm-warn" class="spm-warn" style="display:none;">⚠ LOW POWER — refill to continue</div>
|
||||
<div class="pm-tagline">Fund once · Ask many models</div>
|
||||
</div>
|
||||
|
||||
<!-- Minimap / Controls hint -->
|
||||
|
||||
268
style.css
268
style.css
@@ -330,6 +330,133 @@ canvas#nexus-canvas {
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
}
|
||||
|
||||
/* === SESSION POWER METER HUD === */
|
||||
.power-meter-hud {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-4);
|
||||
width: 200px;
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--panel-radius);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.pm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.pm-icon {
|
||||
font-size: 13px;
|
||||
color: var(--color-primary);
|
||||
filter: drop-shadow(0 0 4px var(--color-primary));
|
||||
}
|
||||
|
||||
.pm-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-bright);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pm-badge {
|
||||
font-family: var(--font-display);
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-secondary);
|
||||
border: 1px solid rgba(123, 92, 255, 0.4);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pm-bar-track {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pm-bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
|
||||
transition: width 0.5s ease, background 0.5s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pm-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 6px var(--color-primary), 0 0 12px var(--color-primary);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.pm-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.pm-pct {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-shadow: 0 0 10px rgba(74, 240, 192, 0.4);
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
}
|
||||
|
||||
.pm-credits {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.pm-warn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-danger);
|
||||
margin-bottom: var(--space-2);
|
||||
animation: warn-flash 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pm-warn-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@keyframes warn-flash {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.pm-tagline {
|
||||
font-size: 9px;
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* === FOOTER === */
|
||||
.nexus-footer {
|
||||
position: fixed;
|
||||
@@ -348,147 +475,6 @@ canvas#nexus-canvas {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* === SESSION POWER METER HUD === */
|
||||
.session-power-hud {
|
||||
position: absolute;
|
||||
top: var(--space-4);
|
||||
right: var(--space-4);
|
||||
width: 220px;
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--panel-radius);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
pointer-events: none;
|
||||
transition: border-color var(--transition-ui);
|
||||
}
|
||||
.session-power-hud.low-power {
|
||||
border-color: rgba(255, 68, 102, 0.5);
|
||||
animation: spm-warn-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes spm-warn-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 68, 102, 0); }
|
||||
50% { box-shadow: 0 0 12px 2px rgba(255, 68, 102, 0.25); }
|
||||
}
|
||||
.spm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.spm-icon {
|
||||
font-size: 14px;
|
||||
animation: spm-icon-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes spm-icon-pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.spm-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.spm-bar-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.spm-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(74, 240, 192, 0.08);
|
||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
.spm-fill {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s ease, background 0.4s ease;
|
||||
}
|
||||
.session-power-hud.low-power .spm-fill {
|
||||
background: linear-gradient(90deg, var(--color-danger), #ff8844);
|
||||
}
|
||||
.spm-tip {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translate(50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 8px var(--color-primary);
|
||||
transition: background 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
.session-power-hud.low-power .spm-tip {
|
||||
background: var(--color-danger);
|
||||
box-shadow: 0 0 8px var(--color-danger);
|
||||
}
|
||||
.spm-pct {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
transition: color 0.4s ease;
|
||||
}
|
||||
.session-power-hud.low-power .spm-pct {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.spm-credits-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
.spm-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.spm-credits-val {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.spm-tier-row {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.spm-tier-badge {
|
||||
font-family: var(--font-display);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.15em;
|
||||
font-weight: 700;
|
||||
color: var(--color-gold);
|
||||
background: rgba(255, 215, 0, 0.08);
|
||||
border: 1px solid rgba(255, 215, 0, 0.25);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.spm-tagline {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.spm-warn {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-danger);
|
||||
animation: spm-warn-text 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes spm-warn-text {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.chat-panel {
|
||||
|
||||
Reference in New Issue
Block a user