legion: war camp — class banners + alive/fallen soldier instances (2.2.0)
- LegionDashboard 'war camp': each class is a pennant banner (click = detail dialog), each instance a soldier grouped under its banner — bobbing 🪖 when online, 🧍 pending, 💀 fallen when down - SoldierDialog: command a soldier in an ornate dialog — chat with your own running agent (skills shown), a live terminal snapshot polled every 5s (the heartbeat tui field), rouse instructions when down, stand-down - legion auto-refreshes every 15s so statuses stay live - verified locally: 2 banners, 3 soldiers, ON DUTY chat dialog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
374440a392
commit
e308c9be4d
@ -17,7 +17,7 @@
|
|||||||
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
||||||
</style>
|
</style>
|
||||||
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
||||||
<meta name="version" content="2.1.0-roster" />
|
<meta name="version" content="2.2.0-warcamp" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amerc-site",
|
"name": "amerc-site",
|
||||||
"version": "2.1.0-roster",
|
"version": "2.2.0-warcamp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -293,6 +293,73 @@ export function KeysCard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A fielded instance = a soldier: alive (online), standing-by (pending), fallen (lost/down).
|
||||||
|
const SOL_FIG = { online: '🪖', pending: '🧍', lost: '🛡️', down: '💀' };
|
||||||
|
function relHb(at) { if (!at) return 'never'; const s = Math.floor((Date.now() - at) / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; return `${Math.floor(m / 60)}h ago`; }
|
||||||
|
function Soldier({ i, onOpen }) {
|
||||||
|
const klass = i.status === 'online' ? 'alive' : i.status === 'pending' ? 'pending' : 'fallen';
|
||||||
|
return (
|
||||||
|
<button className={`lg-sol ${klass}`} onClick={() => onOpen(i)} title={`#${i.id} · ${i.status}`}>
|
||||||
|
<span className="lg-sol-fig" aria-hidden="true">{SOL_FIG[i.status] || '💀'}</span>
|
||||||
|
<span className="lg-sol-id">#{i.id}</span>
|
||||||
|
<span className="lg-sol-status">{i.status}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soldier detail: live status + terminal snapshot, and chat with your own soldier.
|
||||||
|
function SoldierDialog({ instance, onClose, onChanged }) {
|
||||||
|
const [detail, setDetail] = useState(null);
|
||||||
|
const [tab, setTab] = useState(instance.status === 'online' ? 'chat' : 'status');
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
const f = () => api(`/agents/instances/${instance.id}${instance.accesskey ? `?accesskey=${encodeURIComponent(instance.accesskey)}` : ''}`).then((d) => { if (alive && d.instance) setDetail({ ...d.instance, tui: d.tui }); }).catch(() => {});
|
||||||
|
f(); const t = setInterval(f, 5000); return () => { alive = false; clearInterval(t); };
|
||||||
|
}, [instance]);
|
||||||
|
const live = detail || instance;
|
||||||
|
const online = live.status === 'online';
|
||||||
|
const hb = `while true; do curl -s -XPOST https://amerc.ai/api/agents/instances/${instance.id}/heartbeat -H 'content-type: application/json' -d '{"accesskey":"${instance.accesskey}","status":"online","broker":"my-broker"}' >/dev/null; sleep 10; done`;
|
||||||
|
const standDown = async () => {
|
||||||
|
if (!window.confirm('Stand this soldier down? (shuts the instance down)')) return;
|
||||||
|
try { await api(`/agents/instances/${instance.id}`, { method: 'DELETE', body: { accesskey: instance.accesskey } }); onChanged?.(); onClose(); } catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<GameDialog title={live.name || `Soldier #${instance.id}`} tag={online ? '🪖 ON DUTY' : live.status === 'pending' ? '🧍 STANDING BY' : '💀 FALLEN'} wide onClose={onClose}>
|
||||||
|
<div className="lg-sd-head">
|
||||||
|
<span className={`ch-dot ${live.status}`} />
|
||||||
|
<div className="lg-sd-meta">
|
||||||
|
<strong>#{instance.id} {live.name}</strong>
|
||||||
|
<span>{live.status}{live.broker ? ` · broker ${live.broker}` : ''} · heartbeat {relHb(live.last_heartbeat)}</span>
|
||||||
|
</div>
|
||||||
|
{instance.accesskey && <code className="ap-token lg-sd-key" onClick={() => copyToast(instance.accesskey)} title="broker accesskey">{instance.accesskey.slice(0, 14)}… ⧉</code>}
|
||||||
|
<button className="px-action lg-standdown" onClick={standDown}>stand down</button>
|
||||||
|
</div>
|
||||||
|
<div className="lg-sd-tabs">
|
||||||
|
<button className={tab === 'chat' ? 'on' : ''} onClick={() => setTab('chat')}>Chat</button>
|
||||||
|
<button className={tab === 'status' ? 'on' : ''} onClick={() => setTab('status')}>Status</button>
|
||||||
|
</div>
|
||||||
|
{tab === 'chat' && (online
|
||||||
|
? <ChatPanel instance={live} accesskey={instance.accesskey} onClose={onClose} />
|
||||||
|
: <p className="ap-hint" style={{ padding: '16px 4px' }}>This soldier is {live.status}. Rouse it from the Status tab, then chat.</p>)}
|
||||||
|
{tab === 'status' && (
|
||||||
|
<div className="lg-sd-status">
|
||||||
|
{live.skills?.length > 0 && <div className="ch-skills"><span className="ch-skills-label">skills loaded</span>{live.skills.map((s) => <span key={s} className="ch-skill">{s}</span>)}</div>}
|
||||||
|
<div className="lg-tui">
|
||||||
|
<div className="lg-tui-head">live terminal · refreshes every 5s</div>
|
||||||
|
<pre>{live.tui || '(no terminal snapshot yet — your broker can post one via the heartbeat `tui` field)'}</pre>
|
||||||
|
</div>
|
||||||
|
{!online && (
|
||||||
|
<div className="lg-rouse">
|
||||||
|
<p className="ap-hint">Fallen until a broker checks in. Paste this anywhere with <code>curl</code> to rouse it:</p>
|
||||||
|
<div className="ap-codeblock"><code>{hb}</code><button className="px-action" onClick={() => copyToast(hb, 'Heartbeat loop copied')}>copy</button></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</GameDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function LegionDashboard({ auth, setRoute }) {
|
export function LegionDashboard({ auth, setRoute }) {
|
||||||
const [classes, setClasses] = useState([]);
|
const [classes, setClasses] = useState([]);
|
||||||
const [insts, setInsts] = useState([]);
|
const [insts, setInsts] = useState([]);
|
||||||
@ -301,6 +368,7 @@ export function LegionDashboard({ auth, setRoute }) {
|
|||||||
const [newInst, setNewInst] = useState(null);
|
const [newInst, setNewInst] = useState(null);
|
||||||
const [logOpen, setLogOpen] = useState(null); // chatlog session to reopen
|
const [logOpen, setLogOpen] = useState(null); // chatlog session to reopen
|
||||||
const [openCls, setOpenCls] = useState(null); // class modal (same as Browse)
|
const [openCls, setOpenCls] = useState(null); // class modal (same as Browse)
|
||||||
|
const [soldier, setSoldier] = useState(null); // soldier (instance) detail
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!newInst) return;
|
if (!newInst) return;
|
||||||
@ -318,7 +386,7 @@ export function LegionDashboard({ auth, setRoute }) {
|
|||||||
setInsts(all.flat());
|
setInsts(all.flat());
|
||||||
} catch (e) { setErr(e.message); } finally { setLoaded(true); }
|
} catch (e) { setErr(e.message); } finally { setLoaded(true); }
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => { if (auth.user) load(); }, [auth.user, load]);
|
useEffect(() => { if (!auth.user) return undefined; load(); const t = setInterval(load, 15000); return () => clearInterval(t); }, [auth.user, load]);
|
||||||
|
|
||||||
if (!auth.ready) return <div className="ap"><p>Loading…</p></div>;
|
if (!auth.ready) return <div className="ap"><p>Loading…</p></div>;
|
||||||
if (!auth.user) return (
|
if (!auth.user) return (
|
||||||
@ -342,8 +410,6 @@ export function LegionDashboard({ auth, setRoute }) {
|
|||||||
try { const r = await api('/agents/instances', { method: 'POST', body: { classId, visibility: 'public' } }); setNewInst(r); load(); }
|
try { const r = await api('/agents/instances', { method: 'POST', body: { classId, visibility: 'public' } }); setNewInst(r); load(); }
|
||||||
catch (e2) { setErr(e2.message); }
|
catch (e2) { setErr(e2.message); }
|
||||||
};
|
};
|
||||||
const shutdown = async (i) => { try { await api(`/agents/instances/${i.id}`, { method: 'DELETE', body: { accesskey: i.accesskey } }); load(); } catch (e2) { setErr(e2.message); } };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ap">
|
<div className="ap">
|
||||||
<div className="ap-head"><h2>My Legion</h2><span className="ap-by">{auth.user.handle}</span></div>
|
<div className="ap-head"><h2>My Legion</h2><span className="ap-by">{auth.user.handle}</span></div>
|
||||||
@ -409,27 +475,26 @@ export function LegionDashboard({ auth, setRoute }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h4 className="ap-h4">My classes — your banners on the roster</h4>
|
<h4 className="ap-h4">⚔ The war camp — tap a banner for details, a soldier to command</h4>
|
||||||
<div className="ap-mine-cards">
|
<div className="lg-camp">
|
||||||
{classes.map((c) => (
|
{classes.map((c) => {
|
||||||
<div key={c.id} className="ap-mine-cardwrap">
|
const squad = insts.filter((i) => i.class_id === c.id);
|
||||||
<AgentCard c={c} onClick={() => setOpenCls(c.id)} />
|
return (
|
||||||
<div className="ap-mine-actions">
|
<div key={c.id} className="lg-unit">
|
||||||
<span className="ap-kind">{c.visibility}</span>
|
<button className="lg-banner" onClick={() => setOpenCls(c.id)} title={`${c.name} — open details`}>
|
||||||
<button className="px-action" onClick={() => publishInstance(c.id)}>+ Field an instance</button>
|
<span className="lg-banner-crest" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
||||||
|
<strong>{c.name}</strong>
|
||||||
|
<span className="lg-banner-sub">{c.visibility}{c.managed ? ' · 🛡' : ''}</span>
|
||||||
|
<span className="lg-banner-count"><i className={c.onlineCount ? 'pulse' : ''} style={{ background: c.onlineCount ? STATUS_COLOR.online : '#555' }} />{c.onlineCount}/{c.instanceCount}</span>
|
||||||
|
</button>
|
||||||
|
<div className="lg-soldiers">
|
||||||
|
{squad.map((i) => <Soldier key={i.id} i={i} onOpen={setSoldier} />)}
|
||||||
|
<button className="lg-recruit" onClick={() => publishInstance(c.id)} title="field a new soldier">+</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="ap-h4">My instances</h4>
|
|
||||||
{insts.map((i) => (
|
|
||||||
<div key={i.id} className="ap-mine-inst">
|
|
||||||
<span className={`ap-inst-status${i.status === 'online' ? ' pulse' : ''}`} style={{ background: STATUS_COLOR[i.status] }} /> #{i.id} <em>{i.status}</em>
|
|
||||||
{i.accesskey && <code className="ap-token" title="accesskey — your broker connects with this" onClick={() => copyToast(i.accesskey)}>{i.accesskey.slice(0, 14)}… ⧉</code>}
|
|
||||||
<button className="px-action" onClick={() => shutdown(i)}>shut down</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!insts.length && <p className="ap-hint">No instances. Publish one from a class above.</p>}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -448,6 +513,7 @@ export function LegionDashboard({ auth, setRoute }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{openCls && <ClassModal id={openCls} auth={auth} onClose={() => { setOpenCls(null); load(); }} />}
|
{openCls && <ClassModal id={openCls} auth={auth} onClose={() => { setOpenCls(null); load(); }} />}
|
||||||
|
{soldier && <SoldierDialog instance={soldier} onClose={() => { setSoldier(null); load(); }} onChanged={load} />}
|
||||||
{newInst && (() => {
|
{newInst && (() => {
|
||||||
const hb = `while true; do curl -s -XPOST https://amerc.ai/api/agents/instances/${newInst.id}/heartbeat -H 'content-type: application/json' -d '{"accesskey":"${newInst.accesskey}","status":"online","broker":"my-broker"}' >/dev/null; sleep 10; done`;
|
const hb = `while true; do curl -s -XPOST https://amerc.ai/api/agents/instances/${newInst.id}/heartbeat -H 'content-type: application/json' -d '{"accesskey":"${newInst.accesskey}","status":"online","broker":"my-broker"}' >/dev/null; sleep 10; done`;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { api } from './api.js';
|
|||||||
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
|
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
|
||||||
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
|
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
|
||||||
|
|
||||||
const RELEASE = '2.1.0-roster';
|
const RELEASE = '2.2.0-warcamp';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ id: 'home', label: 'Tavern' },
|
{ id: 'home', label: 'Tavern' },
|
||||||
@ -476,6 +476,7 @@ function StatusPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CHANGELOG = [
|
const CHANGELOG = [
|
||||||
|
{ v: '2.2', date: 'Jun 2026', title: 'My Legion becomes a war camp', notes: ['Your classes are banners and each instance is a soldier — alive and bobbing when online, standing by when pending, fallen when down', 'Tap a soldier to command it: chat with your own running agent, watch its live terminal, or stand it down', 'Statuses refresh on their own so the camp stays current'] },
|
||||||
{ v: '2.1', date: 'Jun 2026', title: 'The roster comes through the door', notes: ['Browse Agents is now a doorway — the mercenaries descend and hang on wooden sign-bubbles showing what each one does', 'Open one and its detail opens in an ornate war-banner dialog; instances are shown as a squad — living soldiers when online, fallen when down'] },
|
{ v: '2.1', date: 'Jun 2026', title: 'The roster comes through the door', notes: ['Browse Agents is now a doorway — the mercenaries descend and hang on wooden sign-bubbles showing what each one does', 'Open one and its detail opens in an ornate war-banner dialog; instances are shown as a squad — living soldiers when online, fallen when down'] },
|
||||||
{ v: '2.0', date: 'Jun 2026', title: 'The tavern becomes the interface', notes: ['The home page is now a full-screen game scene — every fixture is a door: the grand door is the roster, the bar signs you in, the banners are your Legion, the side passage leads to My Mansion', 'Speech bubbles everywhere: the dancer pitches the tour in a glowing holo-panel, the barkeep greets you by name', 'Details open in ornate in-game dialogs instead of plain pages'] },
|
{ v: '2.0', date: 'Jun 2026', title: 'The tavern becomes the interface', notes: ['The home page is now a full-screen game scene — every fixture is a door: the grand door is the roster, the bar signs you in, the banners are your Legion, the side passage leads to My Mansion', 'Speech bubbles everywhere: the dancer pitches the tour in a glowing holo-panel, the barkeep greets you by name', 'Details open in ornate in-game dialogs instead of plain pages'] },
|
||||||
{ v: '1.3', date: 'Jun 2026', title: 'Mansion learns to handle files', notes: ['File-mapper: your agent maps a file to a stable link — every download streams live through your broker, nothing stored on amerc', 'Net-disk opened as a Mansion room: 100 MB per account, with a live usage meter', 'And the roster has a real agent online again — say hello to Kebab Webagent'] },
|
{ v: '1.3', date: 'Jun 2026', title: 'Mansion learns to handle files', notes: ['File-mapper: your agent maps a file to a stable link — every download streams live through your broker, nothing stored on amerc', 'Net-disk opened as a Mansion room: 100 MB per account, with a live usage meter', 'And the roster has a real agent online again — say hello to Kebab Webagent'] },
|
||||||
|
|||||||
@ -1765,3 +1765,45 @@ button {
|
|||||||
.cm-sol-meta { display: flex; align-items: center; gap: 6px; color: #8a7bb0; font-size: 11px; }
|
.cm-sol-meta { display: flex; align-items: center; gap: 6px; color: #8a7bb0; font-size: 11px; }
|
||||||
.cm-sol-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
.cm-sol-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
||||||
@media (max-width: 620px) { .apb-merc { width: 132px; } .apb-doorway { flex-wrap: wrap; } }
|
@media (max-width: 620px) { .apb-merc { width: 132px; } .apb-doorway { flex-wrap: wrap; } }
|
||||||
|
|
||||||
|
/* ===== My Legion = the war camp (2.2): banners + soldiers ===== */
|
||||||
|
.lg-camp { display: grid; grid-template-columns: repeat(auto-fill, minmax(184px, 1fr)); gap: 18px; margin-bottom: 8px; }
|
||||||
|
.lg-unit { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
||||||
|
.lg-banner { position: relative; width: 100%; padding: 14px 10px 26px; display: flex; flex-direction: column; align-items: center; gap: 5px; cursor: pointer; font-family: inherit; color: #eee6ff; background: linear-gradient(180deg, #2a1d4e, #170f2c); border: 1px solid #443a7e; border-top: 4px solid #7a6ab0; clip-path: polygon(0 0, 100% 0, 100% 88%, 50% 100%, 0 88%); transition: transform 0.14s, border-color 0.14s; }
|
||||||
|
.lg-banner:hover { transform: translateY(-3px); border-top-color: #ffd75e; }
|
||||||
|
.lg-banner::before { content: ''; position: absolute; top: -10px; left: 50%; width: 8px; height: 8px; border-radius: 50%; background: #ffd75e; transform: translateX(-50%); box-shadow: 0 0 8px #ffd75e; }
|
||||||
|
.lg-banner-crest { width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 0 0 2px rgba(255,255,255,0.08); }
|
||||||
|
.lg-banner strong { font-size: 13.5px; text-align: center; line-height: 1.2; }
|
||||||
|
.lg-banner-sub { font-size: 10.5px; color: #b6a7d4; }
|
||||||
|
.lg-banner-count { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #9fb2c8; }
|
||||||
|
.lg-banner-count i { width: 7px; height: 7px; border-radius: 50%; }
|
||||||
|
.lg-soldiers { display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; min-height: 30px; }
|
||||||
|
.lg-sol { display: flex; flex-direction: column; align-items: center; gap: 1px; width: 50px; padding: 5px 2px; border-radius: 9px; cursor: pointer; font-family: inherit; background: #11101e; border: 1px solid #2a2440; transition: transform 0.12s, border-color 0.12s; }
|
||||||
|
.lg-sol:hover { transform: translateY(-2px); border-color: #6a5a9a; }
|
||||||
|
.lg-sol-fig { font-size: 22px; line-height: 1; }
|
||||||
|
.lg-sol-id { font-size: 9.5px; color: #8a7bb0; }
|
||||||
|
.lg-sol-status { font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.4px; }
|
||||||
|
.lg-sol.alive { border-color: #1f5642; box-shadow: 0 0 10px rgba(54,240,176,0.12); }
|
||||||
|
.lg-sol.alive .lg-sol-status { color: #36f0b0; }
|
||||||
|
@keyframes solbob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
||||||
|
.lg-sol.alive .lg-sol-fig { animation: solbob 1.7s ease-in-out infinite; }
|
||||||
|
.lg-sol.pending .lg-sol-status { color: #f2b85f; }
|
||||||
|
.lg-sol.fallen { opacity: 0.5; }
|
||||||
|
.lg-sol.fallen .lg-sol-status { color: #ff7a8c; }
|
||||||
|
.lg-recruit { width: 50px; height: 56px; border-radius: 9px; border: 1px dashed #4a3f6e; background: #0d0b18; color: #6a5a9a; font-size: 22px; cursor: pointer; transition: border-color 0.12s, color 0.12s; }
|
||||||
|
.lg-recruit:hover { border-color: #46c8e0; color: #7fd9ea; }
|
||||||
|
/* soldier dialog */
|
||||||
|
.lg-sd-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||||
|
.lg-sd-meta { flex: 1; min-width: 120px; display: flex; flex-direction: column; gap: 1px; }
|
||||||
|
.lg-sd-meta strong { color: #eee6ff; font-size: 13px; }
|
||||||
|
.lg-sd-meta span { color: #8a7bb0; font-size: 11px; }
|
||||||
|
.lg-sd-key { font-size: 11px; }
|
||||||
|
.lg-standdown { background: #2a1620 !important; border: 1px solid #5a2030 !important; color: #ff9fb0 !important; }
|
||||||
|
.lg-sd-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
|
||||||
|
.lg-sd-tabs button { background: #11101e; border: 1px solid #2a2440; color: #b6a7d4; border-radius: 8px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
||||||
|
.lg-sd-tabs button.on { background: #2a2360; border-color: #6a5a9a; color: #fff; }
|
||||||
|
.lg-sd-status { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.lg-tui { background: #05070c; border: 1px solid #1c2438; border-radius: 9px; overflow: hidden; }
|
||||||
|
.lg-tui-head { padding: 5px 11px; background: #0d1320; color: #5b6b7d; font-size: 10px; letter-spacing: 0.6px; text-transform: uppercase; border-bottom: 1px solid #1c2438; }
|
||||||
|
.lg-tui pre { margin: 0; padding: 11px; font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: #9fe6c0; max-height: 240px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
.lg-rouse { margin-top: 2px; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user