diff --git a/src/css/styles.css b/src/css/styles.css
index 16835b2..6c19622 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -263,6 +263,18 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.hcard-hint b{color:var(--orange);font-weight:700}
.hcard-hint-ok{color:var(--green)}
.lot-badge{font-size:.5rem;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:2px 6px;background:var(--s3);border:1px solid var(--bd2);color:var(--mu2);font-family:var(--mono)}
+.hcard-spot-muted{color:var(--mu2)}
+
+/* Wide-card layout: when <=2 visible open lots and viewport >=720px,
+ render each card as a full-width horizontal row with 4 columns. */
+@media (min-width:720px){
+ .holdings-grid--wide{grid-template-columns:1fr}
+ .holdings-grid--wide .hcard{display:grid;grid-template-columns:minmax(180px,1fr) minmax(140px,1fr) minmax(220px,1.4fr) minmax(260px,1.4fr);align-items:stretch}
+ .holdings-grid--wide .hcard-hd{border-bottom:none;border-right:1px solid var(--bd);flex-direction:column;align-items:flex-start;gap:8px;justify-content:center}
+ .holdings-grid--wide .hcard-hero{border-right:1px solid var(--bd);display:flex;flex-direction:column;justify-content:center;padding:13px}
+ .holdings-grid--wide .hcard-spot{border-top:none;border-right:1px solid var(--bd);padding:13px;display:flex;flex-direction:column;justify-content:center}
+ .holdings-grid--wide .hcard-stats{border-top:none;grid-template-columns:1fr 1fr 1fr;align-items:center}
+}
/* MERGE LOTS */
.btn-merge{font-size:.62rem;font-family:var(--mono);font-weight:700;padding:3px 10px;border-radius:5px;cursor:pointer;border:1px solid var(--ob);color:var(--orange);background:var(--od);transition:all .15s;white-space:nowrap;letter-spacing:.5px;text-transform:uppercase}
diff --git a/src/js/06-render-table.js b/src/js/06-render-table.js
index a10725c..105cf2a 100644
--- a/src/js/06-render-table.js
+++ b/src/js/06-render-table.js
@@ -321,8 +321,16 @@ function rTable(displayRows, streams, lots) {
: '';
// Live spot, unrealized P&L vs net cost, breakeven hint
const spot = livePrices[a];
- let spotBlock = '';
- if (spot) {
+ let spotBlock;
+ if (!spot) {
+ spotBlock = '
'
+ + '
'
+ + 'Spot'
+ + '—'
+ + '
'
+ + '
spot unavailable
'
+ + '
';
+ } else {
const pnlPerToken = spot - nc;
const pnlTotal = pnlPerToken * lot.size;
const pnlPct = nc > 0 ? (pnlPerToken / nc * 100) : 0;
@@ -377,7 +385,7 @@ function rTable(displayRows, streams, lots) {
+ 'Holdings
'
+ '' + openLotCount + ' open lot' + (openLotCount !== 1 ? 's' : '') + ''
+ ''
- + '' + cardsHtml + '
'
+ + '' + cardsHtml + '
'
+ (mergesHtml ? '' + mergesHtml + '
' : '')
+ '';
} else {
diff --git a/test/integration/holding-card-wide.test.js b/test/integration/holding-card-wide.test.js
new file mode 100644
index 0000000..440f4c5
--- /dev/null
+++ b/test/integration/holding-card-wide.test.js
@@ -0,0 +1,100 @@
+const test = require('node:test');
+const assert = require('node:assert');
+const { setupJsdom } = require('../helpers/setupJsdom');
+
+// Issue #52: Wide-card layout for holding cards at low lot counts.
+// JS-side contract: when the number of *visible* open lots is <= 2, the
+// .holdings-grid element gains the class `holdings-grid--wide`. The CSS
+// activates the wide row layout at >= 720px viewport width via @media.
+
+const ethHolding = {
+ id: 1, asset: 'ETH', type: 'HOLDING', date: '2026-01-01', expiry: '',
+ dte: null, strike: 3000, size: 1, premium: 0, outcome: 'OPEN',
+ closeCost: 0, platform: 'SPOT',
+};
+
+function btcHolding(id, n) {
+ return { id, asset: 'BTC', type: 'HOLDING', date: '2026-0' + n + '-01',
+ expiry: '', dte: null, strike: 60000 + id, size: 0.1, premium: 0,
+ outcome: 'OPEN', closeCost: 0, platform: 'SPOT' };
+}
+
+function getGrid(window) {
+ const g = window.document.querySelector('.holdings-grid');
+ assert.ok(g, 'expected a .holdings-grid element');
+ return g;
+}
+
+test('1 visible open lot: holdings-grid has --wide class', (t) => {
+ const { window, teardown } = setupJsdom({ trades: [ethHolding] });
+ t.after(teardown);
+ const grid = getGrid(window);
+ assert.ok(grid.classList.contains('holdings-grid--wide'),
+ 'grid should have holdings-grid--wide class with 1 visible lot');
+});
+
+test('2 visible open lots: holdings-grid has --wide class', (t) => {
+ const { window, teardown } = setupJsdom({
+ trades: [ethHolding, btcHolding(2, 2)],
+ });
+ t.after(teardown);
+ const grid = getGrid(window);
+ assert.ok(grid.classList.contains('holdings-grid--wide'),
+ 'grid should have holdings-grid--wide class with 2 visible lots');
+});
+
+test('3 visible open lots: holdings-grid does NOT have --wide class', (t) => {
+ const { window, teardown } = setupJsdom({
+ trades: [ethHolding, btcHolding(2, 2), btcHolding(3, 3)],
+ });
+ t.after(teardown);
+ const grid = getGrid(window);
+ assert.ok(!grid.classList.contains('holdings-grid--wide'),
+ 'grid should NOT have holdings-grid--wide class with 3+ visible lots');
+});
+
+test('asset filter honored: BTC filter w/ 1 BTC lot among many → wide', (t) => {
+ const { window, teardown } = setupJsdom({
+ trades: [ethHolding, btcHolding(2, 2), btcHolding(3, 3),
+ { ...ethHolding, id: 4, date: '2026-02-01' }],
+ });
+ t.after(teardown);
+ // Filter to BTC: should reduce visible lots to 2 → wide class applies.
+ window.setFilter('BTC');
+ const grid = getGrid(window);
+ assert.ok(grid.classList.contains('holdings-grid--wide'),
+ 'asset filter should narrow visible-lot count for the threshold');
+});
+
+test('missing spot: card renders stable Spot — placeholder', (t) => {
+ // No livePrices stub → spot is undefined.
+ const { window, teardown } = setupJsdom({ trades: [ethHolding] });
+ t.after(teardown);
+ const card = window.document.querySelector('.hcard');
+ const spot = card.querySelector('.hcard-spot');
+ assert.ok(spot, 'spot block should render even when price missing');
+ assert.match(spot.textContent, /Spot\s*—/,
+ 'placeholder should show "Spot —"');
+ assert.match(spot.textContent, /spot unavailable/i,
+ 'placeholder sub-line should read "spot unavailable"');
+});
+
+test('wide layout preserves edit btn, lot badge, merge btn', (t) => {
+ // 2 ETH lots: edit btn (HOLDING), lot badge (>1 lot), merge btn (>=2 open).
+ const { window, teardown } = setupJsdom({
+ trades: [
+ ethHolding,
+ { ...ethHolding, id: 2, date: '2026-02-01', strike: 3200 },
+ ],
+ });
+ t.after(teardown);
+ const grid = getGrid(window);
+ assert.ok(grid.classList.contains('holdings-grid--wide'),
+ 'precondition: 2 lots → wide');
+ const cards = window.document.querySelectorAll('.hcard');
+ assert.strictEqual(cards.length, 2);
+ assert.ok(cards[0].querySelector('.hcard-edit'), 'edit button present');
+ assert.ok(cards[0].querySelector('.lot-badge'), 'lot badge present');
+ assert.ok(window.document.querySelector('.btn-merge'),
+ 'merge button present');
+});