diff --git a/app/index.html b/app/index.html
index 59f0255..86650cb 100644
--- a/app/index.html
+++ b/app/index.html
@@ -955,6 +955,7 @@ async function generatePDF() {
state.items.forEach(item => {
// Item header
needSpace(lh * 6); // need room for at least header + one line
+ pg.drawLine({start:{x:M.left, y:y+3}, end:{x:M.left+W, y:y+3}, thickness:0.75, color:accent});
pg.drawText('ITEM / PROJECT / TRAVEL', {x:M.left, y, size:sz, font:fontBold, color:accent});
const subStr = `Subtotal: ${baseCur} ${fmtAmt(item._subtotal)}`;
const subW = fontBold.widthOfTextAtSize(subStr, sz);
@@ -967,9 +968,26 @@ async function generatePDF() {
item.lines.forEach((ln, li) => {
needSpace(lh * 8);
- // Thin grey rule above each vendor line
+ // Spacing between consecutive expense lines
+ if (!justBroke && li > 0) y -= 6;
+
+ // Pre-calculate content height so zebra stripe rect can be drawn before text
+ const stripeH = (() => {
+ let h = (lh + 2) * 2; // vendor + description rows
+ if (ln.hasReceipt && ln.receipts.length > 0) h += lh * ln.receipts.length;
+ else if (!ln.hasReceipt) h += lh * Math.max(1, wrapText(ln.noReceiptExplanation||'–', fontBody, szSm, W*0.6).length);
+ else h += lh;
+ h += lh * Math.max(1, (ln.programs||[]).length);
+ return h + 4;
+ })();
+
+ // Zebra stripe: odd-indexed lines get a light grey background
+ if (li % 2 === 1) {
+ pg.drawRectangle({x:M.left, y:y-stripeH, width:W, height:stripeH+lh+2, color:rgb(0.95,0.95,0.95)});
+ }
+
+ // Thin grey rule above vendor text (drawn on top of stripe)
if (!justBroke) {
- if (li > 0) y -= 6;
pg.drawLine({start:{x:M.left, y:y+lh+1}, end:{x:M.left+W, y:y+lh+1}, thickness:0.3, color:lineCol});
}