/* ==========================================================================
   THE PRINTED SHEET — A4.
   Two outputs from one record, switched by a class on #preview:
     .copy-inside  → everything. The copy that stays in this office.
     .copy-outside → .confidential stripped. The copy that leaves it.
   The classes are named after WHERE THE PAPER GOES, not after who reads it:
   the outside copy of an order sheet goes to a workshop and the outside copy
   of a quotation goes to the client, and both are the same switch.
   The page COUNT is not fixed: the images page appears only when there are
   images, and is master-only when every image on it is hidden from the
   workshop. That is why footers are stamped after all pages exist.
   Chrome print dialog: Headers and footers OFF · Background graphics ON.
   ========================================================================== */

@page { size: A4; margin: 14mm; }

/* A table cell whose value must not be broken across lines — a measurement, a
   weight, a money figure. Declared per COLUMN in the doctype (`nowrap: true`),
   because only the schema knows which values are atomic. */
#preview .page td.nowrap, #preview .page th.nowrap { white-space: nowrap; }

/* Declared column widths become authoritative — see the note in tableBlock.

   A goods table carrying nine columns on A4 is genuinely dense, so it is set
   smaller and tighter than the sheet's other tables. That is not a compromise
   on legibility: the alternative measured out as headings printing on top of
   each other and words broken as "Unhea / ted". A table is not running text and
   the brand's 15px floor is about running text. */
/* The equivalent-currency column on a quotation. Quieter than the quoted one,
   because it is a convenience and the sentence beneath says the quoted price
   governs — the two must not read as equally authoritative. */
#preview .page td.faint { color: var(--ink-secondary); }

/* The one sentence that says which currency governs and at what rate. */
#preview .page .settlenote {
  margin-top: 8px; font-size: 11px; line-height: 1.6; color: var(--ink-secondary);
}

/* A required company detail that has not been filled in. Printed as a visible
   gap, never as a blank: an invoice with no account number is unpayable, and one
   that merely looks finished is how it gets sent anyway. */
#preview .page .missingfield {
  color: var(--status-error); font-weight: 600; letter-spacing: 0.04em;
  border-bottom: 1px dashed var(--status-error);
}

#preview .page table.fixedcols { table-layout: fixed; }
#preview .page table.fixedcols th {
  font-size: 8px; letter-spacing: 0.08em; padding: 5px 4px; overflow-wrap: break-word;
}
#preview .page table.fixedcols td {
  font-size: 11.5px; padding: 6px 4px; overflow-wrap: break-word;
}

#preview.copy-outside .confidential { display: none !important; }
#preview.copy-inside  .outside-only { display: none !important; }

/* --- true, fixed A4, at every width — Joe, 17 Sep 2026 ---------------------

   Every `.page` is exactly 210mm × 297mm, always — the sheet of paper, not
   the printable area inside it. Padding is a UNIFORM 14mm on every side — the
   identical content box the printer produces (182mm × 269mm; see
   paginate.js's own header comment) — and `.page` is a column flex box so its
   LAST child, `.foot`, can be pinned to the very bottom of that box with
   `margin-top: auto` (below) rather than sitting wherever the content above
   it happens to end. `paginate.js` measures the footer's own real height and
   reserves exactly that much of the 269mm for it — not a guessed constant —
   so a sheet with little content still shows its footer flush with the
   bottom edge instead of floating mid-page under the last block. Found by
   Joe reviewing round-1 screenshots, 17 Sep 2026: "the footer sits straight
   under the last content, mid-page."

   ⚠ THIS REPLACES SCALING-BY-RELAYOUT. Until 17 Sep 2026 a narrow pane made
   `.page` itself narrower (`width: min(182mm, 100%)`) and, below 759px,
   reflowed its columns to the phone — a different LAYOUT at every width,
   never the one that prints. What makes a page "true" is that its geometry
   never changes; only how large it is drawn does. `#preview` itself carries
   a `transform: scale()` (below, and in app.css's `.sheetscale` wrapper) so
   the page shrinks to fit a phone exactly the way a PDF viewer shrinks a
   page — pinch to zoom for the real thing — rather than becoming a different
   document at that width. Never scaled past 100%; recomputed on resize by
   `updateScale()` in sheetpane.js.

   Joe's call, 17 Sep 2026, replacing the phone-reflow rationale that used to
   stand here (kept, not deleted — HISTORY.md, 17 Sep 2026 entry): "A scaled
   page is a picture of a document" was the argument for reflow when the
   preview was the only place that geometry existed at all. Real pagination
   changed that — the SAME fixed-size sheets are what predicts the printer
   now, on every screen, so showing a different shape on a phone would mean
   the one place Joe most wants to check page breaks before walking to the
   printer is the one place that no longer matches it.

   ⚠ SCOPED TO `#preview` — DO NOT DROP THAT PREFIX. `.page` alone collided with
   the app's own `.page` heading utility (`h1.page.display`, in app.css) and
   every page title in the app — Dashboard, Settings, the Orders "nothing
   selected" screen, every record's own h1 — rendered as a bordered, padded
   card because it matched THIS rule. Found 2026-08-26: Joe reported the
   Settings title looked like "a big button that isn't a button." It was every
   title in the app, not just Settings; this file's own rules were never meant
   to reach outside the print preview, and most of them already said so
   (`#preview .page ...`) — this one and its two counterparts below did not. */
/* Uniform 14mm padding — 269mm content box, matching `PAGE_INNER_MM` in
   engine/paginate.js. Change one without the other and the preview stops
   predicting what prints. */
#preview .page {
  width: 210mm; height: 297mm; box-sizing: border-box;
  margin: 0 auto 20px; background: var(--paper);
  border: 1px solid var(--line); padding: 14mm; position: relative;
  display: flex; flex-direction: column;
  color: var(--ink-secondary); font-family: var(--font-text); font-weight: 300;
}

/* --- masthead ----------------------------------------------------------- */
.masthead { display: flex; align-items: flex-start; gap: 14px; padding-bottom: 10px; }
.masthead img { width: 34px; height: 34px; flex: none; opacity: 0.9; }
.mast-text { flex: 1; }
#preview .page .overline {
  font-size: 9px; font-weight: 600; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--ink-tertiary);
}
.sheet-title {
  font-family: var(--font-display); font-weight: 500; font-size: 17px;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink); margin-top: 4px;
}
.mast-right { text-align: right; flex: none; }
.order-no {
  font-family: var(--font-display); font-weight: 600; font-size: 19px;
  letter-spacing: 0.10em; color: var(--ink);
}
.mast-right .dates { font-size: 11px; color: var(--ink-tertiary); margin-top: 4px; line-height: 1.6; }

.rule       { border-top: 2px solid var(--ink); margin: 8px 0 0; }
.rule-light { border-top: 1px solid var(--line); margin: 10px 0; }

/* --- banners ------------------------------------------------------------ */
.banner {
  margin-top: 10px; padding: 6px 10px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.20em; text-transform: uppercase;
  display: flex; justify-content: space-between; gap: 12px;
}
.banner-workshop { background: var(--paper-sunk); color: var(--ink-secondary); border: 1px solid var(--line-strong); }
.banner-master   { background: var(--status-error); color: #FAF8F4; border: 1px solid var(--status-error); }

/* --- blocks ------------------------------------------------------------- */
.block { margin-top: 13px; break-inside: avoid; }
.block-head {
  font-family: var(--font-display); font-size: 13px; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink);
  border-bottom: 1px solid var(--line-strong); padding-bottom: 4px; margin-bottom: 8px;
  display: flex; justify-content: space-between; align-items: baseline;
}
.block-head .aside {
  font-family: var(--font-text); font-size: 9px; font-weight: 600;
  letter-spacing: 0.18em; color: var(--ink-tertiary); text-transform: uppercase;
}

.grid { display: grid; gap: 9px 14px; }
/* Same reason as .fgrid in app.css: the track counts are inline `repeat(N, 1fr)`
   and a 1fr track takes its minimum from its content, so one long label could
   push the row wider than the page. */
.grid > * { min-width: 0; }

/* ⚠ THE HERO ROW NARROWS EVERY GRID INSIDE IT TO ROUGHLY HALF THE PAGE, but
   every hero-eligible section still declares the SAME `cols` its schema uses
   everywhere else — usually 4, because that is right for the section at full
   page width, and because the on-screen EDIT form (engine/form.js) reads the
   identical `section.cols` and has plenty of room for it. Nothing in
   gridBlock() knows a section is about to be squeezed beside the photo.

   Found by actually measuring it: order.js's "Order" block renders at ~311px
   inside `.hero-meta` — a 4-column grid there gives each field ~67px, and
   "Outside workshop", "World Gem" and even a plain date like "24 Sep 2026"
   wrapped to two lines while the SAME kind of value never wraps in the
   full-width Client block one row down. Capped at 2 here — measured against
   real field values, not a guess — print-only (`#preview`), so the edit form,
   which was never the problem, is untouched. `!important` because the track
   count is written inline by the renderer, same reason the phone breakpoint
   rule below already needs it. */
#preview .page .hero-meta .grid { grid-template-columns: repeat(2, 1fr) !important; }

/* A label that wrapped to two lines — "Chain / bracelet length" beside three
   one-line labels — dropped its own value below the values either side of it,
   so a row of figures on the printed sheet did not sit on one line. Three
   shared rows per cell puts labels and values back on their own baselines. */
@supports (grid-template-rows: subgrid) {
  .grid { grid-auto-rows: auto; }
  .grid > * { grid-row: span 2; display: grid; grid-template-rows: subgrid; align-content: start; }
}

.label {
  font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-tertiary); margin-bottom: 2px;
  align-self: end;
}
/* Bench-critical values: --ink, 15px floor. Never --ink-tertiary. */
.value { font-size: 15px; font-weight: 400; color: var(--ink); line-height: 1.35; }
.value.sm   { font-size: 13px; }
.value.note { font-size: 13px; font-weight: 300; color: var(--ink-secondary); line-height: 1.6; }
.value.hero { font-size: 20px; font-weight: 500; }
.blank { color: var(--ink-tertiary); }
.fill  { display: inline-block; min-width: 70px; border-bottom: 1px solid var(--line-strong); }

/* --- stone -------------------------------------------------------------- */
.stone { background: var(--paper-sunk); border: 1px solid var(--line-strong); padding: 10px 12px 12px; }
.stone .value { font-size: 16px; font-weight: 500; }
.stone .value.hero { font-size: 20px; }

.flag {
  display: inline-block; font-size: 9px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; padding: 3px 7px; border: 1px solid var(--ink); color: var(--ink);
}

.handling { margin-top: 9px; border: 2px solid var(--ink); padding: 7px 10px; display: flex; align-items: baseline; gap: 10px; }
.handling .label { margin: 0; }
.handling .val {
  font-family: var(--font-display); font-size: 15px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink);
}

/* Stone-safety. Never redacted on any copy. */
.cautions { margin-top: 9px; border: 1px solid var(--status-error); padding: 7px 10px; }
.cautions .label { color: var(--status-error); }
.cautions .value { font-size: 13px; font-weight: 500; }

/* --- tables ------------------------------------------------------------- */
#preview .page table { width: 100%; border-collapse: collapse; }
#preview .page th {
  font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-tertiary); text-align: left; background: var(--paper-sunk);
  padding: 5px 7px; border-bottom: 1px solid var(--line-strong);
}
#preview .page td { font-size: 13px; color: var(--ink); padding: 6px 7px; border-bottom: 1px solid var(--line); vertical-align: top; }
#preview .page td.num, #preview .page th.num { text-align: right; }
#preview .page tr.total td { font-weight: 600; border-top: 2px solid var(--ink); border-bottom: none; font-size: 14px; padding-top: 8px; }
#preview .page tr.margin td { font-weight: 600; font-size: 15px; border-bottom: none; padding-top: 2px; }

/* --- compact goods table (invoice, client copy) -------------------------
   Thumbnail · Details · Qty · Unit price · Total — see compactItemsTable in
   sheet.js. Kept deliberately plain: it reuses the ordinary #preview .page
   table/td rules above for everything except the thumbnail and the merged
   Details cell, so it never drifts from the sheet's normal table type.

   ⚠ THE HEADER ROW IS AN EXCEPTION, matched to the mockup exactly (Joe,
   round 2, 17 Sep 2026): no shaded background, a quieter/smaller label than
   the generic `#preview .page th` rule two blocks up, and the underline is
   `--line-strong` rather than the generic rule's own. Higher specificity
   than that rule (`table.compactitems thead th` beats `.page th`), so
   nothing else on the sheet is touched. */
#preview .page table.compactitems td { vertical-align: middle; }
#preview .page table.compactitems thead th {
  background: none; font-family: var(--font-text); font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-secondary);
  padding: 0 8px 7px; border-bottom: 1px solid var(--line-strong);
}
.thumb {
  width: 36px; height: 36px; background: var(--paper-sunk); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative;
}
.thumb img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
/* The no-image icon, when the frame has no photo to show — same muted line
   colour `.r-thumb-icon` already uses on screen for the identical idiom
   (app.css), so an empty slot reads the same way whether it is on the
   record's Edit tab or on the sheet that gets printed from it. */
.thumb .icon { color: var(--line-strong); }
.item-name { font-size: 13px; color: var(--ink); font-weight: 500; }
.item-spec { font-size: 11px; color: var(--ink-tertiary); margin-top: 2px; }
@media print {
  .thumb { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* --- section-label — the mockup's plain, quiet heading (17 Sep 2026) -----
   Used where the sheet's usual Cinzel `.block-head` reads as too heavy for
   the mockup's design: the compact goods table's "The goods" and the client
   party block's "Bill to". No border beneath it — each of those two already
   draws its own line (the table's header row; nothing, for the party block,
   which needs none). Shared with `.party-label` below rather than
   duplicated, since the mockup specs the two identically. */
.section-label-row, .party-label-row {
  display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px;
}
.section-label, .party-label {
  font-family: var(--font-text); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-tertiary); font-weight: 600;
}
.party-label { margin-bottom: 8px; }

/* --- client party block — "Bill to" on the client copy (17 Sep 2026) -----
   Name, address line(s), "email · phone" — no per-field labels, unlike the
   internal copy's own dense grid (gridBlock, unchanged). See
   clientPartyBlock in sheet.js. */
.party-name { font-size: 14px; color: var(--ink); font-weight: 500; }
.party-line { font-size: 12.5px; color: var(--ink-tertiary); margin-top: 3px; }

/* --- compact totals — matched to the mockup's own markup (17 Sep 2026) ---
   div-based, not a table: no "Amount" heading, no currency column-header
   row. Right-aligned, ~280px, same class names as the mockup file itself. */
.totals-wrap { display: flex; justify-content: flex-end; margin-top: 16px; }
.totals { width: 280px; max-width: 100%; }
.totals-row { display: flex; justify-content: space-between; align-items: baseline; padding: 5px 0; font-size: 12.5px; }
.totals-row .k { color: var(--ink-tertiary); }
.totals-row .v { color: var(--ink-secondary); font-variant-numeric: tabular-nums; }
.totals-row.deduct .v { color: var(--ink-tertiary); }
.totals-due {
  display: flex; justify-content: space-between; align-items: baseline;
  border-top: 1px solid var(--line-strong); margin-top: 6px; padding-top: 10px;
}
.totals-due .k {
  font-family: var(--font-display); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink); font-weight: 500;
}
.totals-due .v {
  font-family: var(--font-display); font-size: 19px; color: var(--ink); font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* A second, fainter figure beside the primary one — the dual-currency
   column, folded into the row instead of a table column the mockup's
   layout has no room for. */
.vwrap { display: flex; align-items: baseline; gap: 10px; }
.vwrap .v.faint { font-size: 11px; color: var(--ink-tertiary); }
.totals-due .vwrap .v.faint { font-size: 12px; }

/* --- dual currency on the costing page --------------------------------------
   "Entered" is a column rather than a footnote: six months on, which figure was
   typed and which was arithmetic is the first thing you need to know about a
   cost line you are being asked to defend. */
#preview .page tr.colhead td {
  font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-tertiary);
  border-bottom: 1px solid var(--line-strong); padding-bottom: 4px;
}
#preview .page td.ccy { font-size: 9px; font-weight: 600; letter-spacing: 0.14em; color: var(--ink-tertiary); }

/* A summed cost line sets out its parts underneath it — each stone at its
   weight and price per carat. Set quieter and indented, so the line that
   belongs in the total still reads as the total. */
#preview .page tr.sub td {
  font-size: 11.5px; color: var(--ink-secondary, var(--ink-tertiary));
  border-bottom: 1px dotted var(--line); padding-top: 3px; padding-bottom: 3px;
}
#preview .page tr.sub td:first-child { padding-left: 18px; }

/* Baht figures with no rate to convert them — the USD column has stopped
   tracking and the margin below it is wrong. Printed, not merely on screen. */
.fxwarn-print {
  border: 1px solid var(--status-error); color: var(--status-error);
  padding: 8px 10px; font-size: 11px; line-height: 1.5; margin-top: 10px;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}

/* --- images on the sheet -------------------------------------------------
   Contained, never full-bleed, generous neutral margin — per brand/photography.md.
   Raw stone shots are not cropped to disguise their setting; the frame holds
   them at a fixed ratio instead. --------------------------------------- */
.imgsheet { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.imgsheet figure { break-inside: avoid; margin: 0; }
.imgframe {
  border: 1px solid var(--line-strong); background: var(--paper-sunk);
  aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center;
  padding: 6px; overflow: hidden;
}
.imgframe img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
/* A cropped main photo (invoice) fills the frame inside its 6px mat —
   applyFocus positions it absolutely, so it needs a box of its own. */
.imgframe .imgcrop { position: relative; align-self: stretch; flex: 1; overflow: hidden; }
.imgsheet figcaption { margin-top: 5px; display: flex; gap: 8px; align-items: baseline; }
.cap-slot {
  font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-tertiary); flex: none;
}
.cap-text { font-size: 12px; color: var(--ink); line-height: 1.4; }

/* --- sign-off -----------------------------------------------------------
   `margin-top: auto` — Joe, 17 Sep 2026: pinned to the BOTTOM of whatever
   sheet it lands on, just above the footer, same auto-margin trick `.foot`
   uses. `padding-top` keeps a minimum gap when the page is full; paginate.js
   counts it since only the margin gets zeroed for measurement. */
.signoff { margin-top: auto; padding-top: 16px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
.sigline { border-bottom: 1px solid var(--ink); height: 26px; }
/* Two signatures, not four across — the invoice's own signoff (sheet.js
   marks it `.signoff-roomy` whenever a doctype declares two captions or
   fewer). A 4-column QC/handover strip stays at the height above; two
   people actually signing a document get real room to do it. */
.signoff-roomy { gap: 48px; }
.signoff-roomy .sigline { height: 56px; }
.sigcap { font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-tertiary); margin-top: 4px; }
/* Bank details, below the signatures — client copy, bank transfer only
   (Joe, 25 Sep 2026; `bank` in companyBlocks, sheet.js). Built INSIDE the
   `.signoff` grid so pagination keeps the two together; it spans every
   track and closes the roomy 48px gap to 18px. Line pitches are the PDF's
   own (`drawBankBlock` in server/invoice-pdf.js) — keep the two in step. */
.signoff .bankpay { grid-column: 1 / -1; margin-top: -30px; }
.bankpay .section-label-row { margin-bottom: 8px; }
.bankpay .goods-sub { margin-top: 0; }
.bankpay-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px 48px; }
.bankpay-grid > * { min-width: 0; }
.bankpay-grid > .wide { grid-column: 1 / -1; }
.bankpay-label { font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-tertiary); line-height: 12px; }
.bankpay-value { font-size: 12.5px; font-weight: 400; color: var(--ink); line-height: 17px; white-space: pre-line; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.notesbox { border: 1px solid var(--line); height: 90px; }

/* ⚠ `margin-top: auto`, NOT a fixed value — 17 Sep 2026, round 2. `#preview
   .page` is a column flex box (above) specifically so its LAST child can be
   pushed all the way to the bottom of the sheet by an auto top margin,
   absorbing whatever space the content above it left over. A fixed
   `margin-top` only ever produced a small, constant gap after whatever
   content happened to precede it — correct on a full page, and wrong on a
   short one, where it read as the footer sitting "mid-page." `paginate.js`
   measures this element's own real height and reserves exactly that much of
   the page for it, so the page's usable content budget and where the footer
   actually lands never disagree. */
.foot {
  margin-top: auto; padding-top: 7px; border-top: 1px solid var(--line);
  display: flex; justify-content: space-between;
  font-size: 9px; letter-spacing: 0.10em; color: var(--ink-tertiary); text-transform: uppercase;
}
/* A signoff before the footer on this sheet takes the auto space itself;
   the footer falls back to a fixed gap so the two auto margins don't split
   the leftover space and float the signoff mid-page again. */
.signoff ~ .foot { margin-top: 14px; }

/* --- running identification (RETIRED FROM PRINT, 17 Sep 2026, round 2) -----
   Originally: a .page is a logical page of the sheet, and each one starts a
   fresh physical sheet — but a busy order (two centre stones, a long
   production list) could make logical page 1 run onto a second and third
   piece of paper, discovered only by the browser's own native print
   pagination. The .foot above is the last child of its block, so it printed
   only on the LAST of those physical sheets, leaving the earlier ones
   carrying stone data and costs with nothing to say which order they
   belonged to. `position: fixed` made this band repeat on every physical
   sheet regardless, as a safety net under that uncontrolled overflow.

   That is no longer how a physical sheet comes to exist. `paginate.js` now
   decides the page breaks itself, ahead of time, so every `.page` div IS one
   physical sheet with its own correct `.foot` already on it — the exact
   problem this band existed to cover cannot happen in the ordinary case any
   more. Kept in the DOM (`stampFooters()` in sheet.js still renders exactly
   one, and a test asserts that) but no longer painted in print: pinning
   `.foot` to the true bottom of the page (immediately above, "pin the footer
   to page bottom") puts it in the SAME few millimetres this band already
   occupied at `bottom: 0`, and the two were found overlapping — duplicated,
   run-together text — on a real printed PDF. A safety net that corrupts the
   common case it no longer needs to guard is worse than no net; the one rare
   case it still could catch (a single block too tall to split at all, the
   `console.warn` fallback in paginate.js) loses only this band's OWN
   duplicate identification, not `.foot`'s.

   Screen shows nothing — on screen the pages are visibly separate objects and
   the .foot is already at the end of each. ------------------------------- */
.runfoot { display: none; }

@media print {
  /* A scroll container on paper would clip. */
  .sheettablescroll { overflow: visible !important; }
  .sheettablescroll > table { min-width: 0 !important; }

  /* `.runfoot` is deliberately NOT re-enabled here — see the note above. */
}

/* --- watermark (master only) -------------------------------------------- */
.watermark {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0;
  display: flex; align-items: center; justify-content: center;
}
.watermark span {
  font-family: var(--font-display); font-size: 46px; font-weight: 600;
  letter-spacing: 0.20em; text-transform: uppercase;
  color: var(--status-error); opacity: 0.07; transform: rotate(-38deg); white-space: nowrap;
}
#preview .page > *:not(.watermark) { position: relative; z-index: 1; }

/* ------------------------------------------------------------------- print */
@media print {
  #preview .page {
    /* Explicit, not `auto`: iOS Safari's print pipeline was found to lay
       this out at the phone's on-screen viewport width rather than
       `@page`'s true 182mm content box, which restacked `.invhead`'s two
       halves (see the `flex-wrap: nowrap` override below) and made text
       wrap onto extra lines. An absolute length fixes both. See HISTORY.md,
       17 Sep 2026. */
    width: 182mm;
    /* 269mm — matches `PAGE_INNER_MM` in engine/paginate.js. */
    height: 269mm; margin: 0; border: 0; padding: 0;
    background: transparent;
    /* `avoid`, not `auto`: paginate.js has already done the ONLY
       page-breaking this document should ever get
       (`#preview .page + .page { break-before: page }`, right below), so a
       `.page` splitting itself further is always a bug, never a feature,
       on any engine. */
    break-inside: avoid;
  }
  #preview .page + .page { break-before: page; }
  .banner-master, #preview .page th, .stone, .cautions, .watermark span {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  /* Belt and braces beside the explicit width above: these two rows must
     never restack in print, even if a future change reintroduces an
     ambiguous width somewhere in the chain. Screen is untouched. */
  .invhead, .goodshead { flex-wrap: nowrap; }
}

/* --- primary photo on page 1 ------------------------------------------------
   Half the page, 4:5 portrait, beside the order and piece detail.
   Flex, not grid, deliberately: when the photo is hidden from the workshop copy
   it goes display:none and the text takes the full width by itself. One layout,
   two outputs, nothing to keep in step. */
.hero { display: flex; gap: 14px; margin-top: 13px; align-items: flex-start; break-inside: avoid; }
.hero-img { flex: 0 0 44%; }
.hero-meta { flex: 1 1 auto; min-width: 0; }
.hero-meta .block:first-child { margin-top: 0; }

/* --- seller / buyer, side by side ---------------------------------------
   The near-universal layout on a commercial invoice: seller and buyer at the
   same level, immediately under the header, before the goods — not two
   full-width blocks stacked one above the other (Joe, 5 Sep 2026: "lookup
   official templates online" before deciding this). Built only when the
   schema pairs a company block with a `client` section — today, only the
   invoice; every other doctype is untouched. See sheet.js.

   The Bill To grid still declares 4 columns — unchanged, because that number
   also has to work full-width on the doctypes that are NOT paired like this —
   but every field in it already spans 2 of those 4. Forcing the printed grid
   to 2 columns here needs no field touched: each one simply becomes the full
   width of its half-page column instead of sharing a row with a neighbour,
   which is what the tighter space actually needs. */
.partyrow { display: flex; gap: 14px; margin-top: 13px; align-items: flex-start; break-inside: avoid; }
.partyrow > .block { flex: 1 1 0; min-width: 0; margin-top: 0; }
.partyrow .grid { grid-template-columns: repeat(2, 1fr) !important; }

/* position: relative because applyFocus centres the image absolutely — the only
   way a quarter turn can be made to fill the frame without measuring it. */
.crop45 {
  aspect-ratio: 4 / 5; width: 100%; overflow: hidden; position: relative;
  background: var(--paper-sunk); border: 1px solid var(--line-strong);
}
.crop45 img { width: 100%; height: 100%; display: block; }

@media print {
  .crop45 { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* --- more than one centre stone -------------------------------------------- */
.stone-next { margin-top: 9px; }
.stone-no {
  font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-tertiary); margin-bottom: 7px;
}

/* --- invoice masthead (client-copy redesign, 16 Sep 2026) ------------------
   Replaces the generic .masthead/.mast-* rules above for invoice_commercial's
   page 1 only (`schema.sheet.clientMasthead` — see invoiceMasthead in
   sheet.js). Deliberately its own class names rather than a reuse of
   `.masthead` — the two are sized differently and this one must never drift
   the generic masthead every other doctype still renders. Seller identity
   and address on the left; invoice no. / date / terms / payment and the
   amount due, right-aligned, on the right — the mockup Joe approved. */
.invhead { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; padding-bottom: 10px; }
.invhead-seller { display: flex; flex-direction: column; gap: 10px; min-width: 200px; }
.invhead-seller-top { display: flex; gap: 12px; align-items: flex-start; }
.invhead-seller-top img { width: 38px; height: 38px; flex: none; margin-top: 2px; opacity: 0.9; }
.invhead-name {
  font-family: var(--font-display); font-size: 14px; letter-spacing: 0.12em; font-weight: 500;
  color: var(--ink); text-transform: uppercase;
}
.invhead-sub {
  font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-tertiary);
  font-weight: 600; margin-top: 3px;
}
.invhead-addr { font-size: 11px; color: var(--ink-tertiary); line-height: 1.6; }
.invhead-addr-name { font-size: 11px; color: var(--ink); font-weight: 500; margin-bottom: 2px; }

.invhead-docid { text-align: right; }
.invhead-title {
  font-family: var(--font-display); font-size: 22px; letter-spacing: 0.1em; font-weight: 500;
  color: var(--ink); text-transform: uppercase; margin: 0 0 8px;
}
.invhead-meta { font-size: 11px; }
.invhead-row { display: flex; justify-content: flex-end; gap: 12px; padding: 2px 0; }
.invhead-row .k { color: var(--ink-tertiary); }
.invhead-row .v { color: var(--ink); min-width: 100px; text-align: right; font-variant-numeric: tabular-nums; }
.invhead-due { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.invhead-due .k {
  font-family: var(--font-display); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-tertiary); font-weight: 500; align-self: center;
}
.invhead-due .v {
  font-family: var(--font-display); font-size: 16px; letter-spacing: 0.02em; color: var(--ink); font-weight: 500;
}

/* --- "The Goods — in full" — the client's own page 2 (16 Sep 2026) -------
   A 2-column grid of 4:5 frames, one per client-visible item photo, each
   captioned with the same detail line compactItemsTable prints beside its
   thumbnail on page 1 — see detailLine() in sheet.js. */
.goodshead { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; padding-bottom: 10px; }
.goodshead-left { display: flex; gap: 12px; align-items: flex-start; }
.goodshead-left img { width: 34px; height: 34px; flex: none; margin-top: 2px; opacity: 0.9; }
.goods-title {
  font-family: var(--font-display); font-size: 15px; letter-spacing: 0.1em; font-weight: 500;
  color: var(--ink); text-transform: uppercase;
}
.goods-sub { font-size: 10px; color: var(--ink-tertiary); margin-top: 4px; }
.goodshead-right { text-align: right; font-size: 11px; }
.goodshead-right .invhead-row { justify-content: flex-end; }

.goodsgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px 20px; margin-top: 14px; }
.goodsshot { break-inside: avoid; margin: 0; }
.goodsshot-frame {
  position: relative;   /* a cropped main photo is placed absolutely (applyFocus) */
  aspect-ratio: 4 / 5; background: var(--paper-sunk); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.goodsshot-frame img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.goodsshot figcaption { margin-top: 8px; }
.goodsshot-name { font-size: 12.5px; color: var(--ink); font-weight: 500; }
.goodsshot-spec { font-size: 11px; color: var(--ink-tertiary); margin-top: 2px; }

/* The same grid IN page 1's flow, between the totals and the signatures
   (24 Sep 2026 — goodsInFullBlock in sheet.js). The label row already
   gives the gap the page-top grid needed its own margin for. The signoff
   after it keeps its full `.signoff-roomy` height — paginate.js reserves
   it before placing a single photo — so a wet-ink signature always has
   its ~19mm of clear paper above the line, photos or not. */
/* SQUARE frames, auto-sized: the column count and track size are set
   inline by `shotGrid()` in sheet.js (1-3 photos at 55mm; 4 or more, four
   to a row at ~41.5mm; never over 55mm). `drawGoodsInFlow` in
   server/invoice-pdf.js applies the same rule and must change with it.
   Caption line pitch is the PDF's (15px), so the two agree on how tall a
   wrapped caption is. */
.goodsblock .goodsgrid { margin-top: 0; column-gap: 20px; }
.goodsblock .goodsshot-frame { aspect-ratio: 1 / 1; }
.goodsblock .goodsshot-name, .goodsblock .goodsshot-spec { line-height: 15px; }
.goodsblock .goodsshot-spec { margin-top: 0; }
.goodsblock .goods-sub { margin-top: 0; }

/* The phone reflow this masthead and goods grid used to get below 759px
   (stacked seller/doc-id, single-column goods grid) is gone — Joe, 17 Sep
   2026: A4 everywhere, pinch to zoom. See the long note above `#preview
   .page` for the reasoning; this file no longer has a narrow-screen version
   of these layouts at all, on purpose. */

/* --- the invoice's vertical pitch, matched to the PDF (25 Sep 2026) --------
   The client copy on screen (and in browser print) was ~50mm taller on page 1
   than the PDF of the same invoice (server/invoice-pdf.js). The type sizes
   agreed, but every line box here came from the page's 1.75 body
   line-height, plus padding the PDF does not have. So the PDF could fit the
   large photos above the signatures while the preview pushed them
   overleaf. Joe: "the large images on page 1 is not showing". Each value
   below is the PDF's own pitch for that line, read off drawMasthead,
   drawBillTo, drawItemsTable and drawPricingBlock. Change one side and you
   must change the other, or the preview stops predicting the PDF. Invoice
   blocks only: no other doctype uses these classes. */
.invhead { padding-bottom: 0; }
.invhead-seller-top img { margin-top: 0; }
.invhead-name { line-height: 20px; }
.invhead-sub { line-height: 16px; margin-top: 0; }
.invhead-addr, .invhead-addr-name { line-height: 17px; }
.invhead-addr-name { margin-bottom: 0; }
.invhead-title { line-height: 26px; margin: 0; }
.invhead .invhead-row { padding: 0; line-height: 15px; }
.invhead .invhead-due { margin-top: 4px; padding-top: 12px; line-height: 22px; }

.party-label { line-height: 12px; margin-bottom: 6px; }
.party-name { line-height: 19px; }
.party-line { margin-top: 0; line-height: 17px; }

.section-label-row { margin-bottom: 8px; }
.section-label { line-height: 12px; }
/* 26 Sep 2026 — Joe: the column headings sat almost on the rule under them.
   Now 10.5px in --ink-secondary with ~7px clear above the rule; the PDF's
   drawItemsTableHeader carries the same size, colour and gap. */
#preview .page table.compactitems thead th { line-height: 11px; padding-bottom: 6px; }
#preview .page table.compactitems td { padding-top: 6px; padding-bottom: 6px; }
#preview .page table.compactitems .item-name { line-height: 16px; }
#preview .page table.compactitems .item-spec { line-height: 13px; margin-top: 2px; }

.totals-wrap { margin-top: 0; }
.totals-row { padding: 0; line-height: 20px; }
.totals-due { margin-top: 4px; padding-top: 14px; line-height: 22px; }
#preview .page .totals-wrap ~ .settlenote { margin-top: 0; line-height: 15px; }
