diff --git a/src/css/styles.css b/src/css/styles.css
index 386124b..81a03a8 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -286,8 +286,14 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.ppnl-tabs{display:flex;gap:4px}
.ppnl-tab{font-family:var(--mono);font-size:.65rem;letter-spacing:1px;cursor:pointer;background:transparent;border:1px solid var(--bd);color:var(--mu);padding:4px 10px;transition:all .15s}
.ppnl-tab.active{border-color:var(--green);color:var(--green)}
-.ppnl-cards{padding:16px 20px 20px;display:grid;grid-template-columns:repeat(3,minmax(0,340px));gap:12px;justify-content:center}
+.ppnl-layout{padding:16px 20px 20px;display:grid;grid-template-columns:1.4fr 1fr;gap:14px;align-items:stretch}
.ppnl-card{background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:14px 16px;position:relative}
+.ppnl-trio{display:flex;flex-direction:column;gap:10px}
+.ppnl-trio .ppnl-card{flex:1;display:flex;flex-direction:column;justify-content:center}
+.ppnl-hero{background:linear-gradient(135deg,var(--gd),transparent 65%),var(--s2);border-color:var(--gb);padding:24px 28px;display:flex;flex-direction:column;justify-content:center}
+.ppnl-hero .ppnl-lbl{font-size:.65rem;margin-bottom:14px}
+.ppnl-hero .ppnl-main{font-size:2.2rem;letter-spacing:-1px;color:var(--green)}
+.ppnl-hero .ppnl-sub{font-size:.72rem;margin-top:8px}
/* Generic styled-popover tooltip. Opt in by adding class="has-tip" + data-tip="…" */
.has-tip{position:relative;cursor:help}
@@ -308,8 +314,7 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.ppnl-mtbl tr:last-child td{border-bottom:none}
.ppnl-mtbl .rate-hi{color:var(--green)}
.ppnl-mtbl .rate-lo{color:var(--red)}
-@media(max-width:600px){.ppnl-cards{grid-template-columns:1fr 1fr}}
-@media(max-width:460px){.ppnl-cards{grid-template-columns:1fr}}
+@media(max-width:600px){.ppnl-layout{grid-template-columns:1fr}.ppnl-hero .ppnl-main{font-size:1.8rem}}
/* EMPTY */
.empty{text-align:center;padding:64px 24px;display:flex;flex-direction:column;align-items:center;gap:16px}
diff --git a/src/js/07-render-charts.js b/src/js/07-render-charts.js
index f28bd3f..7b8824a 100644
--- a/src/js/07-render-charts.js
+++ b/src/js/07-render-charts.js
@@ -445,33 +445,36 @@ function rCharts(displayRows, lots) {
if (sPpnlTab === 'total') {
const s = calcStats(displayRows);
- function card(label, main, sub, tip) {
+ function tile(extraClass, label, main, sub, tip) {
const tipAttr = tip ? ' data-tip="' + tip.replace(/"/g, '"') + '"' : '';
- return '
' +
+ const cls = 'ppnl-card' + (extraClass ? ' ' + extraClass : '') + (tip ? ' has-tip' : '');
+ return '
' +
'
' + label + (tip ? ' ⓘ' : '') + '
' +
'
' + main + '
' +
(sub ? '
' + sub + '
' : '') +
'
';
}
- el.className = 'ppnl-cards';
- el.innerHTML = [
- card('Total Premium Collected',
+ el.className = 'ppnl-layout';
+ el.innerHTML =
+ tile('ppnl-hero',
+ 'Total Premium Collected',
s.totalCount > 0 ? '$' + fmt(s.totalPrem) : dash,
s.totalCount > 0 ? pos(s.settled) + ' settled' + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '',
- 'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.'),
- card('Total Notional',
- s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash,
- s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '',
- 'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.'),
- card('Portfolio APR',
- s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash,
- s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '',
- 'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.'),
- card('Return Rate',
- s.returnRate !== null ? s.returnRate.toFixed(1) + '%' : dash,
- s.settled > 0 ? s.otmCount + ' / ' + s.settled + ' exp OTM' : '',
- 'Share of settled options that expired OTM (premium kept, no assignment/call-away). Open options excluded.'),
- ].join('');
+ 'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.') +
+ '
' +
+ tile('', 'Total Notional',
+ s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash,
+ s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '',
+ 'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.') +
+ tile('', 'Portfolio APR',
+ s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash,
+ s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '',
+ 'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.') +
+ tile('', 'Return Rate',
+ s.returnRate !== null ? s.returnRate.toFixed(1) + '%' : dash,
+ s.settled > 0 ? s.otmCount + ' / ' + s.settled + ' exp OTM' : '',
+ 'Share of settled options that expired OTM (premium kept, no assignment/call-away). Open options excluded.') +
+ '
';
} else {
// Group trades by month — OPEN trades by open date, settled by expiry date
diff --git a/test/integration/pnl-tiles.test.js b/test/integration/pnl-tiles.test.js
index 7476ef1..5037a7b 100644
--- a/test/integration/pnl-tiles.test.js
+++ b/test/integration/pnl-tiles.test.js
@@ -72,6 +72,35 @@ test('Return Rate tile has tooltip + ⓘ glyph', (t) => {
assertHasTooltip(findCard(window, /Return Rate/i), /OTM|expired/i);
});
+test('Total tab uses hero + supporting trio layout (issue #42)', (t) => {
+ // Total Premium Collected is the hero (focal point); the other three live in a trio container.
+ const trades = [
+ { id: 1, asset: 'BTC', type: 'PUT', date: '2026-01-01', expiry: '2026-01-15',
+ dte: 14, strike: 50000, size: 0.1, premium: 120, outcome: 'EXPIRED',
+ closeCost: 0, platform: 'RYSK' },
+ ];
+ const { window, teardown } = setupJsdom({ trades });
+ t.after(teardown);
+
+ const hero = window.document.querySelector('.ppnl-hero');
+ assert.ok(hero, 'expected a .ppnl-hero element on the Total tab');
+ const heroLbl = hero.querySelector('.ppnl-lbl');
+ assert.match(heroLbl.textContent, /Total Premium Collected/i,
+ `hero should be Total Premium Collected, got "${heroLbl.textContent}"`);
+
+ const trio = window.document.querySelector('.ppnl-trio');
+ assert.ok(trio, 'expected a .ppnl-trio container');
+ const trioLabels = Array.from(trio.querySelectorAll('.ppnl-lbl')).map(l => l.textContent.trim());
+ assert.strictEqual(trioLabels.length, 3, `trio should hold 3 tiles, got ${trioLabels.length}`);
+ assert.ok(trioLabels.some(l => /Total Notional/i.test(l)));
+ assert.ok(trioLabels.some(l => /Portfolio APR/i.test(l)));
+ assert.ok(trioLabels.some(l => /Return Rate/i.test(l)));
+
+ // Hero retains its tooltip + ⓘ glyph affordance.
+ assert.ok(hero.classList.contains('has-tip'), 'hero should keep has-tip styling');
+ assert.ok(hero.querySelector('.ppnl-tip-ico'), 'hero should keep the ⓘ glyph');
+});
+
test('Total tab no longer renders Realised/Unrealised/Total P&L cards (issue #40)', (t) => {
// Per #40, the hero band is the canonical Realised/Unrealised/Total surface.
// The Total tab must not duplicate them.