browse: the roster door — mercenaries descend with bubble briefs (2.1.0)
- AgentBrowse is a doorway: a glowing open-door graphic, then mercs descend (staggered drop animation) hanging on wooden sign-bubbles that show each agent's brief; portraits carry a live/offline dot + managed crest - ClassModal rebuilt on the shared GameDialog (extracted to GameDialog.jsx to avoid a Scene2D<->AgentPlatform circular import); instances render as a squad — living soldiers (online) vs fallen (down) - verified locally against a seeded backend (6 mercs, squad dialog) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a350ae6884
commit
374440a392
@ -17,7 +17,7 @@
|
||||
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
||||
</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="version" content="2.0.0-tavern" />
|
||||
<meta name="version" content="2.1.0-roster" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amerc-site",
|
||||
"version": "2.0.0-tavern",
|
||||
"version": "2.1.0-roster",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from './api.js';
|
||||
import { copyToast } from './toast.js';
|
||||
import ChatPanel, { SessionLog } from './Chat.jsx';
|
||||
import { GameDialog } from './GameDialog.jsx';
|
||||
|
||||
const STATUS_COLOR = { online: '#36f0b0', pending: '#f2b85f', lost: '#ff9f5a', down: '#ff7a8c' };
|
||||
const TAG_EMOJI = { backend: '🛠️', model: '🧠', frontend: '🎨', ui: '🖌️', web: '🌐', data: '📊', devops: '⚙️', ml: '🤖', security: '🔐', api: '🔌', bot: '🤖', chat: '💬', test: '🧪', docs: '📄', design: '✏️', ops: '🚦', research: '🔬' };
|
||||
@ -77,14 +78,18 @@ export function AgentBrowse({ auth, setRoute }) {
|
||||
}, [classes]);
|
||||
|
||||
return (
|
||||
<div className="ap">
|
||||
<div className="ap-store-head">
|
||||
<div className="ap-store-intro">
|
||||
<h2>The mercenary roster</h2>
|
||||
<p>Hire a ready-made agent by skill and tag — chat with it, embed it on your site, or drive its terminal. Bring your own and it joins the roster.</p>
|
||||
<div className="ap apb">
|
||||
<div className="apb-doorway">
|
||||
<div className="apb-door" aria-hidden="true">
|
||||
<span className="apb-door-leaf l" /><span className="apb-door-leaf r" />
|
||||
<span className="apb-door-light" />
|
||||
</div>
|
||||
<div className="apb-doorhead">
|
||||
<h2>⚔ The mercenary roster</h2>
|
||||
<p>Open the door and the mercenaries come down. Hire one — chat, embed, or drive its terminal. {loaded ? <b>{classes.length} answering the call.</b> : 'Mustering…'}</p>
|
||||
<input className="ap-search" placeholder="search the roster…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ap-tags">
|
||||
<button className={`ap-tag${!tag ? ' on' : ''}`} onClick={() => setTag('')}>★ all</button>
|
||||
{Object.entries(tags).sort((a, b) => b[1] - a[1]).map(([t, n]) => (
|
||||
@ -92,38 +97,51 @@ export function AgentBrowse({ auth, setRoute }) {
|
||||
))}
|
||||
</div>
|
||||
{err && <p className="px-auth-err">{err}</p>}
|
||||
<div className="ap-grid">
|
||||
{!loaded && Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={`sk${i}`} className="ap-card ap-skel" aria-hidden="true">
|
||||
<div className="ap-skel-head"><span className="sk-av" /><span className="sk-tt"><i /><i /></span></div>
|
||||
<span className="sk-p" /><span className="sk-p sk-short" />
|
||||
<div className="ap-skel-foot"><i /><i /></div>
|
||||
<div className="apb-stage" key={tag || 'all'}>
|
||||
{!loaded && Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={`sk${i}`} className="apb-merc apb-skel" aria-hidden="true" style={{ '--d': `${i * 80}ms` }}>
|
||||
<span className="apb-bub sk-p" /><span className="apb-portrait sk-av" />
|
||||
</div>
|
||||
))}
|
||||
{loaded && classes.map((c) => <AgentCard key={c.id} c={c} onClick={() => openClass(c)} />)}
|
||||
{loaded && classes.map((c, i) => (
|
||||
<button key={c.id} className="apb-merc" style={{ '--d': `${Math.min(i, 14) * 75}ms` }} onClick={() => openClass(c)}>
|
||||
<span className="apb-bub">
|
||||
{c.description || 'A ready blade, awaiting orders.'}
|
||||
<i className="apb-bub-tail" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="apb-rope" aria-hidden="true" />
|
||||
<span className="apb-portrait" style={avatarStyle(c.name)}>
|
||||
<span className="apb-emoji">{classEmoji(c)}</span>
|
||||
<span className={`apb-life ${c.onlineCount ? 'on' : 'off'}`} title={c.onlineCount ? 'online' : 'offline'} />
|
||||
{!!c.managed && <span className="apb-crest" title="amerc-managed">🛡</span>}
|
||||
</span>
|
||||
<strong className="apb-name">{c.name}</strong>
|
||||
<span className="apb-meta"><span className="apb-tag-chip">{c.kind}</span> {c.onlineCount}/{c.instanceCount} online · by {c.owner}</span>
|
||||
</button>
|
||||
))}
|
||||
{loaded && classes.length > 0 && (
|
||||
<button className="ap-card ap-card-add" onClick={() => setRoute('legion')}>
|
||||
<span className="ap-add-plus" aria-hidden="true">+</span>
|
||||
<strong>Publish your own</strong>
|
||||
<p>Register an agent class and field it from your legion — your mercenary joins the roster right here.</p>
|
||||
<span className="ap-add-cta">Open My Legion →</span>
|
||||
<button className="apb-merc apb-merc-add" style={{ '--d': `${Math.min(classes.length, 14) * 75}ms` }} onClick={() => setRoute('legion')}>
|
||||
<span className="apb-bub">Bring your own blade — it joins the roster the moment your broker checks in.<i className="apb-bub-tail" aria-hidden="true" /></span>
|
||||
<span className="apb-rope" aria-hidden="true" />
|
||||
<span className="apb-portrait apb-portrait-add">+</span>
|
||||
<strong className="apb-name">Publish your own</strong>
|
||||
<span className="apb-meta">Open My Legion →</span>
|
||||
</button>
|
||||
)}
|
||||
{loaded && !classes.length && (q || tag) && (
|
||||
<div className="ap-empty-search">
|
||||
<span className="ap-empty-search-ico" aria-hidden="true">🔍</span>
|
||||
<h3>No mercenaries match {q ? `“${q}”` : `#${tag}`}.</h3>
|
||||
<p>Try a different term or clear the filter.</p>
|
||||
<h3>No mercenaries answer {q ? `“${q}”` : `#${tag}`}.</h3>
|
||||
<button className="px-action" onClick={() => { setQ(''); setTag(''); }}>Clear search</button>
|
||||
</div>
|
||||
)}
|
||||
{loaded && !classes.length && !q && !tag && (
|
||||
<div className="ap-empty-start">
|
||||
<h3>No agents here yet — be the first.</h3>
|
||||
<h3>The hall is empty — be the first blade.</h3>
|
||||
<ol>
|
||||
<li><b>Publish a class</b> in <button className="ap-link" onClick={() => setRoute('legion')}>My Legion</button> — name it, tag it (e.g. <code>backend</code>, <code>model</code>).</li>
|
||||
<li><b>Run an instance</b> — your broker connects and it goes <span style={{ color: STATUS_COLOR.online }}>online</span>.</li>
|
||||
<li><b>Use it</b> — embed it as a chatbox or drive it through a web terminal.</li>
|
||||
<li><b>Publish a class</b> in <button className="ap-link" onClick={() => setRoute('legion')}>My Legion</button> — name it, tag it.</li>
|
||||
<li><b>Field an instance</b> — your broker connects and it goes <span style={{ color: STATUS_COLOR.online }}>online</span>.</li>
|
||||
<li><b>Use it</b> — chat, embed, or drive its terminal.</li>
|
||||
</ol>
|
||||
<button className="px-action" onClick={() => setRoute('legion')}>Open My Legion →</button>
|
||||
</div>
|
||||
@ -146,17 +164,15 @@ function ClassModal({ id, auth, onClose }) {
|
||||
if (!data) return null;
|
||||
const c = data.class;
|
||||
return (
|
||||
<div className="ap-modal" onClick={onClose}>
|
||||
<div className="ap-modal-box" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="ap-modal-head">
|
||||
<div className="ap-modal-title">
|
||||
<span className="ap-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
||||
<div><h3>{c.name}</h3><span className="ap-kind">{c.kind}</span> <span className="ap-by">by {c.owner}</span></div>
|
||||
</div>
|
||||
<button onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<p className="ap-desc">{c.description}</p>
|
||||
<GameDialog title={c.name} tag={`⚔ ${c.kind}`} wide onClose={onClose}>
|
||||
<div className="cm-top">
|
||||
<span className="ap-avatar cm-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
||||
<div className="cm-top-body">
|
||||
<span className="ap-by">by {c.owner}</span>
|
||||
<p className="ap-desc">{c.description || 'No description.'}</p>
|
||||
<div className="ap-card-tags">{c.tags.map((t) => <span key={t}>{t}</span>)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ap-props">
|
||||
{!!c.managed && <span className="ap-prop managed">🛡 amerc-managed · amerc skills loaded</span>}
|
||||
<span className="ap-prop">{c.terminal_mode === 'shared' ? '🖥 one shared terminal across sessions' : '🖥 fresh terminal per session'}</span>
|
||||
@ -167,21 +183,22 @@ function ClassModal({ id, auth, onClose }) {
|
||||
{auth.user ? <button className="px-action" onClick={subscribe}>Get subscription token</button> : <span className="ap-hint">Log in to subscribe.</span>}
|
||||
{sub && <code className="ap-token" onClick={() => copyToast(sub)}>{sub} ⧉</code>}
|
||||
</div>
|
||||
<h4 className="ap-h4">Instances</h4>
|
||||
<div className="ap-inst-list">
|
||||
<h4 className="ap-h4">🛡 The squad — {data.instances.length} {data.instances.length === 1 ? 'soldier' : 'soldiers'}</h4>
|
||||
<div className="cm-squad">
|
||||
{data.instances.map((i) => (
|
||||
<div key={i.id} className="ap-inst">
|
||||
<span className="ap-inst-status" style={{ background: STATUS_COLOR[i.status] }} title={i.status} />
|
||||
<span className="ap-inst-name">#{i.id} {i.name}</span>
|
||||
<span className="ap-inst-meta">{i.status}{i.broker ? ` · ${i.broker}` : ''} · {i.visibility}</span>
|
||||
<button className="px-action" disabled={i.status !== 'online'} onClick={() => setUse(i)}>Use ▸</button>
|
||||
<div key={i.id} className={`cm-sol ${i.status === 'online' ? 'alive' : 'fallen'}`}>
|
||||
<span className="cm-sol-ico" aria-hidden="true">{i.status === 'online' ? '🪖' : '💀'}</span>
|
||||
<div className="cm-sol-body">
|
||||
<strong>#{i.id} {i.name}</strong>
|
||||
<span className="cm-sol-meta"><span className="cm-sol-dot" style={{ background: STATUS_COLOR[i.status] }} />{i.status}{i.broker ? ` · ${i.broker}` : ''} · {i.visibility}</span>
|
||||
</div>
|
||||
<button className="px-action" disabled={i.status !== 'online'} onClick={() => setUse(i)}>{i.status === 'online' ? 'Chat ▸' : 'down'}</button>
|
||||
</div>
|
||||
))}
|
||||
{!data.instances.length && <p className="ap-hint">No instances published. The owner can field one in My Legion.</p>}
|
||||
{!data.instances.length && <p className="ap-hint">No soldiers fielded yet. The owner can field one in My Legion.</p>}
|
||||
</div>
|
||||
{use && <UsePanel instance={use} onClose={() => setUse(null)} />}
|
||||
</div>
|
||||
</div>
|
||||
</GameDialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
27
src/GameDialog.jsx
Normal file
27
src/GameDialog.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
// Ornate, gem-cornered in-game dialog. Every "detail view" in amerc opens in
|
||||
// one of these — shared by the tavern, the roster, the legion and the mansion.
|
||||
export function GameDialog({ title, tag, onClose, children, wide }) {
|
||||
useEffect(() => {
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
return (
|
||||
<div className="gd-backdrop" onClick={onClose}>
|
||||
<div className={`gd${wide ? ' gd-wide' : ''}`} onClick={(e) => e.stopPropagation()} role="dialog" aria-label={title}>
|
||||
<span className="gd-gem tl" aria-hidden="true" /><span className="gd-gem tr" aria-hidden="true" />
|
||||
<span className="gd-gem bl" aria-hidden="true" /><span className="gd-gem br" aria-hidden="true" />
|
||||
<header className="gd-head">
|
||||
{tag && <span className="gd-tag">{tag}</span>}
|
||||
<h3>{title}</h3>
|
||||
<button className="gd-x" onClick={onClose} type="button" aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div className="gd-body">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GameDialog;
|
||||
@ -4,13 +4,14 @@ import { AgentBrowse, LegionDashboard } from './AgentPlatform.jsx';
|
||||
import Mansion from './Mansion.jsx';
|
||||
import Account from './Account.jsx';
|
||||
import TavernGame from './TavernGame.jsx';
|
||||
import { GameDialog } from './GameDialog.jsx';
|
||||
import { api } from './api.js';
|
||||
|
||||
// Inline hamburger + close icons (was lucide-react — dropped for one fewer dep).
|
||||
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 RELEASE = '2.0.0-tavern';
|
||||
const RELEASE = '2.1.0-roster';
|
||||
|
||||
const NAV = [
|
||||
{ id: 'home', label: 'Tavern' },
|
||||
@ -108,29 +109,6 @@ function Reveal({ children, className = '', delay = 0, tag: Tag = 'div' }) {
|
||||
return <Tag ref={ref} className={`${className} ${armed ? 'reveal' : ''} ${shown ? 'in' : ''}`.replace(/\s+/g, ' ').trim()} style={delay && armed ? { transitionDelay: `${delay}ms` } : undefined}>{children}</Tag>;
|
||||
}
|
||||
|
||||
// Ornate in-game dialog — every "detail view" opens in one of these.
|
||||
export function GameDialog({ title, tag, onClose, children, wide }) {
|
||||
useEffect(() => {
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
return (
|
||||
<div className="gd-backdrop" onClick={onClose}>
|
||||
<div className={`gd${wide ? ' gd-wide' : ''}`} onClick={(e) => e.stopPropagation()} role="dialog" aria-label={title}>
|
||||
<span className="gd-gem tl" aria-hidden="true" /><span className="gd-gem tr" aria-hidden="true" />
|
||||
<span className="gd-gem bl" aria-hidden="true" /><span className="gd-gem br" aria-hidden="true" />
|
||||
<header className="gd-head">
|
||||
{tag && <span className="gd-tag">{tag}</span>}
|
||||
<h3>{title}</h3>
|
||||
<button className="gd-x" onClick={onClose} type="button" aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div className="gd-body">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Home({ setRoute, auth }) {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [dlg, setDlg] = useState(null); // 'tour' | 'login' | null
|
||||
@ -498,6 +476,7 @@ function StatusPage() {
|
||||
}
|
||||
|
||||
const CHANGELOG = [
|
||||
{ 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: '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.2', date: 'Jun 2026', title: 'Legion fields agent cards', notes: ['Your classes in My Legion are now the same agent cards as Browse Agents — open one to see and use its instances right there', 'New class properties at publish time: amerc-managed (loads amerc skills), terminal-per-session vs shared terminal, and skills-reload on shared terminals'] },
|
||||
|
||||
@ -1712,3 +1712,56 @@ button {
|
||||
.gm-stage3 { padding: 4px; }
|
||||
.gm-tagline { font-size: 19px; }
|
||||
}
|
||||
|
||||
/* ===== Browse = the roster door; mercenaries descend (2.1) ===== */
|
||||
.apb { max-width: 1080px; margin: 0 auto; }
|
||||
.apb-doorway { display: flex; align-items: center; gap: 20px; padding: 14px 4px 18px; }
|
||||
.apb-door { position: relative; flex: 0 0 auto; width: 96px; height: 116px; border-radius: 48px 48px 8px 8px; background: #20140a; border: 3px solid #5a3a1c; overflow: hidden; box-shadow: inset 0 0 18px rgba(0,0,0,0.7); perspective: 200px; }
|
||||
.apb-door-light { position: absolute; inset: 4px; border-radius: 44px 44px 6px 6px; background: radial-gradient(ellipse at 50% 40%, rgba(127,233,255,0.85), rgba(40,120,200,0.35) 55%, transparent 75%); animation: apbGlow 3s ease-in-out infinite; }
|
||||
@keyframes apbGlow { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
|
||||
.apb-door-leaf { position: absolute; top: 0; width: 50%; height: 100%; background: linear-gradient(180deg, #6b4326, #3a2414); transform-origin: var(--o) center; box-shadow: inset 0 0 8px rgba(0,0,0,0.6); }
|
||||
.apb-door-leaf.l { left: 0; --o: left; transform: rotateY(58deg); border-right: 2px solid #2a1810; border-radius: 46px 0 0 6px; }
|
||||
.apb-door-leaf.r { right: 0; --o: right; transform: rotateY(-58deg); border-left: 2px solid #2a1810; border-radius: 0 46px 6px 0; }
|
||||
.apb-doorhead { flex: 1; min-width: 0; }
|
||||
.apb-doorhead h2 { margin: 0 0 4px; font-family: Georgia, serif; font-size: 24px; color: #f3e9d2; }
|
||||
.apb-doorhead p { margin: 0 0 10px; color: #9b8fb8; font-size: 13px; }
|
||||
.apb-doorhead .ap-search { width: min(360px, 100%); }
|
||||
|
||||
.apb-stage { display: flex; flex-wrap: wrap; gap: 30px 22px; padding: 30px 6px 24px; align-items: flex-start; }
|
||||
@keyframes apbDrop { from { transform: translateY(-46px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
.apb-merc { position: relative; width: 156px; display: flex; flex-direction: column; align-items: center; gap: 6px; background: none; border: none; cursor: pointer; font-family: inherit; padding: 0; animation: apbDrop 0.55s cubic-bezier(0.2, 1.3, 0.4, 1) backwards; animation-delay: var(--d, 0ms); }
|
||||
.apb-rope { position: absolute; top: -30px; left: 50%; width: 2px; height: 30px; background: linear-gradient(180deg, transparent, #6b5a3a); transform: translateX(-50%); }
|
||||
.apb-bub { position: relative; background: linear-gradient(180deg, #241509, #170d05); border: 2px solid #8a5a32; color: #f3e3c2; border-radius: 11px; padding: 8px 11px; font-size: 11.5px; line-height: 1.4; text-align: left; min-height: 38px; display: flex; align-items: center; box-shadow: 0 3px 0 rgba(0,0,0,0.4); width: 100%; box-sizing: border-box; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
||||
.apb-bub-tail { position: absolute; left: 50%; bottom: -7px; width: 11px; height: 11px; background: #170d05; border-right: 2px solid #8a5a32; border-bottom: 2px solid #8a5a32; transform: translateX(-50%) rotate(45deg); }
|
||||
.apb-portrait { position: relative; width: 72px; height: 72px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 30px; box-shadow: 0 0 0 3px #2a2440, 0 8px 18px rgba(0,0,0,0.5); transition: transform 0.15s; }
|
||||
.apb-merc:hover .apb-portrait { transform: scale(1.07) translateY(-2px); }
|
||||
.apb-merc:hover .apb-bub { border-color: #ffd75e; }
|
||||
.apb-emoji { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
|
||||
.apb-life { position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px; border-radius: 50%; border: 3px solid #0c0918; }
|
||||
.apb-life.on { background: #36f0b0; box-shadow: 0 0 9px #36f0b0; animation: chpulse 2s infinite; }
|
||||
.apb-life.off { background: #54607a; }
|
||||
.apb-crest { position: absolute; left: -4px; top: -4px; font-size: 15px; filter: drop-shadow(0 0 4px rgba(54,240,176,0.6)); }
|
||||
.apb-name { color: #eee6ff; font-size: 13.5px; text-align: center; }
|
||||
.apb-meta { color: #8a7bb0; font-size: 10.5px; text-align: center; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; justify-content: center; }
|
||||
.apb-tag-chip { background: #1a2740; border: 1px solid #2a3a5a; color: #9fb2c8; border-radius: 999px; padding: 1px 7px; font-size: 9.5px; }
|
||||
.apb-portrait-add { background: #141022; border: 2px dashed #4a3f6e; color: #8a7bb0; font-size: 28px; box-shadow: none; }
|
||||
.apb-merc-add .apb-name { color: #b6a7d4; }
|
||||
.apb-skel { pointer-events: none; }
|
||||
.apb-skel .apb-bub { border-color: #243049; }
|
||||
|
||||
/* ClassModal as a war-banner dialog: squad of soldiers */
|
||||
.cm-top { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
|
||||
.cm-avatar { width: 64px; height: 64px; font-size: 30px; flex: 0 0 auto; }
|
||||
.cm-top-body { min-width: 0; }
|
||||
.cm-top-body .ap-desc { margin: 6px 0 8px; }
|
||||
.cm-squad { display: flex; flex-direction: column; gap: 8px; }
|
||||
.cm-sol { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 10px; border: 1px solid #243049; background: #0b0f1a; }
|
||||
.cm-sol.alive { border-color: #1f5642; box-shadow: inset 0 0 16px rgba(54,240,176,0.06); }
|
||||
.cm-sol.fallen { opacity: 0.62; }
|
||||
.cm-sol-ico { font-size: 24px; filter: grayscale(0); }
|
||||
.cm-sol.fallen .cm-sol-ico { filter: grayscale(0.5); }
|
||||
.cm-sol-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.cm-sol-body strong { color: #eee6ff; font-size: 13px; }
|
||||
.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; }
|
||||
@media (max-width: 620px) { .apb-merc { width: 132px; } .apb-doorway { flex-wrap: wrap; } }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user