Spaces:
Running
Running
Personas → heroes: shield icon, hero terminology, empty state, active highlight
Browse files- Nav icon for Personas: 🪖 (contemporary helmet) → 🛡 (fantasy shield).
- "Recruit a soldier" → "Recruit hero"; all "soldier" copy → "hero" ("No heroes saved
yet", prompts, store defaults, diary).
- Persona page hides the hero fields until a hero is generated or picked (empty state
"Recruit a hero, or pick one from the barracks").
- The selected hero is highlighted in the barracks roster (ink fill + red accent bar).
Verified: icon, empty/shown toggle, active highlight, terminology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- web/diaryPanel.js +2 -2
- web/personaPanel.js +21 -12
- web/personaPrompts.js +5 -5
- web/personaStore.js +2 -2
- web/shell/nav.json +1 -1
- web/shell/persona.css +12 -1
web/diaryPanel.js
CHANGED
|
@@ -80,7 +80,7 @@ export function mountDiaryPanel(host) {
|
|
| 80 |
// Ensure the TTS model is loaded (showing progress), then return a fresh narrator.
|
| 81 |
async function makeReadyNarrator() {
|
| 82 |
// For Qwen3-TTS's "persona voice", design a voice from this unit's traits.
|
| 83 |
-
setVoiceDescription(`A war-weary
|
| 84 |
ttsStatus.textContent = 'loading voice…'
|
| 85 |
await ensureTts((frac) => { ttsStatus.textContent = `downloading voice… ${Math.round(frac * 100)}% (one-time)` })
|
| 86 |
ttsStatus.textContent = 'reading on your device…'
|
|
@@ -116,7 +116,7 @@ export function mountDiaryPanel(host) {
|
|
| 116 |
busy = true; btn.disabled = true; stats.textContent = ''
|
| 117 |
if (window.innerWidth <= 768) result.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
| 118 |
stopNarration()
|
| 119 |
-
const header = `— Diary of ${(unit.value || 'a nameless
|
| 120 |
out.textContent = header
|
| 121 |
lastBody = ''
|
| 122 |
|
|
|
|
| 80 |
// Ensure the TTS model is loaded (showing progress), then return a fresh narrator.
|
| 81 |
async function makeReadyNarrator() {
|
| 82 |
// For Qwen3-TTS's "persona voice", design a voice from this unit's traits.
|
| 83 |
+
setVoiceDescription(`A war-weary hero's voice — ${(traits.value || 'battle-hardened').trim()}.`)
|
| 84 |
ttsStatus.textContent = 'loading voice…'
|
| 85 |
await ensureTts((frac) => { ttsStatus.textContent = `downloading voice… ${Math.round(frac * 100)}% (one-time)` })
|
| 86 |
ttsStatus.textContent = 'reading on your device…'
|
|
|
|
| 116 |
busy = true; btn.disabled = true; stats.textContent = ''
|
| 117 |
if (window.innerWidth <= 768) result.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
| 118 |
stopNarration()
|
| 119 |
+
const header = `— Diary of ${(unit.value || 'a nameless hero').trim()} —\n\n`
|
| 120 |
out.textContent = header
|
| 121 |
lastBody = ''
|
| 122 |
|
web/personaPanel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
// Tiny Army persona panel — mounted by tiny.js into #persona-stage. Recruits a
|
| 2 |
// (name/about/traits + a voice design + a spoken quote), lets you CREATE a voice file of
|
| 3 |
// them saying their quote (Qwen3-TTS) and REPLAY it, edit every field inline (auto-saved),
|
| 4 |
// and keeps everyone in a local-first barracks roster (personaStore) so they persist for
|
|
@@ -29,7 +29,7 @@ export function mountPersonaPanel(host) {
|
|
| 29 |
const seed = el('input', { class: 'persona-input', type: 'text', placeholder: 'a word, a vibe… (optional)' })
|
| 30 |
const stats = el('div', { class: 'persona-stats' })
|
| 31 |
const status = el('div', { class: 'persona-status' }, 'Runs on your device — no cloud.')
|
| 32 |
-
const btn = el('button', { class: 'persona-go', type: 'button' }, '⚔ Recruit
|
| 33 |
const rosterEl = el('div', { class: 'persona-roster' })
|
| 34 |
|
| 35 |
const nameEl = el('div', { class: 'persona-name persona-edit', 'data-ph': 'Name' })
|
|
@@ -56,22 +56,32 @@ export function mountPersonaPanel(host) {
|
|
| 56 |
btn, stats, status,
|
| 57 |
el('label', { class: 'persona-label persona-roster-label' }, 'Barracks (saved)'), rosterEl,
|
| 58 |
])
|
| 59 |
-
const
|
|
|
|
| 60 |
nameEl, tagsEl,
|
| 61 |
secHead('About'), aboutEl,
|
| 62 |
secHead('Quote', playBtn), quoteEl,
|
| 63 |
secHead('Voice design'), voiceEl,
|
| 64 |
-
thinkWrap,
|
| 65 |
])
|
|
|
|
| 66 |
host.appendChild(el('div', { class: 'persona-view' }, [controls, result]))
|
| 67 |
|
| 68 |
let lastPersona = null // the persona currently shown
|
| 69 |
let savedId = null // its roster id (set the moment it's shown — always saved)
|
| 70 |
let hasVoice = false // a cached voice file exists for this persona
|
| 71 |
let working = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
// The line the voice actually says (quote, else about, else a fallback).
|
| 74 |
-
const lineFor = (p) => (p.quote || '').trim() || (p.about || '').trim() || `${p.name || 'A
|
| 75 |
// Cached audio is stale if the line or the voice design changed since it was made.
|
| 76 |
const isDirty = () => hasVoice && lastPersona && (lineFor(lastPersona) !== lastPersona.voiceQuote || (lastPersona.voice || '') !== (lastPersona.voiceDesignUsed || ''))
|
| 77 |
// Badge when there's a persona but no current voice (none yet, or it went stale).
|
|
@@ -121,7 +131,7 @@ export function mountPersonaPanel(host) {
|
|
| 121 |
quoteEl.textContent = p.quote || ''
|
| 122 |
voiceEl.textContent = p.voice || ''
|
| 123 |
hasVoice = savedId ? !!(await getAudio(savedId)) : false
|
| 124 |
-
updateVoiceUI()
|
| 125 |
}
|
| 126 |
|
| 127 |
// ▶ The one voice button: if the cached voice is current, just replay it. If there's
|
|
@@ -161,13 +171,13 @@ export function mountPersonaPanel(host) {
|
|
| 161 |
}
|
| 162 |
playBtn.addEventListener('click', play)
|
| 163 |
|
| 164 |
-
// ── Barracks roster (saved
|
| 165 |
function renderRoster(personas) {
|
| 166 |
-
if (!personas.length) { rosterEl.replaceChildren(el('div', { class: 'persona-roster-empty' }, 'No
|
| 167 |
rosterEl.replaceChildren(...personas.map((p) =>
|
| 168 |
el('div', { class: 'persona-roster-item' + (p.id === savedId ? ' active' : '') }, [
|
| 169 |
el('button', { class: 'persona-roster-name', type: 'button', title: 'View', onclick: () => showPersona(p, { savedId: p.id }).then(() => renderRoster(listPersonas())) },
|
| 170 |
-
`${p.name || '
|
| 171 |
el('button', { class: 'persona-roster-x', type: 'button', title: 'Remove', onclick: () => removePersona(p.id) }, '🗑'),
|
| 172 |
])))
|
| 173 |
}
|
|
@@ -200,10 +210,9 @@ export function mountPersonaPanel(host) {
|
|
| 200 |
}
|
| 201 |
})
|
| 202 |
|
| 203 |
-
let busy = false
|
| 204 |
async function generate() {
|
| 205 |
if (busy) return
|
| 206 |
-
busy = true; btn.disabled = true
|
| 207 |
if (window.innerWidth <= 768) result.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
| 208 |
nameEl.textContent = '…'; aboutEl.textContent = ''; tagsEl.replaceChildren()
|
| 209 |
quoteEl.textContent = ''; voiceEl.textContent = ''
|
|
@@ -240,7 +249,7 @@ export function mountPersonaPanel(host) {
|
|
| 240 |
} catch (e) {
|
| 241 |
status.textContent = `couldn't run the local model: ${e.message || e} · 📋 Copy debug`
|
| 242 |
lastDebug = buildDebug('EXCEPTION: ' + (e.message || e) + (e.stack ? '\n' + e.stack : ''), acc); thinkWrap.open = true
|
| 243 |
-
} finally { busy = false; btn.disabled = false }
|
| 244 |
}
|
| 245 |
btn.addEventListener('click', generate)
|
| 246 |
}
|
|
|
|
| 1 |
+
// Tiny Army persona panel — mounted by tiny.js into #persona-stage. Recruits a hero
|
| 2 |
// (name/about/traits + a voice design + a spoken quote), lets you CREATE a voice file of
|
| 3 |
// them saying their quote (Qwen3-TTS) and REPLAY it, edit every field inline (auto-saved),
|
| 4 |
// and keeps everyone in a local-first barracks roster (personaStore) so they persist for
|
|
|
|
| 29 |
const seed = el('input', { class: 'persona-input', type: 'text', placeholder: 'a word, a vibe… (optional)' })
|
| 30 |
const stats = el('div', { class: 'persona-stats' })
|
| 31 |
const status = el('div', { class: 'persona-status' }, 'Runs on your device — no cloud.')
|
| 32 |
+
const btn = el('button', { class: 'persona-go', type: 'button' }, '⚔ Recruit hero')
|
| 33 |
const rosterEl = el('div', { class: 'persona-roster' })
|
| 34 |
|
| 35 |
const nameEl = el('div', { class: 'persona-name persona-edit', 'data-ph': 'Name' })
|
|
|
|
| 56 |
btn, stats, status,
|
| 57 |
el('label', { class: 'persona-label persona-roster-label' }, 'Barracks (saved)'), rosterEl,
|
| 58 |
])
|
| 59 |
+
const emptyEl = el('div', { class: 'persona-empty' }, 'Recruit a hero, or pick one from the barracks.')
|
| 60 |
+
const bodyEl = el('div', { class: 'persona-body' }, [
|
| 61 |
nameEl, tagsEl,
|
| 62 |
secHead('About'), aboutEl,
|
| 63 |
secHead('Quote', playBtn), quoteEl,
|
| 64 |
secHead('Voice design'), voiceEl,
|
|
|
|
| 65 |
])
|
| 66 |
+
const result = el('div', { class: 'persona-result' }, [emptyEl, bodyEl, thinkWrap])
|
| 67 |
host.appendChild(el('div', { class: 'persona-view' }, [controls, result]))
|
| 68 |
|
| 69 |
let lastPersona = null // the persona currently shown
|
| 70 |
let savedId = null // its roster id (set the moment it's shown — always saved)
|
| 71 |
let hasVoice = false // a cached voice file exists for this persona
|
| 72 |
let working = false
|
| 73 |
+
let busy = false
|
| 74 |
+
|
| 75 |
+
// Hide the hero fields until a hero is generated or picked from the barracks.
|
| 76 |
+
function refreshVisibility() {
|
| 77 |
+
const show = !!lastPersona || busy
|
| 78 |
+
bodyEl.style.display = show ? '' : 'none'
|
| 79 |
+
emptyEl.style.display = show ? 'none' : ''
|
| 80 |
+
}
|
| 81 |
+
refreshVisibility()
|
| 82 |
|
| 83 |
// The line the voice actually says (quote, else about, else a fallback).
|
| 84 |
+
const lineFor = (p) => (p.quote || '').trim() || (p.about || '').trim() || `${p.name || 'A hero'} reporting for duty.`
|
| 85 |
// Cached audio is stale if the line or the voice design changed since it was made.
|
| 86 |
const isDirty = () => hasVoice && lastPersona && (lineFor(lastPersona) !== lastPersona.voiceQuote || (lastPersona.voice || '') !== (lastPersona.voiceDesignUsed || ''))
|
| 87 |
// Badge when there's a persona but no current voice (none yet, or it went stale).
|
|
|
|
| 131 |
quoteEl.textContent = p.quote || ''
|
| 132 |
voiceEl.textContent = p.voice || ''
|
| 133 |
hasVoice = savedId ? !!(await getAudio(savedId)) : false
|
| 134 |
+
updateVoiceUI(); refreshVisibility()
|
| 135 |
}
|
| 136 |
|
| 137 |
// ▶ The one voice button: if the cached voice is current, just replay it. If there's
|
|
|
|
| 171 |
}
|
| 172 |
playBtn.addEventListener('click', play)
|
| 173 |
|
| 174 |
+
// ── Barracks roster (saved heroes) ──────────────────────────────────────
|
| 175 |
function renderRoster(personas) {
|
| 176 |
+
if (!personas.length) { rosterEl.replaceChildren(el('div', { class: 'persona-roster-empty' }, 'No heroes saved yet.')); return }
|
| 177 |
rosterEl.replaceChildren(...personas.map((p) =>
|
| 178 |
el('div', { class: 'persona-roster-item' + (p.id === savedId ? ' active' : '') }, [
|
| 179 |
el('button', { class: 'persona-roster-name', type: 'button', title: 'View', onclick: () => showPersona(p, { savedId: p.id }).then(() => renderRoster(listPersonas())) },
|
| 180 |
+
`${p.name || 'Hero'}${p.unitClass ? ` · ${p.unitClass}` : ''}`),
|
| 181 |
el('button', { class: 'persona-roster-x', type: 'button', title: 'Remove', onclick: () => removePersona(p.id) }, '🗑'),
|
| 182 |
])))
|
| 183 |
}
|
|
|
|
| 210 |
}
|
| 211 |
})
|
| 212 |
|
|
|
|
| 213 |
async function generate() {
|
| 214 |
if (busy) return
|
| 215 |
+
busy = true; btn.disabled = true; refreshVisibility()
|
| 216 |
if (window.innerWidth <= 768) result.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
| 217 |
nameEl.textContent = '…'; aboutEl.textContent = ''; tagsEl.replaceChildren()
|
| 218 |
quoteEl.textContent = ''; voiceEl.textContent = ''
|
|
|
|
| 249 |
} catch (e) {
|
| 250 |
status.textContent = `couldn't run the local model: ${e.message || e} · 📋 Copy debug`
|
| 251 |
lastDebug = buildDebug('EXCEPTION: ' + (e.message || e) + (e.stack ? '\n' + e.stack : ''), acc); thinkWrap.open = true
|
| 252 |
+
} finally { busy = false; btn.disabled = false; refreshVisibility() }
|
| 253 |
}
|
| 254 |
btn.addEventListener('click', generate)
|
| 255 |
}
|
web/personaPrompts.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
| 2 |
// in-browser path). War-legend tone, not woid's.
|
| 3 |
|
| 4 |
export const PERSONA_SYSTEM =
|
| 5 |
-
'You invent tiny
|
| 6 |
'fighter writes its own legend. Given a class and an optional seed, return ONE JSON ' +
|
| 7 |
'object and NOTHING else, with exactly these keys:\n' +
|
| 8 |
-
' "name": a short evocative
|
| 9 |
' "about": 1-2 short sentences of backstory (about 25 words) in a heroic, wry war-legend tone,\n' +
|
| 10 |
' "specialty": a 1-3 word combat specialty,\n' +
|
| 11 |
' "personality": a 1-3 word personality tag,\n' +
|
|
@@ -18,18 +18,18 @@ export const PERSONA_SYSTEM =
|
|
| 18 |
'Output strictly valid JSON. No preamble, no code fences, no commentary.'
|
| 19 |
|
| 20 |
export const DIARY_SYSTEM =
|
| 21 |
-
'You are a tiny
|
| 22 |
'war-diary entry. Given your name and traits, write just 1-2 vivid sentences (about ' +
|
| 23 |
'60 words, no more) in first person about a day on the battlefield — heroic, grounded, ' +
|
| 24 |
'a touch of dark humor. Prose only: no headings, no lists, no preamble. Be brief.'
|
| 25 |
|
| 26 |
export function personaUserPrompt(unitClass = '', seed = '') {
|
| 27 |
const s = seed && seed.trim() ? ` Seed inspiration: "${seed.trim()}".` : ''
|
| 28 |
-
return `Class: ${(unitClass || '
|
| 29 |
}
|
| 30 |
|
| 31 |
export function diaryUserPrompt(unit = '', traits = '') {
|
| 32 |
-
const u = (unit || 'a nameless
|
| 33 |
const t = (traits || 'untested').trim()
|
| 34 |
return `Name: ${u}. Traits: ${t}. Write the diary entry.`
|
| 35 |
}
|
|
|
|
| 2 |
// in-browser path). War-legend tone, not woid's.
|
| 3 |
|
| 4 |
export const PERSONA_SYSTEM =
|
| 5 |
+
'You invent tiny heroes for a fantasy auto-battler called Tiny Army, where every ' +
|
| 6 |
'fighter writes its own legend. Given a class and an optional seed, return ONE JSON ' +
|
| 7 |
'object and NOTHING else, with exactly these keys:\n' +
|
| 8 |
+
' "name": a short evocative hero name (2-4 words),\n' +
|
| 9 |
' "about": 1-2 short sentences of backstory (about 25 words) in a heroic, wry war-legend tone,\n' +
|
| 10 |
' "specialty": a 1-3 word combat specialty,\n' +
|
| 11 |
' "personality": a 1-3 word personality tag,\n' +
|
|
|
|
| 18 |
'Output strictly valid JSON. No preamble, no code fences, no commentary.'
|
| 19 |
|
| 20 |
export const DIARY_SYSTEM =
|
| 21 |
+
'You are a tiny hero in the auto-battler Tiny Army, writing a short first-person ' +
|
| 22 |
'war-diary entry. Given your name and traits, write just 1-2 vivid sentences (about ' +
|
| 23 |
'60 words, no more) in first person about a day on the battlefield — heroic, grounded, ' +
|
| 24 |
'a touch of dark humor. Prose only: no headings, no lists, no preamble. Be brief.'
|
| 25 |
|
| 26 |
export function personaUserPrompt(unitClass = '', seed = '') {
|
| 27 |
const s = seed && seed.trim() ? ` Seed inspiration: "${seed.trim()}".` : ''
|
| 28 |
+
return `Class: ${(unitClass || 'hero').trim()}.${s} Return the JSON object now.`
|
| 29 |
}
|
| 30 |
|
| 31 |
export function diaryUserPrompt(unit = '', traits = '') {
|
| 32 |
+
const u = (unit || 'a nameless hero').trim()
|
| 33 |
const t = (traits || 'untested').trim()
|
| 34 |
return `Name: ${u}. Traits: ${t}. Write the diary entry.`
|
| 35 |
}
|
web/personaStore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
// Local-first roster of saved
|
| 2 |
// store: a single JSON blob in localStorage, simple CRUD, change listeners, and a
|
| 3 |
// pluggable `sync` hook so a backend can push/pull later WITHOUT changing callers.
|
| 4 |
// Voice is stored as the design TEXT + quote (re-synthesized on replay) — the audio
|
|
@@ -33,7 +33,7 @@ export function savePersona(p) {
|
|
| 33 |
const id = p.id || newId()
|
| 34 |
const rec = {
|
| 35 |
id,
|
| 36 |
-
name: p.name || 'Unnamed
|
| 37 |
unitClass: p.unitClass || '',
|
| 38 |
about: p.about || '',
|
| 39 |
quote: p.quote || '',
|
|
|
|
| 1 |
+
// Local-first roster of saved heroes (personas-as-agents). Modeled on woid's Shelter
|
| 2 |
// store: a single JSON blob in localStorage, simple CRUD, change listeners, and a
|
| 3 |
// pluggable `sync` hook so a backend can push/pull later WITHOUT changing callers.
|
| 4 |
// Voice is stored as the design TEXT + quote (re-synthesized on replay) — the audio
|
|
|
|
| 33 |
const id = p.id || newId()
|
| 34 |
const rec = {
|
| 35 |
id,
|
| 36 |
+
name: p.name || 'Unnamed hero',
|
| 37 |
unitClass: p.unitClass || '',
|
| 38 |
about: p.about || '',
|
| 39 |
quote: p.quote || '',
|
web/shell/nav.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
| 25 |
"title": "Barracks",
|
| 26 |
"items": [
|
| 27 |
{ "label": "War Diaries", "icon": "📓", "space": "Barracks" },
|
| 28 |
-
{ "label": "Personas", "icon": "
|
| 29 |
]
|
| 30 |
},
|
| 31 |
{
|
|
|
|
| 25 |
"title": "Barracks",
|
| 26 |
"items": [
|
| 27 |
{ "label": "War Diaries", "icon": "📓", "space": "Barracks" },
|
| 28 |
+
{ "label": "Personas", "icon": "🛡", "space": "Personas" }
|
| 29 |
]
|
| 30 |
},
|
| 31 |
{
|
web/shell/persona.css
CHANGED
|
@@ -109,7 +109,13 @@
|
|
| 109 |
.persona-edit:focus { background: var(--p-card); box-shadow: 0 0 0 1.5px var(--p-transmit); }
|
| 110 |
.persona-edit:empty::before { content: attr(data-ph); color: var(--p-muted); opacity: .6; font-style: italic; }
|
| 111 |
|
| 112 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
.persona-roster-label { margin-top: 18px; }
|
| 114 |
.persona-roster { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; max-height: 230px; overflow-y: auto; }
|
| 115 |
.persona-roster-empty { font-family: var(--p-mono); font-size: 10px; color: var(--p-muted); padding: 4px 0; }
|
|
@@ -121,6 +127,11 @@
|
|
| 121 |
padding: 7px 9px !important; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 122 |
}
|
| 123 |
.persona-roster-name:hover { background: var(--p-paper-2) !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
.persona-roster-x {
|
| 125 |
cursor: pointer; flex-shrink: 0; font-size: 11px !important;
|
| 126 |
color: var(--p-transmit) !important; background: var(--p-card) !important;
|
|
|
|
| 109 |
.persona-edit:focus { background: var(--p-card); box-shadow: 0 0 0 1.5px var(--p-transmit); }
|
| 110 |
.persona-edit:empty::before { content: attr(data-ph); color: var(--p-muted); opacity: .6; font-style: italic; }
|
| 111 |
|
| 112 |
+
/* Empty state — shown until a hero is recruited or picked from the barracks. */
|
| 113 |
+
.persona-empty {
|
| 114 |
+
font-family: var(--p-mono); font-size: 12px; letter-spacing: .04em; color: var(--p-muted);
|
| 115 |
+
padding: 28px 0; max-width: 50ch;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* ── Barracks roster (saved heroes) ────────────────────────────────────────── */
|
| 119 |
.persona-roster-label { margin-top: 18px; }
|
| 120 |
.persona-roster { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; max-height: 230px; overflow-y: auto; }
|
| 121 |
.persona-roster-empty { font-family: var(--p-mono); font-size: 10px; color: var(--p-muted); padding: 4px 0; }
|
|
|
|
| 127 |
padding: 7px 9px !important; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 128 |
}
|
| 129 |
.persona-roster-name:hover { background: var(--p-paper-2) !important; }
|
| 130 |
+
/* The selected hero is highlighted in the barracks. */
|
| 131 |
+
.persona-roster-item.active .persona-roster-name {
|
| 132 |
+
background: var(--p-ink) !important; color: var(--p-paper) !important;
|
| 133 |
+
border-color: var(--p-ink) !important; box-shadow: inset 4px 0 0 var(--p-transmit); font-weight: 700;
|
| 134 |
+
}
|
| 135 |
.persona-roster-x {
|
| 136 |
cursor: pointer; flex-shrink: 0; font-size: 11px !important;
|
| 137 |
color: var(--p-transmit) !important; background: var(--p-card) !important;
|