Jimin Huang commited on
Commit
3136a69
·
1 Parent(s): 10e2649

Change settings

Browse files
Files changed (1) hide show
  1. src/components/HeaderOpen.vue +99 -61
src/components/HeaderOpen.vue CHANGED
@@ -1,6 +1,6 @@
1
  <template>
2
  <div class="live">
3
- <!-- Toolbar (assets + mode) -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
@@ -26,7 +26,7 @@
26
  </div>
27
  </section>
28
 
29
- <!-- Podium + Cards -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards-grid-f1">
32
  <article
@@ -43,7 +43,7 @@
43
  }"
44
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
45
  >
46
- <!-- Podium ribbon (no crown) -->
47
  <div v-if="c.rank && c.rank <= 3" class="podium-ribbon" :data-rank="c.rank"></div>
48
 
49
  <!-- Header: logo + names -->
@@ -60,7 +60,7 @@
60
 
61
  <!-- Net value row -->
62
  <div class="net">
63
- <div class="net__label">Net Value</div>
64
  <div class="net__value">{{ fmtUSD(c.balance) }}</div>
65
  </div>
66
 
@@ -101,16 +101,16 @@ import AssetTabs from '../components/AssetTabs.vue'
101
  import CompareChartE from '../components/CompareChartE.vue'
102
  import { dataService } from '../lib/dataService'
103
 
104
- /* helpers */
105
  import { getAllDecisions } from '../lib/dataCache'
106
  import { readAllRawDecisions } from '../lib/idb'
107
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
108
  import { STRATEGIES } from '../lib/strategies'
109
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
110
 
111
- /* config */
112
- const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA']
113
- const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
114
 
115
  const ASSET_ICONS = {
116
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
@@ -127,22 +127,25 @@ const AGENT_LOGOS = {
127
  }
128
  const ASSET_CUTOFF = { BTC: '2025-08-01' }
129
 
130
- /* state */
131
  const mode = ref('usd')
132
  const asset = ref('BTC')
133
  const rowsRef = ref([])
134
  let allDecisions = []
135
  const cards = shallowRef([])
 
136
  let unsubscribe = null
137
 
138
- /* bootstrap */
139
  onMounted(async () => {
140
  unsubscribe = dataService.subscribe((state) => {
141
  rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
142
  })
143
 
 
144
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
145
 
 
146
  if (!dataService.loaded && !dataService.loading) {
147
  dataService.load(false).catch(e => console.error('LiveView: load failed', e))
148
  }
@@ -161,24 +164,24 @@ onBeforeUnmount(() => {
161
  if (unsubscribe) { unsubscribe(); unsubscribe = null }
162
  })
163
 
164
- /* helpers */
165
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
166
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
167
  const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
168
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
169
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
170
 
171
- /* filters */
172
  const filteredRows = computed(() =>
173
  (rowsRef.value || []).filter(r => {
174
  if (r.asset !== asset.value) return false
175
- if (r.strategy !== 'long_only') return false
176
  const name = (r?.agent_name || '').toLowerCase()
177
  return !EXCLUDED_AGENT_NAMES.has(name)
178
  })
179
  )
180
 
181
- /* winners */
182
  const winners = computed(() => {
183
  const byAgent = new Map()
184
  for (const r of filteredRows.value) {
@@ -199,12 +202,14 @@ const winnersForChart = computed(() =>
199
  decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
200
  }))
201
  )
 
 
202
  const winnersKey = computed(() => {
203
  const sels = winnersForChart.value || []
204
  return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
205
  })
206
 
207
- /* perf */
208
  async function buildSeq(sel) {
209
  const { agent_name: agentName, asset: assetCode, model } = sel
210
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
@@ -214,6 +219,7 @@ async function buildSeq(sel) {
214
 
215
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
216
 
 
217
  if (!ids.length) {
218
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
219
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
@@ -257,6 +263,7 @@ watch(
257
 
258
  if (!perfs.length) { cards.value = []; return }
259
 
 
260
  const first = perfs[0]
261
  const assetCode = first.sel.asset
262
  const bhCard = {
@@ -274,6 +281,7 @@ watch(
274
  barPct: 0
275
  }
276
 
 
277
  const agentCards = perfs.map(({ sel, perf }) => {
278
  const gapUsd = perf.stratLast - perf.bhLast
279
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
@@ -286,15 +294,22 @@ watch(
286
  balance: perf.stratLast,
287
  date: perf.date,
288
  logo: AGENT_LOGOS[sel.agent_name] || null,
289
- gapUsd, gapPct, profitUsd,
 
290
  rank: null,
291
  barPct: 0
292
  }
293
  })
294
 
 
295
  agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
296
  agentCards.forEach((c, i) => { c.rank = i + 1 })
297
 
 
 
 
 
 
298
  cards.value = [bhCard, ...agentCards].slice(0,5)
299
  } finally { computing = false }
300
  },
@@ -303,75 +318,98 @@ watch(
303
  </script>
304
 
305
  <style scoped>
306
- :root {
307
- --ama-start: 0, 0, 185;
308
- --ama-end: 240, 0, 15;
309
- --gold: #d4af37;
310
- --silver: #c0c0c0;
311
- --bronze: #cd7f32;
312
- }
313
-
314
- /* page */
315
- .live { max-width: 1280px; margin: 0 auto; padding: 16px 24px 64px; background: radial-gradient(ellipse at top, #fdfdfd 0%, #f8f9ff 100%); color: #0f172a; }
316
 
317
  /* toolbar */
318
- .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0 12px; background: #ffffff; border-bottom: 2px solid rgba(var(--ama-end), .15); backdrop-filter: blur(10px); }
319
  .toolbar__right { display: flex; align-items: center; gap: 12px; }
320
 
321
  /* mode buttons */
322
- .mode__btn { height: 32px; min-width: 42px; padding: 0 10px; border-radius: 10px; border: 1px solid #CDD3E1; background: #ffffff; font-weight: 700; color: #0f172a; transition: all 0.2s ease; }
323
- .mode__btn.is-active { background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end))); color: #ffffff; border: none; box-shadow: 0 0 6px rgba(var(--ama-end), .3); }
324
 
325
  /* panels */
326
- .panel { background: #ffffff; border: 1px solid #E7ECF3; border-radius: 16px; margin-top: 16px; }
327
- .panel--chart { padding: 10px; }
328
- .panel--cards { padding: 16px; }
329
 
330
  /* empty */
331
- .empty { padding: 20px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .95rem; text-align: center; }
332
-
333
- /* grid */
334
- .cards-grid-f1 { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
335
-
336
- /* F1 card base */
337
- .card-f1 { position: relative; display: grid; grid-template-rows: auto auto auto; gap: 10px; padding: 18px 18px 20px; min-height: 210px; border-radius: 16px; background: linear-gradient(145deg,#ffffff,#f8faff 80%,#ffffff 100%); border: 1px solid #E6E8F0; box-shadow: 0 2px 12px rgba(0,0,0,.04); transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease; }
338
- .card-f1:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0,0,0,.08); }
339
- .card-f1.bh { border-style: dashed; }
340
-
341
- /* podium styles */
342
- .card-f1.gold { border-color: var(--gold); box-shadow: 0 0 0 1px rgba(212,175,55,.25), 0 8px 26px rgba(212,175,55,.18); }
343
- .card-f1.silver { border-color: var(--silver); box-shadow: 0 0 0 1px rgba(192,192,192,.25), 0 8px 26px rgba(192,192,192,.16); }
344
- .card-f1.bronze { border-color: var(--bronze); box-shadow: 0 0 0 1px rgba(205,127,50,.22), 0 8px 26px rgba(205,127,50,.14); }
345
-
346
- /* small podium ribbon at top */
347
- .podium-ribbon { position: absolute; top: 0; left: 0; right: 0; height: 6px; border-top-left-radius: 16px; border-top-right-radius: 16px; }
348
- .card-f1.gold .podium-ribbon { background: linear-gradient(90deg, #f6e27a, var(--gold)); }
349
- .card-f1.silver .podium-ribbon { background: linear-gradient(90deg, #e9eef2, var(--silver)); }
350
- .card-f1.bronze .podium-ribbon { background: linear-gradient(90deg, #f0c6a1, var(--bronze)); }
351
-
352
- /* head */
353
- .head { display: grid; grid-template-columns: 44px minmax(0,1fr); align-items: center; gap: 12px; }
354
- .logo { width: 44px; height: 44px; border-radius: 12px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E5E7EB; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  .logo img { width: 100%; height: 100%; object-fit: contain; }
356
  .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }
357
  .names { min-width: 0; }
358
  .agent { font-size: 16px; font-weight: 900; letter-spacing: .02em; text-transform: uppercase; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
359
  .model { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
360
 
361
- /* net row */
362
  .net { display: grid; grid-template-columns: 1fr auto; align-items: end; }
363
  .net__label { font-size: 12px; color: #6b7280; }
364
  .net__value { font-size: clamp(18px, 2.2vw, 25px); font-weight: 700; letter-spacing: -.01em; color: #0f172a; }
365
 
366
- /* bar vs B&H */
367
  .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; }
368
  .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
369
  .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
370
 
371
- /* bottom */
372
  .bottom { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; align-items: end; gap: 8px; }
373
- .chips{ grid-column:1 / -1; grid-row:1; display:inline-flex; gap:8px; flex-wrap:wrap; }
374
- .eod{ grid-column:2; grid-row:2; justify-self:end; font-size:12px; color:#6b7280; }
 
 
 
 
 
 
 
 
 
 
 
 
375
  .chip { font-size: 12px; font-weight: 800; padding: 4px 8px; border-radius: 999px; background: #F6F8FB; color: #0f172a; border: 1px solid #E7ECF3; }
376
  .chip.pos { color: #0e7a3a; background: #E9F7EF; border-color: #d7f0e0; }
377
  .chip.neg { color: #B91C1C; background: #FBEAEA; border-color: #F3DADA; }
 
1
  <template>
2
  <div class="live">
3
+ <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
 
26
  </div>
27
  </section>
28
 
29
+ <!-- F1 Scoreboard Cards -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards-grid-f1">
32
  <article
 
43
  }"
44
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
45
  >
46
+ <!-- Podium ribbon (rank 1-3 only) -->
47
  <div v-if="c.rank && c.rank <= 3" class="podium-ribbon" :data-rank="c.rank"></div>
48
 
49
  <!-- Header: logo + names -->
 
60
 
61
  <!-- Net value row -->
62
  <div class="net">
63
+ <div class="net__label">Net value</div>
64
  <div class="net__value">{{ fmtUSD(c.balance) }}</div>
65
  </div>
66
 
 
101
  import CompareChartE from '../components/CompareChartE.vue'
102
  import { dataService } from '../lib/dataService'
103
 
104
+ /* --- same helpers as chart --- */
105
  import { getAllDecisions } from '../lib/dataCache'
106
  import { readAllRawDecisions } from '../lib/idb'
107
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
108
  import { STRATEGIES } from '../lib/strategies'
109
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
110
 
111
+ /* ---------- config ---------- */
112
+ const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
113
+ const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
114
 
115
  const ASSET_ICONS = {
116
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
 
127
  }
128
  const ASSET_CUTOFF = { BTC: '2025-08-01' }
129
 
130
+ /* ---------- state ---------- */
131
  const mode = ref('usd')
132
  const asset = ref('BTC')
133
  const rowsRef = ref([])
134
  let allDecisions = []
135
  const cards = shallowRef([])
136
+
137
  let unsubscribe = null
138
 
139
+ /* ---------- bootstrap ---------- */
140
  onMounted(async () => {
141
  unsubscribe = dataService.subscribe((state) => {
142
  rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
143
  })
144
 
145
+ // immediate sync with current state (in case data already loaded)
146
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
147
 
148
+ // trigger a load only if nothing is in-flight
149
  if (!dataService.loaded && !dataService.loading) {
150
  dataService.load(false).catch(e => console.error('LiveView: load failed', e))
151
  }
 
164
  if (unsubscribe) { unsubscribe(); unsubscribe = null }
165
  })
166
 
167
+ /* ---------- helpers ---------- */
168
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
169
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
170
  const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
171
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
172
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
173
 
174
+ /* rows for selected asset (exclude vanilla/vinilla) - only show long_only strategy like leaderboard */
175
  const filteredRows = computed(() =>
176
  (rowsRef.value || []).filter(r => {
177
  if (r.asset !== asset.value) return false
178
+ if (r.strategy !== 'long_only') return false // 只显示 Aggressive Long Only
179
  const name = (r?.agent_name || '').toLowerCase()
180
  return !EXCLUDED_AGENT_NAMES.has(name)
181
  })
182
  )
183
 
184
+ /* winners: best model per agent (by leaderboard balance) */
185
  const winners = computed(() => {
186
  const byAgent = new Map()
187
  for (const r of filteredRows.value) {
 
202
  decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
203
  }))
204
  )
205
+
206
+ /* stable key to avoid identity churn */
207
  const winnersKey = computed(() => {
208
  const sels = winnersForChart.value || []
209
  return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
210
  })
211
 
212
+ /* ---------- PERF (chart parity) ---------- */
213
  async function buildSeq(sel) {
214
  const { agent_name: agentName, asset: assetCode, model } = sel
215
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
 
219
 
220
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
221
 
222
+ // if using decision_ids, data already prefiltered
223
  if (!ids.length) {
224
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
225
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
 
263
 
264
  if (!perfs.length) { cards.value = []; return }
265
 
266
+ // Buy & Hold first
267
  const first = perfs[0]
268
  const assetCode = first.sel.asset
269
  const bhCard = {
 
281
  barPct: 0
282
  }
283
 
284
+ // Agents
285
  const agentCards = perfs.map(({ sel, perf }) => {
286
  const gapUsd = perf.stratLast - perf.bhLast
287
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
 
294
  balance: perf.stratLast,
295
  date: perf.date,
296
  logo: AGENT_LOGOS[sel.agent_name] || null,
297
+ gapUsd, gapPct,
298
+ profitUsd,
299
  rank: null,
300
  barPct: 0
301
  }
302
  })
303
 
304
+ // Rank by balance (agents only)
305
  agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
306
  agentCards.forEach((c, i) => { c.rank = i + 1 })
307
 
308
+ // Perf bar width scaled to max |gapUsd|
309
+ const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
310
+ agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
311
+
312
+ // BH first, then top agents
313
  cards.value = [bhCard, ...agentCards].slice(0,5)
314
  } finally { computing = false }
315
  },
 
318
  </script>
319
 
320
  <style scoped>
321
+ .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; background: #ffffff; padding-bottom: 56px; }
 
 
 
 
 
 
 
 
 
322
 
323
  /* toolbar */
324
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
325
  .toolbar__right { display: flex; align-items: center; gap: 12px; }
326
 
327
  /* mode buttons */
328
+ .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
329
+ .mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
330
 
331
  /* panels */
332
+ .panel { background: #ffffff; border: 1px solid #E7ECF3; border-radius: 14px; }
333
+ .panel--chart { padding: 10px 10px 2px; }
334
+ .panel--cards { padding: 12px; }
335
 
336
  /* empty */
337
+ .empty { padding: 14px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .92rem; background: #ffffff; }
338
+
339
+ /* GRID */
340
+ .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
341
+ @media (max-width: 1400px) { .cards-grid-f1 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
342
+ @media (max-width: 1100px) { .cards-grid-f1 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
343
+ @media (max-width: 900px) { .cards-grid-f1 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
344
+ @media (max-width: 640px) { .cards-grid-f1 { grid-template-columns: 1fr; } }
345
+
346
+ /* F1 Card */
347
+ .card-f1 {
348
+ position: relative;
349
+ display: grid;
350
+ grid-template-rows: auto auto auto; /* head, net, bottom */
351
+ gap: 10px;
352
+ padding: 16px 16px 18px;
353
+ min-height: 210px;
354
+ border-radius: 14px;
355
+ background: linear-gradient(145deg,#ffffff,#fafbfd 55%,#ffffff 100%);
356
+ border: 1px solid #E7ECF3;
357
+ box-shadow: 0 1px 2px rgba(16,24,40,.03), 0 4px 12px rgba(16,24,40,.04);
358
+ color: #0f172a;
359
+ transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease;
360
+ }
361
+ .card-f1:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(16,24,40,.08); border-color: #D9E2EF; }
362
+ .card-f1.bh { border-style: dashed; opacity: 1; }
363
+
364
+ /* Podium accents for top 3 */
365
+ .card-f1.gold { border-color: #d4af37; box-shadow: 0 0 0 1px rgba(212,175,55,.25), 0 10px 24px rgba(212,175,55,.12); }
366
+ .card-f1.silver { border-color: #c0c0c0; box-shadow: 0 0 0 1px rgba(192,192,192,.25), 0 10px 24px rgba(192,192,192,.10); }
367
+ .card-f1.bronze { border-color: #cd7f32; box-shadow: 0 0 0 1px rgba(205,127,50,.22), 0 10px 24px rgba(205,127,50,.10); }
368
+ .podium-ribbon { position: absolute; top: 0; left: 0; right: 0; height: 6px; border-top-left-radius: 14px; border-top-right-radius: 14px; }
369
+ .card-f1.gold .podium-ribbon { background: linear-gradient(90deg, #f7e27a, #d4af37); }
370
+ .card-f1.silver .podium-ribbon { background: linear-gradient(90deg, #eef2f6, #c0c0c0); }
371
+ .card-f1.bronze .podium-ribbon { background: linear-gradient(90deg, #f3c39d, #cd7f32); }
372
+
373
+ /* Head */
374
+ .head {
375
+ display: grid;
376
+ grid-template-columns: 40px minmax(0,1fr);
377
+ align-items: center;
378
+ gap: 10px;
379
+ }
380
+ .logo { width: 40px; height: 40px; border-radius: 10px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E7ECF3; }
381
  .logo img { width: 100%; height: 100%; object-fit: contain; }
382
  .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }
383
  .names { min-width: 0; }
384
  .agent { font-size: 16px; font-weight: 900; letter-spacing: .02em; text-transform: uppercase; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
385
  .model { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
386
 
387
+ /* Net row */
388
  .net { display: grid; grid-template-columns: 1fr auto; align-items: end; }
389
  .net__label { font-size: 12px; color: #6b7280; }
390
  .net__value { font-size: clamp(18px, 2.2vw, 25px); font-weight: 700; letter-spacing: -.01em; color: #0f172a; }
391
 
392
+ /* Bar vs B&H */
393
  .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; }
394
  .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
395
  .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
396
 
397
+ /* Bottom */
398
  .bottom { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; align-items: end; gap: 8px; }
399
+ .chips{
400
+ grid-column:1 / -1;
401
+ grid-row:1;
402
+ display:inline-flex;
403
+ gap:8px;
404
+ flex-wrap:wrap;
405
+ }
406
+ .eod{
407
+ grid-column:2;
408
+ grid-row:2;
409
+ justify-self:end;
410
+ font-size:12px;
411
+ color:#6b7280;
412
+ }
413
  .chip { font-size: 12px; font-weight: 800; padding: 4px 8px; border-radius: 999px; background: #F6F8FB; color: #0f172a; border: 1px solid #E7ECF3; }
414
  .chip.pos { color: #0e7a3a; background: #E9F7EF; border-color: #d7f0e0; }
415
  .chip.neg { color: #B91C1C; background: #FBEAEA; border-color: #F3DADA; }