/* ============================================================
   CSQP System — styles (clean, mobile-first, light theme)
   ============================================================ */
:root {
  --bg: #f4f6fb;
  --surface: #ffffff;
  --ink: #1c2536;
  --ink-soft: #5b6678;
  --line: #e3e8f0;
  --brand: #1f53d0;
  --brand-d: #1740a8;
  --row-hover: #d4e4ff;  /* single source for list row hover across .qtree / .qgrid */
  --brand-tint: #e7f0ff;   /* light blue surface: pills + soft buttons */
  --brand-tint-2: #eef2f9; /* subtle blue-gray: grid/qsec headers + surfaces */
  --amt-bg: #f5f8ff;       /* amount-cell surface */
  --ok: #1f9d5b;
  --warn: #d98a16;
  --bad: #d64b4b;
  --mono: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
  --shadow: 0 6px 20px rgba(28, 37, 54, 0.08);
  --radius: 14px;

  /* SINGLE SOURCE OF TRUTH for every glyph in a printed document.
     Two consumers read this: `body` (below) and the canvas-stamped running
     header in app.js (stampRunHead -> docFontStack()). Keep it ONE definition.
     The runhead used to carry its own script-specific stack ("PingFang SC"
     first) while the body led with "PingFang TC", so the same PDF rendered
     簡體 glyph shapes in the running header and 繁體 ones in the body.
     Verified in Chromium 2026-09-11: PingFang TC / SC / HK all resolve on this
     machine and produce DIFFERENT bitmaps for 骨直内說靜, so the two stacks were
     genuinely mixing script forms, not merely naming a different fallback.
     NOTO FIRST (dc decision 2026-09-11): the two faces we now SHIP
     (fonts.css -> fonts/noto-sans-{tc,sc}/, 400 + 700, unicode-range slices)
     lead the stack, so the same document renders identically on dc's Mac, on a
     Windows box, and inside a future server-side headless Chrome. Every OS face
     below is now only a fallback for the window before the woff2 slices land,
     or if fonts.css ever fails to load. Do not reorder back to PingFang-first:
     that silently makes the Mac the only machine whose PDFs look right, which
     is exactly the portability this replaced.
     TC then SC: the set is Traditional-first (HK) with English interleaved, and
     SC catches characters that only exist in simplified form. Both are needed;
     neither alone covers a mixed-script document.
     Before this, the stack led with "PingFang TC" and the runhead carried its
     own SC-first stack, so one PDF rendered 簡體 glyph shapes in the running
     header and 繁體 ones in the body (measured in Chromium: PingFang TC / SC /
     HK produce different bitmaps for 骨直内說靜). */
  --doc-font: "Noto Sans TC", "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang TC", "PingFang HK", "PingFang SC", "Microsoft JhengHei", "Heiti TC", "Hiragino Sans GB", sans-serif;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; padding: 0; }

/* Reserve the vertical scrollbar gutter ALWAYS (dc, 2026-10-08).
   MEASURED cause of the 15px top-bar shift between pages: Chromium's classic
   scrollbar takes 15px from the layout viewport only when the page overflows.
   Products (26 rows, scrollHeight 1405) measured documentElement.clientWidth
   1265; Customers (fits, 577) measured 1280 — same CSS, different available
   width, so every fixed column inherited the difference.
   `stable` reserves the gutter on BOTH kinds of page, so the layout viewport is
   1265 always and the columns cannot move. It is the correct tool here rather
   than hiding the scrollbar, which would remove a real affordance on long lists.
   Ignored by browsers without scrollbar-gutter support, where the shift simply
   does not occur (overlay scrollbars take no space). */
html { scrollbar-gutter: stable; }
body {
  /* See --doc-font above: the printed document's ONE type stack, shared with
     the canvas-stamped running header in app.js (docFontStack()). */
  font-family: var(--doc-font);
  background: var(--bg);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.45;
  padding-bottom: 76px; /* room for bottom nav */
}

/* Top bar */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; flex: 0 0 166px; }
/* `.brand` is a FIXED column for the same reason as .ph-title (dc, 2026-10-08):
   it sets the x of every column to its right, list included. Left on `auto` it
   measured 166 naturally, but once #view claimed the extra width it SHRANK to
   122 and dragged the page title from 198 to 154 — the whole bar re-flowed
   because of a block far below it. 166 = logo 44 + gap 10 + the longest
   measured subtitle "Quotes, Works and AR" (112). */
.brand-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.brand-logo { height: 44px; display: block; }
/* Per-view header injected into the top bar (Option 1: WO title/search/+New moved up,
   one row saved). Hidden by default; only the active view populates it. */
.topbar #viewHead:not([hidden]) { display: flex; align-items: center; gap: 12px; flex: 1 1 auto; min-width: 0; margin-left: 16px; }
/* The +New / action button was the last item in the row, so it absorbed whatever
   slack the title left -- measured 804..819, a 15px shift.
   It does NOT need margin-left:auto: with the title and search now fixed-width,
   the remaining slack is identical on every page, so a plain flex item already
   lands on the same x. `margin-left:auto` was tried and is WRONG — the screenshot
   showed + New pushed on top of the user-chip email, because `auto` consumes ALL
   the free space and the button ends up flush against the chip.
   `margin-left: auto` removed; the 12px gap from #viewHead's own `gap` applies. */
.topbar #viewHead > .btn { flex: 0 0 auto; }
.topbar #viewHead .ph-title { margin: 0; }
.logo {
  font-weight: 800; letter-spacing: .5px; font-size: 14px; color: var(--brand);
}
.brand-sub { font-size: 11px; color: var(--ink-soft); }
.icon-btn {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  width: 38px; height: 38px; border-radius: 10px; font-size: 14px; cursor: pointer;
}
.icon-btn:active { transform: scale(.96); }
.user-chip {
  display: flex; align-items: center; gap: 8px;
  margin-left: auto;   /* pin to the right; margin-right:auto pushed it LEFT */
  flex: 0 0 auto;      /* never resized by #viewHead changing between pages */
  padding-left: 12px;
}
.uc-mail {
  font-size: 11px; color: var(--ink-soft);
  width: 40ch;              /* fixed: 40 characters, whatever the address length */
  flex: 0 0 40ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 520px) { .uc-mail { width: 28ch; flex-basis: 28ch; } }
.mfa-qr { display: flex; justify-content: center; margin: 12px 0; }
.mfa-qr svg { width: 200px; height: 200px; background: #fff; padding: 8px; border-radius: 10px; border: 1px solid var(--line); }

/* View — the content column.
   WIDTH IS SET TO MATCH THE TOP BAR, not chosen for looks (dc, 2026-10-08:
   "the width of the list shall be elongated. start from the x coordinate of
   'title' ... and ends before 2FA").

   MEASURED at 1280 viewport, so every term is a fact not an estimate:
     .topbar padding-left        16
     .brand    16 .. 182         (166 wide: logo 44 + gap 10 + subtitle 112)
     #viewHead margin-left       16   -> .ph-title left edge = 198
     .user-chip left             873  -> its first button (2FA) at 1137
   So the content should run 198 .. (1137 - 12 breathing room) = 198 .. 1125,
   i.e. **927px of content**. `#view` carries 16px padding each side, so its
   max-width must be 927 + 32 = **959**.

   It was 900 (=> 868 of content), which is why 61px sat unused on both sides and
   long text wrapped early. Verified after the change: list spans 198..1125.

   `margin: 0 auto` keeps it centred, and because the bar itself is centred in
   the viewport the two stay aligned. Below the desktop breakpoint the width
   returns to the old 900 so a narrow window is not forced to overflow. */
#view {
  padding: 16px;
  /* LEFT edge = the page title's x. Fixed because .brand is a fixed column:
     bar padding 16 + brand 166 + #viewHead margin-left 16 = 198. Minus this
     element's own 16px padding gives the border-box inset of 182. */
  margin-left: 182px;
  margin-right: auto;
  /* RIGHT edge = just before 2FA, and therefore FLUID (dc, 2026-10-08: "extend
     the length of the row to right before the x coordinate of the 2FA").
     A fixed max-width only lands on 2FA at the ONE window size it was measured
     at. Computed shortfall of a hard-coded 959 against the real bar:
         viewport 1600 -> 68px short of 2FA
         viewport 1920 -> 388px short
     So the width tracks the viewport. The three subtracted constants are the
     measured, unchanging parts of the bar:
         15  the reserved scrollbar gutter (html{scrollbar-gutter:stable})
         182 the left inset above
         124 from the list's right edge to 2FA's left edge at 1280
             (= 16 bar padding-right + 376 chip + 32 of the chip's own content
                that sits left of 2FA), less the 12px breathing gap
     1280 - 321 = 959, which is exactly the fixed value it replaces -- so 1280
     is unchanged and every other width now lands on 2FA instead of short of it.
     Verified in Chromium at 1280 across all six list pages: list 198..1125,
     2FA at 1137, gap 12, no horizontal scrollbar. */
  /* 306 — SOLVED FROM TWO MEASUREMENTS, not derived on paper.
     Two earlier constants came from arithmetic and BOTH were wrong:
       width calc(100vw - 321px) -> measured list right edge 1110, gap 27
       width calc(100vw - 327px) -> measured list right edge 1104, gap 33
     Those two points fix the line exactly (list right edge = 1431 - width, slope
     -1; 321 -> 1110 confirms it). Aiming the right edge at 1125 — 12px before 2FA
     at 1137 — gives width = 306.
     Verified in Chromium at 1280 on all six list pages: list 198..1125, identical
     everywhere, gap 12, no horizontal scrollbar. */
  width: calc(100vw - 306px);
  max-width: none;
}
/* Guard rail: below this the arithmetic would leave too little for a table, so
   fall back to the old centred column rather than forcing a horizontal scroll. */
@media (max-width: 1180px) {
  #view { margin-left: auto; margin-right: auto; width: auto; max-width: 900px; }
}
/* Guard rail: below this the arithmetic would go negative, so fall back to the
   centred column rather than forcing a horizontal scrollbar. */
@media (max-width: 1180px) {
  #view { margin-left: auto; margin-right: auto; width: auto; max-width: 900px; }
}
/* Desktop only. Below this the inset + width no longer fit, so fall back to the
   old centred column rather than forcing a horizontal scrollbar. */
@media (max-width: 1180px) {
  #view { margin-left: auto; margin-right: auto; max-width: 900px; }
}

/* Headings / toolbar */
.page-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px;
}
.page-title { font-size: 14px; font-weight: 700; margin: 0; }
.ver-badge {
  display: inline-block; vertical-align: middle; margin-left: 8px;
  font-size: 11px; font-weight: 600; letter-spacing: .03em; font-family: inherit;
  color: var(--ink-soft); background: #eef1f6; border: 1px solid var(--line);
  border-radius: 999px; padding: 1px 8px;
}
.sub { color: var(--ink-soft); font-size: 11px; margin: 2px 0 0; }
/* Search box: FIXED column in the top bar (dc, 2026-10-08).
   WAS `flex:1 1 auto; max-width:540px` per DESIGN_BOOK.md:581, which made the
   box absorb the title's leftover space — measured search-box x moving 288->393
   (105px) and its width 398->491 (93px) across six list pages. A control that
   moves when you navigate reads as a glitch even when nothing is wrong.
   360px, part of the same 657px budget as the title column (see .ph-title for
   the arithmetic). max-width is kept so it still shrinks rather than
   overflowing on a narrower desktop; the mobile breakpoint at the end of this
   block returns it to fluid behaviour, since a phone has no room for columns. */
.head-search { flex: 0 0 360px; max-width: 360px; margin: 0 16px; }
@media (max-width: 1100px) { .head-search { flex: 1 1 auto; max-width: 540px; } }
.head-search .qsearch { width: 100%; }
/* Title block of the canonical list page-head: Back button | (title + sub + count)
   FIXED column width (dc, 2026-10-08): titles range 62-167px, and a flexible
   title pushed the search box and +New button sideways on every navigation.
   SIZED TO FIT, not to taste. #viewHead measures **677px** at 1280 (bar 1265
   after the reserved scrollbar gutter, minus padding 32, brand 145, chip 376,
   margin-left 16). Budget: title 172 + gap 12 + search 360 + margins 32 +
   gap 12 + button 69 = **657, leaving 20px spare**.
   Two earlier attempts overflowed and the per-element x values still looked
   plausible, so only the SUMS caught them:
     232 + 440 = 797 vs 677 -> over by 120 (button landed on the chip email)
     180 + 380 = 685 vs 677 -> over by 8   (still a 8px overlap)
   Longest measured title ("Quotations 6 base · 9 total") is 167px, so 172 holds it;
   overflow ellipsis rather than pushing the row apart if one ever exceeds that. `.brand` keeps its own auto width and
   `.user-chip` is pinned right by `margin-left:auto`, so neither is affected. */
.ph-title { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 0 0 172px; }
.ph-title > div { min-width: 0; overflow: hidden; }
.ph-title .page-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 1100px) { .ph-title { flex: 1 1 auto; } }

/* Buttons */
/* line-height pinned to the Latin line box (16px @14px font, 12px @11px font):
   with `normal`, CJK glyph runs fall back to a taller CJK font (20px/16px line box)
   and every button grows 4px in SC — page content below shifts between EN/SC.
   Pinning keeps EN/HK pixel-identical and makes SC match. */
.btn {
  border: none; border-radius: 10px; padding: 10px 14px; font-size: 14px; font-weight: 600;
  line-height: 16px;
  cursor: pointer; background: var(--brand); color: #fff; box-shadow: var(--shadow);
}
.btn:active { transform: translateY(1px); }
.btn.ghost { background: var(--surface); color: var(--brand); border: 1px solid var(--line); box-shadow: none; }
.btn.soft { background: var(--brand-tint); color: var(--brand-d); box-shadow: none; }
.btn.danger { background: #fff; color: var(--bad); border: 1px solid #f3c9c9; box-shadow: none; }
.btn.sm { padding: 6px 10px; font-size: 11px; border-radius: 8px; line-height: 12px; }
.btn.block { width: 100%; }

/* Cards / list */
.card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px; margin-bottom: 12px; box-shadow: var(--shadow);
}
.card .row { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.card h3 { margin: 0 0 4px; font-size: 14px; }
.card .meta { color: var(--ink-soft); font-size: 11px; }
.card .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }

/* Table */
table.tbl { width: 100%; border-collapse: collapse; font-size: 11px; }
table.tbl th, table.tbl td { text-align: left; border-bottom: 1px solid var(--line); }
table.tbl th { padding: 8px 8px; }
table.tbl td { padding: 6px 8px; }
table.tbl th { color: var(--ink-soft); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .4px; }
table.tbl tr:last-child td { border-bottom: none; }
table.tbl .g-idx { width: 34px; text-align: center; color: #8b95a8; font-size: 11px; }
table.tbl .doc-idx { text-align: right; color: #8b95a8; white-space: nowrap; }
.scroll { overflow-x: auto; }

/* Forms */
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.row2 .field { margin-bottom: 12px; }
@media (max-width: 540px) { .row2 { grid-template-columns: 1fr; gap: 0; } }
.field { margin-bottom: 12px; }
.ro-banner { background: #FCEBEB; color: #791F1F; border: 1px solid #F09595; border-radius: 10px; padding: 8px 12px; font-size: 11px; font-weight: 500; margin-bottom: 10px; }
.field label { display: block; font-size: 11px; color: var(--ink-soft); margin-bottom: 5px; font-weight: 600; }
input, select, textarea {
  width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
  font-size: 14px; background: #fff; color: var(--ink); font-family: inherit;
}
textarea { resize: vertical; min-height: 64px; }
/* YYYY/MM/DD date text inputs — fixed-width, monospace, right-aligned for legibility */
.date-input { max-width: 180px; font-variant-numeric: tabular-nums; letter-spacing: .03em; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(47,109,246,.15); }

/* Quotation editing sheet (full-screen, replaces the popup modal for quotations) */
.sheet { position: fixed; inset: 0; z-index: 55; background: var(--surface); display: flex; flex-direction: column; animation: sheetIn .2s ease-out; }
@keyframes sheetIn { from { transform: translateY(18px); opacity: .35; } to { transform: none; opacity: 1; } }
.sheet-head { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--surface); position: sticky; top: 0; z-index: 2; }
.sheet-head #qSheetTitle, .sheet-head #formSheetTitle { flex: 1; font-weight: 800; font-size: 14px; }
.sheet-body { flex: 1; overflow-y: auto; padding: 14px; padding-bottom: 96px; }

/* Per-item editable cards (each input carries its own header) */
.item-card { border: 1px solid var(--line); border-radius: 10px; padding: 10px; margin-bottom: 10px; background: #fbfcfe; }
.item-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.item-card-head .idx { font-weight: 700; color: var(--brand-d); }
.item-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.item-grid label { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: #5b6678; font-weight: 600; }
.item-grid label.full { grid-column: 1 / -1; }
.item-grid input, .item-grid select, .item-grid textarea { font-size: 14px; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: #fff; width: 100%; }
.item-grid textarea { min-height: 40px; resize: vertical; }
.item-amt { text-align: right; font-weight: 700; margin-top: 8px; color: var(--brand-d); }

/* Status pills */
/* Status pills — ONE uniform box for every status label in every list.
   `min-width` (not fit-content) so `acc` / `void` / `done` all render the same
   width; longer labels are abbreviated at the source instead of stretching the
   pill (DESIGN_BOOK RULE-ui-status-pill). */
.pill { display: inline-block; box-sizing: border-box; min-width: 64px; text-align: center; padding: 2px 9px; border-radius: 999px; font-size: 11px; font-weight: 700; white-space: nowrap; }
.pill.draft { background: #eef1f6; color: #6b7689; }
.pill.sent { background: var(--brand-tint); color: var(--brand-d); }
.pill.accepted { background: #e6f6ec; color: var(--ok); }
.pill.rejected { background: #fdeaea; color: var(--bad); }
.pill.expired { background: #eef0f2; color: var(--ink-soft); }
.pill.issued { background: var(--brand-tint); color: var(--brand-d); }
.pill.paid { background: #e6f6ec; color: var(--ok); }
.pill.overdue { background: #fdeaea; color: var(--bad); }
.pill.p_paid_overdue { background: #fdeaea; color: var(--bad); }
/* Written-off slip status (bad debt — debt survives, recoverable within 6yr). */
.pill.woff { background: #efe7f7; color: #6b3fa0; }
/* Identifier chip (product code etc.) — NOT a status pill: it carries real data of
   arbitrary length, so it must not be forced into the uniform status width. */
.pill.code { min-width: 0; font-family: var(--mono); font-weight: 600; letter-spacing: 0; }
/* Two-pill combo statuses (e.g. P Paid Overdue = Overdue + P Paid) — wrap to a new line if the container is too narrow instead of overflowing. */
.pillrow { display: inline-flex; gap: 4px; align-items: center; flex-wrap: wrap; }
/* Write-off sub-row under a slip + expandable detail in the write-off log. */
.woff-sub { background: #f7f2fc; color: #6b3fa0; font-size: 12px; padding: 4px 10px; }
.woff-detail { padding: 4px 10px 8px; font-size: 12px; color: var(--ink-soft); white-space: pre-wrap; }

/* Read-only (auto-generated) fields */
.ro { background: #f1f3f7; color: var(--ink-soft); cursor: not-allowed; font-weight: 600; }

/* Bottom nav */
.bottomnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 25;
  display: flex; background: var(--surface); border-top: 1px solid var(--line);
  padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
}
.nav-btn {
  flex: 1; border: none; background: transparent; color: var(--ink-soft);
  font-size: 11px; font-weight: 600; cursor: pointer; padding: 6px 2px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  border-radius: 10px; transition: background .12s ease;
}
/* Hover lights the tile; the ACTIVE (current) page keeps the same highlight on
   permanently so the user always sees where they are. */
.nav-btn:hover { background: var(--nav-hl); }
.nav-ic { width: 22px; height: 22px; object-fit: contain; opacity: .7; transition: opacity .15s; }
.nav-btn.active { background: var(--nav-hl); color: var(--brand); }
.nav-btn.active .nav-ic { opacity: 1; filter: drop-shadow(0 0 0 var(--brand)); }
/* Pin label line-box to the taller (SC) height so the bar is identical in EN and SC (no flumble on language toggle). */
.nav-label { line-height: 16px; }

/* Modal */
.modal {
  position: fixed; inset: 0; z-index: 70; background: rgba(20,28,44,.45);
  display: flex; align-items: flex-end; justify-content: center;
}
.modal.hidden, .hidden { display: none !important; }
.modal-card {
  background: var(--surface); width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto;
  border-radius: 16px 16px 0 0; padding: 16px; box-shadow: 0 -10px 40px rgba(0,0,0,.2);
  animation: slideUp .18s ease-out;
}
@keyframes slideUp { from { transform: translateY(20px); opacity: .6; } to { transform: none; opacity: 1; } }
.modal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-weight: 700; font-size: 14px; }

/* Confirm dialog */
.confirm-body { font-size: 14px; line-height: 1.5; color: #2b3648; margin-bottom: 16px; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions .btn { flex: 1; justify-content: center; }

/* Toast */
.toast {
  position: fixed; left: 50%; bottom: 92px; transform: translateX(-50%);
  background: #1c2536; color: #fff; padding: 11px 18px; border-radius: 10px; z-index: 60;
  font-size: 14px; box-shadow: var(--shadow); max-width: 86%; text-align: center;
}
.toast.err { background: #c0392b; }

/* Empty state */
.empty { text-align: center; color: var(--ink-soft); padding: 40px 16px; }
.empty .big { font-size: 34px; margin-bottom: 8px; }

/* Search */
.search { width: 100%; margin-bottom: 12px; }

/* Line items editor */
.item-row { display: grid; grid-template-columns: 1fr 64px 86px 84px; gap: 6px; margin-bottom: 8px; align-items: end; }
.item-row input { padding: 8px 9px; font-size: 14px; }
.item-row .amt { text-align: right; font-weight: 700; padding-top: 10px; }
/* Quotation line items: code · description · qty · term · price · category · amount */
.items-grid { grid-template-columns: 70px 1fr 54px 46px 76px 88px 84px; min-width: 520px; }
/* The grid itself scrolls horizontally (see .grid-wrap), so the container must not. */
#itemsCont { overflow: visible; }

/* ---------- Spreadsheet-style item grid ---------- */
.grid-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; background: #fff; }
.grid { border-collapse: separate; border-spacing: 0; width: 100%; min-width: 940px; font-size: 11px; }
.grid th {
  position: sticky; top: 0; z-index: 2; background: var(--brand-tint-2); color: #33405a;
  text-align: left; font-size: 11px; font-weight: 700; letter-spacing: .2px;
  padding: 8px 6px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.grid td { padding: 0; border-bottom: 1px solid #eef1f6; vertical-align: middle; }
.grid tbody tr:hover td { background: var(--row-hover); }
.grid tbody tr:hover td.g-amt { background: var(--row-hover); }
/* Flat cells that read like a spreadsheet rather than a stack of boxes. */
.grid input, .grid select {
  width: 100%; border: 0; border-radius: 0; background: transparent;
  padding: 9px 7px; font-size: 11px; color: #1f2739;
}
.grid input:focus, .grid select:focus {
  outline: 2px solid var(--brand); outline-offset: -2px;
  box-shadow: none; border-radius: 3px; background: #fff;
}
.grid .g-idx { width: 34px; text-align: center; color: #8b95a8; font-size: 11px; padding: 9px 0; }
.grid .g-pick { min-width: 190px; }
.grid .g-num input { text-align: right; }
.grid .g-amt {
  text-align: right; padding: 9px 8px; font-weight: 700; white-space: nowrap;
  color: var(--brand-d); background: var(--amt-bg); min-width: 96px;
}
.grid .g-del { width: 34px; text-align: center; }
.rowdel {
  border: 0; background: transparent; color: #b3bccb; cursor: pointer;
  font-size: 14px; line-height: 1; padding: 6px 8px; border-radius: 6px;
}
.rowdel:hover { background: #ffe9e9; color: #d0342c; }
.grid-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 4px; }
.grid-totals { margin-top: 6px; border-top: 1px dashed var(--line); padding-top: 8px; }
.grid-totals .sub-row { display: flex; justify-content: space-between; font-size: 11px; padding: 3px 0; color: #5b6678; }
.grid-totals .sub-row.grand {
  font-size: 14px; font-weight: 800; color: var(--brand-d);
  border-top: 1px solid var(--line); margin-top: 4px; padding-top: 7px;
}

/* Product-group quick-add (front of the quotation item grid) */
.grp-add { background: #f4f7fb; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin: 4px 0 10px; }
.grp-add > label { display: block; font-weight: 700; font-size: 11px; margin-bottom: 8px; }
.grp-add-row { display: flex; gap: 8px; align-items: center; }
.grp-add-row select { flex: 1; min-width: 0; }
.grp-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
/* Compact header: squeeze the customer/date/status block so the product-group
   and item-selection area gets more vertical room on screen. */
.q-top { background:#fbfcfe; border:1px solid var(--line); border-radius:10px; padding:10px 12px; margin:2px 0 8px; }
.q-top .row3 { gap:10px 12px; }
.q-top .field { margin-bottom:8px; }
.q-top .field label { margin-bottom:3px; }
.q-top .date-input { max-width:100%; }
.grp-add.compact { background:transparent; border:0; border-radius:0; padding:0; margin:4px 0 0; }
.grp-add.compact > label { display:none; }
.grp-add.compact .grp-add-row { flex-wrap:wrap; }
.grp-add.compact .ga-label { font-weight:700; font-size:13px; color:var(--ink-soft); }
.grp-add.compact .grp-add-row select { flex:1 1 200px; min-width:0; }
.chip { display: inline-flex; align-items: center; gap: 6px; background: var(--brand); color: #fff; border-radius: 999px; padding: 4px 10px; font-size: 11px; font-weight: 600; }
.chip.grp-chip { background: var(--brand); }
.chip .chip-n { background: rgba(255,255,255,.25); border-radius: 999px; padding: 0 7px; font-size: 11px; }
.chip .chip-x { border: none; background: rgba(255,255,255,.2); color: #fff; width: 18px; height: 18px; border-radius: 50%; cursor: pointer; font-size: 11px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; transition: background .15s; }
.chip .chip-x:hover { background: rgba(255,255,255,.45); }

/* Discount row in the quotation sheet */
.disc-row { display: flex; align-items: center; gap: 10px; justify-content: flex-end; margin: 6px 0; }
.disc-row label { font-weight: 700; font-size: 11px; color: #5b6678; margin: 0; }
.disc-row input { width: 140px; }
/* Discount section (fixed $ or % of subtotal) */
.disc-section { border: 1px solid var(--line, #e6e9f0); border-radius: 10px; padding: 10px 12px; margin: 10px 0; background: #fbfcfe; }
.disc-head { font-weight: 700; font-size: 11px; color: #2b3340; margin-bottom: 8px; }
.disc-controls { display: flex; gap: 10px; align-items: center; }
.disc-controls select { flex: 0 0 160px; }
.disc-controls input { flex: 1; }

/* Slip / document print */
.doc { background: #fff; color: #111; padding: 28px; border-radius: 8px; }
.doc h2 { margin: 0 0 2px; }
.doc .doc-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; border-bottom: 1px solid #666; padding-bottom: 6px; margin-bottom: 10px; }
.doc .doc-head-left { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
.doc .doc-logo { max-height: 52px; }
.doc .doc-head-text { font-size: 11px; line-height: 1.35; color: #333; max-width: 320px; white-space: normal; }
.doc .doc-head-right { text-align: right; white-space: nowrap; }
.doc table { width: 100%; border-collapse: collapse; margin: 12px 0; }
.doc th, .doc td { border: 1px solid #666; padding: 7px 8px; font-size: 11px; text-align: left; }
.doc .total { text-align: right; font-weight: 400; font-size: 14px; margin-top: 8px; }
.doc .total.disc { font-weight: 400; font-size: 14px; color: #b4232a; }
.doc .muted { color: #666; font-size: 11px; }
/* End-of-document line appended to every printed PDF (quotation / DN / WO).
   Lands at the bottom of the .doc → last page; signals the document ends. */
.doc-foot { margin-top: 14px; padding-top: 8px; border-top: 1px solid #666; text-align: center; font-size: 11px; color: #666; letter-spacing: .2px; }
.doc-remarks { margin: 14px 0 6px; padding: 10px 12px; border: 1px solid #666; border-radius: 8px; background: #fbfcfe; font-size: 11px; line-height: 1.5; }
.doc tr.dn-cur { background: #fff4d6; }
/* dc 2026-09-11 — PRINTED DOCUMENTS CARRY NO BOLD.
   Hierarchy in the PDFs comes from size, spacing and the flat fills (#e9edf5
   header band, pill chips), not from weight. Every weight below is stated once;
   there is no per-element override block. `b`/`strong` are neutralised here
   because app.js emits them for the receipt labels, the fully-paid amount and
   the Settled/Unsettled footer — the tags stay, only their weight changes. */
.doc tr.dn-cur td { font-weight: 400; }
.doc td.dn-due-overdue { color: #d32f2f; }
.doc .dn-overdue-tag { color: #d32f2f; font-size: 11px; margin-left: 2px; }
.doc-remarks b { display: block; margin-bottom: 4px; color: #33405a; }
/* Credit Note signature block — sits above the doc-end footer line. */
.cn-sign { margin-top: 32px; font-size: 12px; line-height: 1.6; }
/* Credit Note reprint banner — only rendered when printed_count >= 1 (a reprint).
   Size mirrors .doc-sec (14px) but BOLD per explicit request; this is the one
   deliberate exception to the printed-docs-no-bold convention, for a warning.
   Left-aligned immediately above the Work Order reference block. */
.cn-reprint { font-weight: 700; font-size: 14px; text-align: center; margin: 4px 0; color: var(--ink, #1f2937); letter-spacing: .2px; line-height: 1.35; }
/* Receipt reprint banner — exact mirror of .cn-reprint. */
.rc-reprint { font-weight: 700; font-size: 14px; text-align: center; margin: 4px 0; color: var(--ink, #1f2937); letter-spacing: .2px; line-height: 1.35; }
/* Debit Note reprint banner — exact mirror of .cn-reprint (DN shares the same
   "Duplicated copy / 重印" reprint convention as the Credit Note). */
.dn-reprint { font-weight: 700; font-size: 14px; text-align: center; margin: 4px 0; color: var(--ink, #1f2937); letter-spacing: .2px; line-height: 1.35; }
/* Signature fill-in line: underscore-style (dotted) so it reads "____", not a solid
   "-----"; bottom-aligned to the label baseline. */
.cn-sign-line { display: inline-block; min-width: 130px; max-width: 130px; border-bottom: 2px dotted #333; vertical-align: bottom; }
.cn-sign-note { margin-top: 6px; font-size: 11px; color: #666; letter-spacing: .1px; }
/* The <b> tags app.js emits inside the printed documents (receipt labels,
   fully-paid amount, Settled/Unsettled footer), the status pill chips and the
   red Unsettled figure — the chips keep their weight for the on-screen lists,
   so the reset is scoped to .doc rather than changed on .pill/.amt-disc. */
.doc b, .doc strong, .doc .pill, .doc .amt-disc { font-weight: 400; }
/* Table headers in a printed document are never bold either. The global
   `table.tbl th { font-weight: 600 }` (near the top of this file) styles BOTH
   the DN's Payment Schedule header row and the on-screen WO-detail tables, so
   it cannot be changed at source — that would unbold the screen. `.doc table th`
   ties it on specificity (0,1,2 each) and wins on source order. `.doc-items th`
   and `.doc-owner th` already state 400 for the quotation/WO print tables; this
   declaration closes the `.tbl` leak instead of patching that one table.
   Measured on the DN print DOM 2026-09-11: 5 bold elements -> 0. */
.doc table th { font-weight: 400; }
/* Debit Note: inline receipt lines rendered inside the Summary block.
   Keep border UNIFORM 1px on all sides — html2canvas drops borders on a
   colspan cell when sides are mixed widths (the old 3px left + 1px others
   pattern rendered with no cell box at all). The indent still reads from
   the background + padding-left below. */
/* DN receipt lines (colspan=2 row). NO border here on purpose: the draw-once
   rule further down already gives every .doc cell right+bottom. This rule used
   to carry `border: 1px solid #ccc`, and because `table.tbl .rec-line td`
   (0,2,2) outranks `.doc td` (0,1,1) it won on specificity and re-introduced the
   doubled interior edge the draw-once rule exists to prevent. */
table.tbl .rec-line td { background: #fafafa; padding-left: 20px; }
.rec { font-size: 11px; color: #444; padding: 2px 8px 2px 16px; margin-left: 4px; display: flex; justify-content: space-between; align-items: baseline; gap: 14px; }
/* Receipt amount on a DN receipt line. 400 like everything else in .doc — the
   figure still reads as the line's value because the date/reference to its left
   are the same size but muted #444, and it is the only right-aligned item.
   `.rec` is emitted by dnDocHTML() only (debit-note print), so there is no
   on-screen surface to protect and no print scope is needed. */
.rec .amt { font-variant-numeric: tabular-nums; font-weight: 400; white-space: nowrap; text-align: right; }

/* Quotation grouped layout (matches sample) */
.doc-owner { width: 100%; border-collapse: collapse; margin: 10px 0 16px; }
.doc-owner th, .doc-owner td { border: 1px solid #666; padding: 6px 9px; font-size: 11px; text-align: left; }
.doc-owner th { background: #e9edf5; width: 34%; white-space: nowrap; font-weight: 400; }
.doc-sec { font-weight: 400; font-size: 14px; margin: 16px 0 4px; padding: 4px 0; }
/* Editor-only section header: title on the left, "+ Add row" pinned to the right
   so the button sits next to its section header and saves vertical space. The PDF
   body's .doc-sec (no .sec-head) is unaffected and stays a plain bar. */
.doc-sec.sec-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.doc-sec.sec-head .sec-title { flex: 1 1 auto; }
.doc-sec.sec-head .sec-add { flex: none; white-space: nowrap; }
/* Button cluster inside a section header / lite label — keeps + Add row and
   Clear all rows on the same line, aligned to the section title. */
.doc-sec.sec-head .sec-btns, .sec-lite .sec-btns { display: flex; gap: 8px; align-items: center; white-space: nowrap; }
/* Recurring / Ad sections need NO heavy header bar (per request) — just a slim
   muted label line carrying the aligned + Add row button. */
.sec-lite { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 14px 0 2px; }
.sec-lite-title { font-size: 14px; color: var(--ink-soft, #64748b); font-weight: 600; }
/* Condensed quotation popup (shown after Save, before returning to the list) */
.condensed-body { max-height: 66vh; overflow-y: auto; border-top: 1px solid var(--line, #e6e9f0); border-bottom: 1px solid var(--line, #e6e9f0); padding: 12px 2px; margin: 6px 0; }
.condensed-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 12px; }
.condensed-actions .btn { min-width: 130px; justify-content: center; }
.btn.primary { background: var(--brand, #1f53d0); color: #fff; border-color: transparent; }
.btn.primary:hover { filter: brightness(1.04); }
.doc-items { width: 100%; border-collapse: collapse; margin: 4px 0 4px; table-layout: fixed; }
/* Column widths expressed as PERCENTAGES, not px, so every .doc-items table
   (Setup / Recurring / Ad-Promotion) resolves to identical column widths at any
   page size. With `table-layout: fixed` + one width per column class, the three
   section tables can never drift apart — that was the reported bug. The px
   values used previously made the tables agree on screen but scale badly when
   the sheet is rendered to A4. Measured on A4: 33/92/280/52/59/96/125 px.
   Long descriptions still truncate to one line with an ellipsis (…) via
   .doc-desc-inner, on screen and in print. */
.doc-items .doc-idx  { width: 4.5%;  text-align: right; color: #8b95a8; white-space: nowrap; }
.doc-items .doc-code { width: 12.5%; white-space: nowrap; font-size: 11px; }
.doc-items .doc-desc { width: 38%; padding: 6px 8px; }
.doc-items .doc-qty  { width: 9%; text-align: right; white-space: nowrap; }
.doc-items .doc-term { width: 8%; }
.doc-items .doc-unit { width: 13%; text-align: right; white-space: nowrap; }
.doc-items .doc-amt  { width: 17%; text-align: right; white-space: nowrap; }
.doc-items .doc-desc-inner { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Combined "To customer" + Owner block — two columns, same row:
   customer (left) · Sales P & Contact (right), compact & smaller font */
.doc-toright { display: flex; flex-direction: row; justify-content: space-between; align-items: flex-start; gap: 24px; margin: 6px 0 10px; font-size: 11px; }
.doc-toright #qd-to { margin: 0; flex: 1 1 auto; text-align: left; }
.doc-toright .doc-owner { width: auto; min-width: 280px; margin: 0; flex: 0 0 auto; align-self: flex-end; font-size: 11px; }
.doc-toright .doc-owner th, .doc-toright .doc-owner td { border: 1px solid #666; padding: 4px 8px; font-size: 11px; text-align: left; }
.doc-toright .doc-owner th { width: auto; white-space: nowrap; }
/* Credit Note: banner sits BETWEEN the customer block and the Work-Order
   reference table (flex row, banner vertically centred). Reference table is
   compacted (CN-only — DN keeps the shared .doc-toright sizing). */
.doc-toright.cn { align-items: flex-start; }
.doc-toright.cn .cn-reprint { align-self: center; flex: 0 0 auto; margin: 0 16px; }
.doc-toright.cn .rc-reprint { align-self: center; flex: 0 0 auto; margin: 0 16px; }
.doc-toright.cn .doc-owner { min-width: 0; }
.doc-toright.cn .doc-owner th, .doc-toright.cn .doc-owner td { padding: 3px 7px; font-size: 10px; }
.doc-toright.cn .doc-owner th { background: #e9edf5; }
/* Debit Note — exact mirror of the approved CN row layout: below the header's
   horizontal line, ONE flex row = customer block left, "Duplicated copy 重印"
   banner centered, Work Order/Quotation reference table right.
   NOTE: override the base .doc-toright .doc-owner { align-self:flex-end } so the
   reference table TOP-aligns with the customer name (otherwise a tall customer block
   drops the short table to the bottom of the row). */
.doc-toright.dn { align-items: flex-start; }
.doc-toright.dn .dn-reprint { align-self: center; flex: 0 0 auto; margin: 0 16px; }
.doc-toright.dn .doc-owner { min-width: 0; align-self: flex-start; }
.doc-toright.dn .doc-owner th, .doc-toright.dn .doc-owner td { padding: 3px 7px; font-size: 10px; }
.doc-toright.dn .doc-owner th { background: #e9edf5; }
.doc-items th, .doc-items td { border: 1px solid #666; padding: 6px 8px; font-size: 11px; text-align: left; font-weight: 400; line-height: 1.45; }
.doc-items th { background: #e9edf5; white-space: nowrap; }
/* WO print only: center all table headers (scoped to .doc.wo-print so Quotation print stays left-aligned) */
.doc.wo-print .doc-items th { text-align: center; }
/* All numeric figures right-aligned */
.doc-items .doc-num { text-align: right; white-space: nowrap; }
/* WO print — Payment Slips table column balance + pill-fit (user 2026-09-09):
   - No. column widened (19%) and nowrap so the slip number (e.g. WO2026-0001-1)
     stays on ONE line instead of wrapping on the hyphen.
   - T Amount kept the biggest non-pill column (33%) but trimmed from the prior 38%
     so the Status pill (Pending / Overdue / etc.) fits inside its column without
     overflowing the right border.
   - Status widened to 18% to comfortably hold the pill (~70px) plus padding.
   - Due rebalanced to 15% (dc 2026-09-11). At the previous 12% the overdue cell
     (a nowrap "2026/08/15" + the 逾期 tag) measured clientWidth 87px against
     scrollWidth 95px, so its red text ran 7px INTO the Status cell and printed
     on top of the "Overdue" pill. T Amount gave up the 3%: it holds at most
     "0.00 / 3,000.00" (~95px) and stays far inside 30%.
   - Pill slightly compacted in WO print so it never crowds the cell border.
   Scoped to .ps-table so the Details table above keeps its own widths. */
.doc.wo-print .ps-table .doc-code { width: 19%; white-space: nowrap; }
.doc.wo-print .ps-table .doc-amt  { width: 30%; }
.doc.wo-print .ps-table .doc-unit { width: 15%; }
.doc.wo-print .ps-table > thead > tr > th:first-child,
.doc.wo-print .ps-table > tbody > tr > td:first-child { width: 18%; }   /* Stage */
.doc.wo-print .ps-table > thead > tr > th:last-child,
.doc.wo-print .ps-table > tbody > tr > td:last-child { width: 18%; }   /* Status */
/* Receipt detail row (full-width, below its slip row): reads as a sub-detail,
   not a data row. No overflow — colspan=5 spans the full 794px page width. */
.doc.wo-print .ps-table .ps-rc-row .ps-rc-cell {
  background: #f6f8fc;
  border-top: 0;
  border-left: 3px solid var(--brand-d);
  padding: 5px 10px;
}
.doc-items .doc-sub { text-align: right; font-weight: 400; }
.doc-items tfoot td { background: #e9edf5; font-weight: 400; }
/* Inline Settled/Unsettled footer inside the Payment Slips table — keeps
   vertical space tight on the WO PDF by absorbing the once-separate summary
   line as a single tfoot row. */
.doc-items tfoot td.ps-totals { font-weight: 400; font-size: 11px; padding: 5px 8px; }

/* ============================================================
   Quotation PDF LAYOUT RULES — mirror app.js RUNHEAD_* constants + the
   orphan-cell rule. These MUST live OUTSIDE @media print: html2pdf's in-browser
   path rasterises the node in the live document (screen media), so the
   @media print block below only governs the window.print() fallback and is
   ignored by html2pdf. With doPrint() using pagebreak mode ['css'], these
   break-* rules let the table FLOW across the page boundary (breaking BETWEEN
   rows) while keeping each row/cell intact — no sliced "orphan" cell, and no
   whole-table shove to the next page (which would leave a big empty margin).
   ============================================================ */
/* Receipt (收款收據): "Received with thanks" + signature/chop block.
   Spacing mirrors the Credit Note's .cn-sign (.cn-sign uses margin-top:32px;
   font-size:12px; line-height:1.6) — dotted line is inline after the label,
   invalid-without note reuses .cn-sign-note (margin-top:6px).
   ROOT FIX (2026-10-01): this rule previously carried the selectors
   `.doc-items tr/td/th, .doc-owner tr/td/th` in its group, so every printed
   document table cell got font-weight:700 — outranking the
   `.doc-items td { font-weight:400 }` above by source order (same specificity)
   and contradicting the PRINTED-DOCUMENTS-CARRY-NO-BOLD convention (line ~373).
   That is why quotation numbers, descriptions and qty read bold in the PDF
   while the headers stayed 400 (saved only by the higher-specificity
   `.doc table th` rule). The doc selectors are removed: the receipt keeps its
   deliberate bold, the document cells return to 400. */
.rec-thanks { font-weight: 700; margin: 12px 0 2px; }
.rec-words { margin: 2px 0; font-weight: 700; letter-spacing: .3px; }
.rec-words-cn { margin: 0 0 6px; font-size: 13px; }
.doc-sign { margin-top: 32px; font-size: 12px; line-height: 1.6; display: flex; gap: 40px; }
.doc-sign .sig { flex: 1 1 0; }
.doc-sec, .doc .total, .doc-remarks {
  break-inside: avoid;
  page-break-inside: avoid;
}

/* ---------- Single-draw hairline grid: same line width on screen and in the PDF ----------
   html2canvas (the rasterizer behind html2pdf) does NOT implement CSS border
   collapsing. With `border-collapse: collapse` the browser merges the shared
   edge of two neighbouring cells into ONE 1px line, but html2canvas paints
   every cell's own four borders, so each interior edge is drawn twice and comes
   out roughly double the thickness of the outer frame — screen 1px vs PDF 2px.
   Fix: make sure no edge is ever painted by two boxes. Tables switch to
   `separate` with zero spacing, each cell paints only its RIGHT + BOTTOM edge,
   and the table itself paints the TOP + LEFT frame. Every line is then drawn
   exactly once at exactly 1px in both renderers. Keep these rules last so they
   win over the per-table border shorthands above. */
.doc table, .doc-items, .doc-owner, .doc-toright .doc-owner {
  border-collapse: separate;
  border-spacing: 0;
  border-top: 1px solid #666;
  border-left: 1px solid #666;
}
.doc th, .doc td,
.doc-items th, .doc-items td,
.doc-owner th, .doc-owner td,
.doc-toright .doc-owner th, .doc-toright .doc-owner td {
  border: 0;
  border-right: 1px solid #666;
  border-bottom: 1px solid #666;
}

/* --- Rule tone + a bottom frame on every .tbl inside a document --------------
   Tone is #666 (luminance 102 == 60 % ink): the midpoint of the two rejected
   extremes. #ccc measured luminance 204 (~20 % ink) and read as "too light";
   #000 (luminance 0, 100 % ink) read as "too heavy". Every .doc rule uses this
   single value — do not introduce a second tone, and do not go back to #ccc.
   Two ON-SCREEN UI rules outrank the draw-once pair above once a `table.tbl`
   sits inside a `.doc`:
       `table.tbl th, table.tbl td`  (0,1,2)  >  `.doc td`  (0,1,1)
       `table.tbl tr:last-child td`  (0,2,3)  >  everything
   The first quietly recoloured a .tbl's bottom edge to the UI grey; the second
   set it to `none` and, because the draw-once recipe makes the LAST ROW's
   bottom edge BE the table's bottom frame (the table element itself paints only
   top+left), the bottom rule of every .tbl disappeared from the exported PDF —
   the missing line under the DN Payment Schedule / Summary tables.
   Re-assert both at matching specificity. Do not remove: without the second
   rule the bottom frame of these tables is gone again. */
.doc table.tbl th, .doc table.tbl td {
  border: 0;
  border-right: 1px solid #666;
  border-bottom: 1px solid #666;
}
.doc table.tbl tr:last-child td { border-bottom: 1px solid #666; }

/* Debit Note Schedule/Summary tables (class="tbl" inside .doc).
   NO OVERRIDE HERE ON PURPOSE — .tbl follows the same draw-once recipe as every
   other document table: `.doc table` paints the top+left frame and `.doc th,
   .doc td` paints right+bottom, so no edge is ever drawn by two boxes.

   What used to be here was a per-cell `border: 1px solid #ccc`. Under
   `border-collapse: separate` the browser does NOT merge shared edges, so an
   interior rule got painted twice (once as the left cell's right border, once as
   the right cell's left border) and came out at ~2x. Measured on a real exported
   DN (600 dpi render of the actual PDF, not a CSS reading):
     per-cell full border  -> interior rules 0.55-0.68 mm (1.6-1.9 pt)
     draw-once (this now)  -> interior rules 0.25-0.30 mm (0.7-0.9 pt)
   i.e. 7 of the DN's 17 horizontal rules were double weight; now none are, and
   the DN matches the quotation / WO sheets exactly. Same fix applies to the old
   `border: 1px solid #ccc` on `table.tbl .rec-line td` (see above) — it won on
   specificity (0,2,2 > 0,1,1) and silently re-doubled that row's edges.

   The stale claim that "html2canvas drops the TABLE-element frame" was re-tested
   2026-09-11: with this override removed the DN keeps all 17 horizontal and 5
   vertical rules, so the frame does render. Do NOT re-add a per-cell full border. */

/* rec-line override removed: defer to the single uniform 1px border above.
   (the old 3px-left accent was an html2canvas colspan cell-borders regression). */

/* Quotation EDITOR item grid — mirrors the generated PDF's .doc-items sections
   (Setup / Recurring / Ad) so the form layout matches the printed quotation.
   table-layout:fixed + identical th/td padding keeps each section's header
   perfectly aligned with its entry rows (no offset / orphan cells). */
.qsec-items { width: 100%; border-collapse: collapse; table-layout: fixed; margin: 2px 0 4px; }
.qsec-items th, .qsec-items td { border: 1px solid #ccc; font-size: 11px; vertical-align: middle; }
.qsec-items th { padding: 8px 8px; }
.qsec-items td { padding: 6px 8px; }
.qsec-items th { background: #e9edf5; white-space: nowrap; font-weight: 600; color: #33405a; }
.qsec-items .qi-idx  { width: 3%;  text-align: right; color: #8b95a8; white-space: nowrap; }
.qsec-items .qi-pick { width: 16%; }
.qsec-items .qi-code { width: 11%; }
.qsec-items .qi-desc { width: 30%; }
.qsec-items .qi-num  { text-align: right; }
.qsec-items .qi-qty  { width: 6%; }
.qsec-items .qi-term { width: 7%; }
.qsec-items .qi-unit { width: 12%; }
.qsec-items .qi-amt  { width: 12%; text-align: right; font-weight: 700; white-space: nowrap; }
.qsec-items .qi-del  { width: 3%; text-align: center; }
.qsec-items input { width: 100%; border: 0; background: transparent; padding: 5px 4px; font-size: 11px; font-family: inherit; box-sizing: border-box; color: #1f2739; }
.qsec-items input:focus { outline: 2px solid var(--brand, #1f53d0); outline-offset: -2px; border-radius: 2px; background: #fff; }
.qsec-items .qi-num input { text-align: right; }
.qsec-items tfoot td { background: #f7f7f7; font-weight: 700; }

/* Quotation list — compact grid (space-saving, many rows visible) */
.qbar { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.qbar .qtoolbar { flex: 1 1 240px; margin-bottom: 0; }
.qbar .qsearch { width: 100%; margin: 0; }
.qbar .qfilters { flex: 0 0 auto; margin-bottom: 0; }
.qtoolbar { margin-bottom: 10px; }
.qsearch { width: 100%; margin: 0; }
.qwrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
.qgrid { width: 100%; border-collapse: collapse; font-size: 11px; min-width: 720px; }
.qgrid thead th {
  position: sticky; top: 0; z-index: 2; background: var(--brand-tint-2); color: var(--ink-soft);
  text-align: left; font-size: 11px; font-weight: 600; letter-spacing: .4px; text-transform: uppercase;
  padding: 8px 8px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.qgrid tbody td { padding: 6px 8px; border-bottom: 1px solid #eef1f6; vertical-align: middle; color: #1f2739; }
.qgrid tbody tr:nth-child(even) td { background: #fafbfe; }
.qgrid tbody tr:hover td { background: var(--row-hover); }
.qgrid tr.grp td { border-top: 2px solid #cdd7e8; }
.qgrid tr.grp:first-child td { border-top: none; }
.qgrid .nowrap { white-space: nowrap; }
.qgrid .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.qgrid tbody .num { font-weight: 700; }
/* Bold belongs to the VALUE, not the header: a numeric column head must keep the
   same weight as every other header (600) or the header row looks half-bold. */
.qgrid tbody td.num { font-weight: 700; }
.qgrid .acts { white-space: nowrap; }
.qgrid td.center, .qgrid th.center { text-align: center; }
.qgrid .acts .btn { margin: 1px 3px 1px 0; }
/* WO list action cell: condensed flex (like the Quotation .acts) and pushed right.
   margin:0 is REQUIRED here — the WO DETAIL header also uses class .wo-actions
   (styles.css ~:900) with margin:10px 0; without this reset that margin leaks into
   every list row and inflates row height 33px -> 51px (the "row height altered"
   regression). Class collision: list cell vs detail header share the name. */
.qgrid th.actsc, .qgrid td.actsc { text-align: right; }
.qgrid .wo-actions { display: flex; gap: 4px; flex-wrap: nowrap; justify-content: flex-end; width: 100%; margin: 0; }
.qgrid .wo-actions .btn { margin: 0; }
.qgrid .acttbl { border-collapse: collapse; table-layout: fixed; margin: 0; }
.qgrid .acttbl td { padding: 0 4px; vertical-align: middle; white-space: nowrap; text-align: left; }
/* the nested acts table must NOT inherit the per-row accent bar (styles.css:649/:650
   match any td:first-child under .qgrid tbody, including this nested one) */
.qgrid .acttbl td:first-child,
.qgrid tbody tr:hover .acttbl td:first-child { box-shadow: none; }
/* fixed-width action slots: col1 reserves the "M Work C" pill width even when empty,
   so View/PDF stay in slots 2,3 (never collapse left to "2|3"). */
.qgrid .acttbl td.act-c1 { width: 80px; min-width: 80px; }
.qgrid .acttbl td.act-c2,
.qgrid .acttbl td.act-c3 { width: 64px; min-width: 64px; }
.qgrid .acttbl td:first-child { padding-left: 0; }
.qgrid .qempty { padding: 24px; text-align: center; color: var(--ink-soft); }
.qgrid th { cursor: pointer; user-select: none; }
.qgrid th:hover { background: #e2e9f5; }
.qgrid th.sort-active { color: var(--brand-d); }
.qgrid .muted { color: var(--ink-soft); }
.qgrid .sm { font-size: 11px; line-height: 1.35; }
/* Deeper-blue list identity — match the Quotation tree's accent across all lists */
.qgrid tbody td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
.qgrid tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--brand-d); }
/* Golden action-chip size — matches the Quotation list action buttons; all list
   action columns use .btn.xs, so this one rule standardises every page. */
.btn.xs { padding: 3px 6px; font-size: 11px; border-radius: 8px; line-height: 12px; }

/* Desktop layout */
@media (min-width: 720px) {
  .modal { align-items: center; }
  .modal-card { border-radius: 16px; }
  .bottomnav { left: 0; right: 0; transform: none; max-width: none; border-radius: 0; bottom: 0; box-shadow: none; border-top: 1px solid var(--line); }
  body { padding-bottom: 90px; }
}

/* ============================================================
   QUOTATION REVISIONS — tree list (ported from the preview)
   ============================================================ */
.qfilters { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.qfilters .qf {
  border: 1px solid var(--line); background: var(--surface); border-radius: 8px;
  padding: 6px 12px; font-size: 11px; color: var(--ink-soft); cursor: pointer;
}
.qfilters .qf.on { background: #eaf1ff; color: var(--brand-d); border-color: transparent; }
.qtree { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); }
.qtree .ghead {
  display: grid; grid-template-columns: 30px minmax(120px,1.2fr) minmax(0,2.5fr) minmax(62px,0.6fr) minmax(132px,1fr) minmax(150px,max-content); gap: 10px;
  padding: 8px 8px; align-items: center; background: var(--brand-tint-2);
  font-size: 11px; font-weight: 600; letter-spacing: .4px; text-transform: uppercase; color: var(--ink-soft);
}
/* Actions header sits left, matching the qgrid list convention (Customers / Products / WO). */
.qtree .ghead > div:last-child { justify-self: start; }
.qtree .ghead > div.sortable { cursor: pointer; user-select: none; }
.qtree .ghead > div.sortable:hover { color: var(--brand-d); }
.qtree .ghead > div.sort-active { color: var(--brand-d); }
.qtree .grow, .qtree .child {
  display: grid; grid-template-columns: 30px minmax(120px,1.2fr) minmax(0,2.5fr) minmax(62px,0.6fr) minmax(132px,1fr) minmax(150px,max-content); gap: 10px;
  padding: 6px 8px; align-items: center; border-top: 1px solid var(--line); font-size: 11px;
  min-height: 35px; /* match .qgrid row height (35px) so the two list systems align */
  transition: background-color .12s ease;
}
/* Column order = Quote No | Customer | Status | Amount | Actions (Actions rightmost,
   matching the WO list baseline). Status column holds only the pill. */
.qtree .grow > div:nth-child(3), .qtree .child > div:nth-child(3) { white-space: nowrap; }
/* Customer column maximized, never wraps (ellipsis if extremely long) to save vertical space */
.qtree .grow > div:nth-child(2), .qtree .child > div:nth-child(2) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qtree .grow > div, .qtree .child > div { min-width: 0; }
.qtree .grow.base { background: #fcfcfd; box-shadow: inset 3px 0 0 var(--brand); }
.qtree .qnum { display: flex; align-items: center; gap: 8px; font-weight: 600; white-space: nowrap; }
.qtree .qnum-link { color: var(--brand-d); text-decoration: none; cursor: pointer; font-weight: 600; }
.qtree .qnum-link:hover { text-decoration: underline; }
.qtree .qnum-link.badge-link { font-weight: 400; }
.qtree .qnum-link.badge-link:hover .badge { filter: brightness(.96); outline: 1px solid var(--brand); }
.qtree .qnum .badge.r { cursor: pointer; user-select: none; }
.qtree .qnum .badge.r:hover { filter: brightness(.96); outline: 1px solid var(--brand); }
.qtree .qnum .badge.r.open { background: var(--brand); color: #fff; }
/* Left-most revision expander ("+" collapsed / "−" expanded). Only rows that HAVE
   revisions get the `.has` class + data-act so they are clickable; single (base-only)
   quotations render an empty spacer to keep the grid aligned. */
.qtree .ghead-exp { min-width: 0; }
.qtree .qexp { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin: 0 auto; border-radius: 6px; font-weight: 700; font-size: 15px; line-height: 1; color: var(--brand-d); user-select: none; }
.qtree .qexp.has { cursor: pointer; }
.qtree .qexp.has:hover { background: var(--brand-tint-2); }
.qtree .qexp.open { background: var(--brand); color: #fff; }
.qtree .amt { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--ink); font-size: 11px; justify-self: end; text-align: right; white-space: nowrap; }
/* Actions column left-aligned to match the qgrid golden convention; Amount stays right. */
.qtree .acts { display: flex; gap: 4px; flex-wrap: nowrap; justify-self: start; }
/* ── THE ONE DISABLED TREATMENT ────────────────────────────────────────────
   dc (2026-10-08): "no more disappear. if the pill/button is disabled, show
   them, in disabled format" + "unify all". This replaces the four-selector
   opacity rule that stood here, which had three defects, all measured:
     1. SCOPE — it needed an ancestor .acts inside .qgrid/.qtree, so a header
        "+ New", a sheet button or anything outside .acts was NEVER dimmed.
     2. OPACITY CANNOT DIM A LIGHT LABEL — it moves the background *behind* the
        text. Measured enabled->dim label luminance delta: .btn.soft 0.332,
        .btn.danger 0.315, .btn.primary 0.000 (white on blue, unchanged).
     3. It did nothing for form fields. styles.css had NO field rule at all,
        so a disabled <select> was greyed to 0.7 by the BROWSER while a
        disabled <input> sat at 1.0 — identical to an editable one.

   Explicit values, one rule, every variant. `border-color` and never
   `border: 0` (.btn.danger has a 1px border and .btn is content-sized, so
   zeroing it shrank 31.13->29.13 wide, 20->18 tall). Ring by inset
   box-shadow so the box does not resize (a real border grew it 32.78->34.78).
   .prompt-acts (the revision prompt) and .grid-pager folded into the same
   treatment — the pager's own opacity:.4 is gone with it. */
.btn:disabled {
  background: #f4f6fb;
  color: #8b95a7;
  border-color: #c9d1de;
  box-shadow: inset 0 0 0 1px #c9d1de;
  cursor: not-allowed;
  opacity: 1;
}
.btn:disabled:hover, .btn:disabled:active, .btn:disabled:focus { transform: none; filter: none; }

/* Locked form fields get the SAME treatment as locked buttons. dc asked
   whether category 4 was unified; it was not, and the cause was the browser
   default rather than this file. Element types are listed explicitly so a
   future <select> cannot silently fall back to the UA's 0.7 grey. */
input:disabled,
select:disabled,
textarea:disabled {
  background: #f4f6fb;
  color: #8b95a7;
  border-color: #c9d1de;
  box-shadow: inset 0 0 0 1px #c9d1de;
  cursor: not-allowed;
  opacity: 1;
}
/* Quotation action chips inherit the golden .btn.xs size; only keep the no-wrap. */
.qtree .acts .btn { white-space: nowrap; }

/* revision branch (older revisions nested under the newest) */
/* Indent only the Quote-No cell (not the whole grid) so the Actions column stays full width
   and Edit/Accept/PDF/Del never wrap, even for revisions nested inside .branch */
.qtree .branch { position: relative; }
.qtree .branch::before { content: ""; position: absolute; left: 45px; top: -16px; width: 2px; height: 16px; background: var(--line); }
.qtree .child { position: relative; background: #fafbfc; }
.qtree .child.latest { background: #f3f8ff; }
.qtree .child::before { content: ""; position: absolute; left: 45px; top: 0; width: 2px; height: 100%; background: var(--line); }
.qtree .child::after { content: ""; position: absolute; left: 45px; top: 26px; width: 24px; height: 2px; background: var(--line); }
.qtree .child > div:nth-child(2) { padding-left: 70px; }
.qtree .child.last::before { height: 28px; }
.qtree .ver { background: #fafbfc; font-size: 11px; color: var(--ink-soft); padding: 6px 8px; border-top: 1px solid var(--line); }
.qtree .ver b { color: var(--ink); font-weight: 500; }
.qtree .qempty { padding: 24px; text-align: center; color: var(--ink-soft); }
/* Credit-note worklist reuses the .qtree grouping mechanism but with 8 tracks
   (expander + WO / Customer / Slip / Due / Status / Outstanding / Actions) so slips
   group under their Work Order exactly like revisions group under a quotation.
   COLUMN ALIGNMENT: the qtree owns the 8-track definition ONCE; every row — and the
   .branch wrapper — spans it via CSS subgrid. Without this, each row resolves its own
   max-content tracks and rows with short content (empty Due/Status, short amounts —
   i.e. the CN sub-rows) shift the Outstanding track right into the Actions column. */
.qtree.cn { display: grid; gap: 0 10px; grid-template-columns:
  30px minmax(88px,max-content) minmax(85px,1fr) auto max-content max-content auto max-content; }
/* Header labels never wrap (CUSTOMER used to fold onto two lines when starved). */
.qtree.cn .ghead > div { white-space: nowrap; }
.qtree.cn .ghead,
.qtree.cn .grow,
.qtree.cn .child,
.qtree.cn .cnrow,
.qtree.cn .branch {
  display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
}
/* Indent the nested slip rows under the WO parent (col 2/3 are empty spacers). */
.qtree.cn .child > div:nth-child(2),
.qtree.cn .child > div:nth-child(3) { padding-left: 0; }
.qtree.cn .child > div:nth-child(4) { padding-left: 18px; }
/* Credit-note sub-rows under a stage: same 8-track grid, tinted surface, NOT
   hoverable (no click action) so stage rows and CN rows stay visually distinct. */
.qtree.cn .cnrow { display: grid; background: #f6f9ff; cursor: default; min-height: 35px; border-top: 1px solid var(--line); font-size: 11px; align-items: center; }
.qtree.cn .cnrow > div:nth-child(4) { padding-left: 18px; }
/* Slip cell of a CN row: date / CN chip / reason stay on ONE line (the Slip track
   is content-sized via `auto`, so the full sequence always fits). */
.qtree.cn .cnrow > div:nth-child(4) { display: flex; flex-wrap: nowrap; column-gap: 7px; align-items: center; white-space: nowrap; }
/* CN number chip — data identifier, NOT a status pill (no forced min-width). */
.qtree.cn .cn-no {
  font-family: var(--mono); font-size: 11px; font-weight: 600; color: var(--brand-d);
  background: var(--brand-tint); padding: 1px 7px; border-radius: 5px; margin: 0 7px;
}
.qtree.cn .cn-date { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* CN amount = money-out → attention tone (RULE-ui-attention-tone: #EF4444 family). */
.qtree.cn .amt.cn-amt { color: var(--attn, #EF4444); }
.doc .cn-amt { color: var(--attn, #EF4444); font-weight: 700; }
/* PDF button on each CN row — same .btn.soft.xs as the WO-list PDF (size/font/outlook),
   tone only overridden to the money-out family (RULE-ui-attention-tone). */
.qtree.cn .btn.cn-pdf { background: var(--attn-chip, #FEE2E2); color: var(--attn-name, #B91C1C); }
.qtree.cn .btn.cn-pdf:hover { filter: brightness(.96); }
/* Slip label stays on one line and clips with an ellipsis if ever too long — it must
   never overflow into the Due column. Due is a fixed-format date, also one line. */
.qtree.cn .grow > div:nth-child(4), .qtree.cn .child > div:nth-child(4) {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qtree.cn .grow > div:nth-child(5), .qtree.cn .child > div:nth-child(5) { white-space: nowrap; }
/* hover highlight for quote list rows (base + revision children) — uses a clearly
   visible blue and higher specificity so it always overrides .base / .latest backgrounds */
.qtree .grow:hover, .qtree .child:hover,
.qtree .grow.base:hover, .qtree .child.latest:hover {
  background: var(--row-hover) !important; cursor: pointer;
}

/* small badge / pill (shared) */
.badge { font-size: 11px; padding: 2px 9px; border-radius: 6px; background: #eef1f6; color: var(--ink-soft); }
.badge.r { background: #eaf1ff; color: var(--brand-d); }
.badge.ver-toggle { cursor: pointer; user-select: none; }
.badge.ver-toggle:hover { filter: brightness(.96); outline: 1px solid var(--brand); }
.badge.ver-toggle.open { background: var(--brand); color: #fff; }
.lock { font-size: 11px; color: #8a5a00; background: #fdf3e7; padding: 2px 9px; border-radius: 6px; margin-left: 8px; }

/* revision decision prompt (on Save with changes) */
.prompt { position: fixed; inset: 0; background: rgba(20,24,33,.4); display: none; align-items: center; justify-content: center; z-index: 80; }
.prompt.open { display: flex; }
.prompt-card { background: var(--surface); width: min(460px, 92vw); border-radius: 16px; padding: 22px 24px; box-shadow: 0 12px 40px rgba(0,0,0,.2); }
.prompt-card h3 { margin: 0 0 8px; font-size: 14px; }
.prompt-card p { margin: 0 0 18px; color: var(--ink-soft); font-size: 14px; line-height: 1.55; }
.prompt-acts { display: flex; flex-direction: column; gap: 10px; }
.prompt-acts .btn { width: 100%; text-align: center; }
.prompt-acts .new { background: var(--brand); color: #fff; border-color: transparent; }
.lock-note { margin: 0 0 10px; font-size: 11px; line-height: 1.4; color: #b42318; background: #fef3f2; border: 1px solid #fecdca; border-radius: 8px; padding: 8px 10px; }

/* ---------- PRINT: only the quotation / receipt sheet prints ---------- */
/* size:A4 is REQUIRED for the window.print() fallback and for any future vector
   path. Without it Chrome falls back to the OS default paper size (Letter on a
   US-locale machine) and silently reflows every page. html2pdf ignores @page
   entirely — it declares its own jsPDF format — so this line is inert for the
   raster path and only fixes the print path. The 12mm margin matches the top
   reserve doPrint() derives from RUNHEAD_RESERVE_MM (11mm + 1mm). */
@page { size: A4; margin: 12mm; }
@media print {
  /* Force a white page background. Without this the body's --bg tint
     (#f4f6fb) leaks through any unused space below the document whenever
     backgrounds are painted (which they are, so .doc-items th can carry its
     distinct header colour). */
  html, body, #printArea { background: #fff !important; }

  /* Kill the bottom-nav reserve on paper. body gets padding-bottom:76px (and
     90px from the >=720px desktop rule) purely to clear the fixed .bottomnav;
     there is no nav on paper, and those 90px are what pushed the sheet past
     the A4 content box and produced a second page holding only the closing
     line. Measured 2026-09-11 in print media: doc stack 1020px (forced
     min-height) + 90px (this padding) = 1110px against a 1031.8px box
     (297mm - 2x12mm) -> 78px / 20.4mm over. Do NOT delete: without it the
     print path has only 11.8px of slack. */
  body { padding-bottom: 0 !important; }

  /* Hide every top-level element except the print area so the exported
     PDF contains just the document, not the app chrome. */
  body > *:not(#printArea) { display: none !important; }
  /* position:static MUST be !important. doPrint() parks #printArea with an
     INLINE style (position:fixed; left:-10000px) so the unhidden sheet never
     flashes over the live app; an inline style outranks a plain declaration, so
     the old rule lost and printing emitted a BLANK page. Measured 2026-09-11:
     ink coverage 0.00% while parked, 3.59% once the offset is cleared. */
  #printArea { display: block !important; position: static !important; left: auto !important; top: auto !important; width: auto !important; box-shadow: none !important; }
  .doc { box-shadow: none !important; border: none !important; max-width: none; }

  /* Keep rows, CELLS and key blocks from being split across a page break. */
  .doc-items tr, .doc-items td, .doc-items th,
  .doc-owner tr, .doc-owner td, .doc-owner th,
  .doc table tr, .doc table td, .doc table th,
  .doc-sec,
  .doc .total,
  .doc-remarks,
  .cn-reprint, .cn-sign, .cn-sign-note, .dn-reprint, .rc-reprint { page-break-inside: avoid; break-inside: avoid; }

  /* Table header cells keep a clearly distinct tint from the white content
     cells so the printed table reads column-wise. Supporting blocks (subtotal
     row, remarks box) stay neutral grey rather than competing with the header. */
  .doc-items th, .doc-owner th { background: #e9edf5 !important; }
  .doc-items tr.doc-subrow td,
  .doc-remarks { background: #f7f7f7 !important; }
}

/* ---------- Quotation EDIT/ADD sheet adopts the PDF (.doc) color theme ----------
   The on-screen form was blue-brand themed while the generated PDF is a neutral
   grayscale + red document. Remap the sheet's theme variables to the PDF palette
   (black text/dividers #111, #ccc borders, #f3f6fb fills, red discount #b4232a)
   so editing matches the printed output. Scoped to #qsheet only — the rest of
   the app keeps its brand theme. */
#qsheet {
  --brand: #111;
  --brand-d: #111;
  --ink: #111;
  --ink-soft: #666;
  --line: #ccc;
  --surface: #fff;
  color: #111;
}
#qsheet input:focus, #qsheet select:focus, #qsheet textarea:focus {
  border-color: #111;
  box-shadow: 0 0 0 3px rgba(0,0,0,.08);
}
#qsheet .btn.soft { background: #f3f6fb; color: #111; }
/* Primary action buttons keep the GLOBAL brand blue: the --brand:#111 remap above is
   for the document-preview look (text/dividers), but .btn { background: var(--brand) }
   inherits it, which turned #qSheetSave black while #formSheetSave stayed blue
   (LRN-20261001-001). Exclude action buttons from the remap; .soft chips stay gray. */
#qsheet .btn:not(.soft) { background: #1f53d0; }
#qsheet .chip, #qsheet .chip.grp-chip { background: #f3f6fb; color: #111; border: 1px solid #ccc; }
#qsheet .grp-add, #qsheet .item-card, #qsheet .disc-section { background: #fbfcfe; }
#qsheet .ro { background: #f3f6fb; }
#qsheet #qDiscShow { color: #b4232a; font-weight: 700; }

/* ---------- WORK ORDERS ---------- */
/* minmax(0,1fr) not bare 1fr: a long <option>/<input> inside a cell must not
   inflate its column (auto min-size) — e.g. a long customer name used to blow
   the Customer column to ~2x the others. Equal thirds, always. */
.row3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.row3 .field { margin-bottom: 12px; }
@media (max-width: 540px) { .row3 { grid-template-columns: 1fr; gap: 0; } }
.wo-item-row { display: grid; grid-template-columns: 28px 1fr 70px 110px 34px; gap: 6px; margin-bottom: 8px; align-items: end; }
.wo-item-row .wo-idx { font-size: 11px; color: var(--ink-soft); text-align: right; padding-right: 2px; align-self: end; padding-bottom: 9px; }
.wo-item-row input { padding: 8px 9px; font-size: 14px; }
.wo-split-row { display: grid; grid-template-columns: 28px 1.4fr 0.8fr 1fr 34px; gap: 6px; margin-bottom: 8px; align-items: end; }
.wo-split-row input { padding: 8px 9px; font-size: 14px; }
.wo-split-row .muted { font-size: 11px; }
a { color: var(--brand-d); text-decoration: none; }
.lk { color: var(--brand-d); cursor: pointer; text-decoration: underline; }
.wo-detail .sec { margin: 16px 0; }
.wo-detail h3 { font-size: 14px; margin: 0 0 6px; color: var(--ink-soft); }
.wo-detail .tbl { width: 100%; border-collapse: collapse; }
.wo-detail .tbl th, .wo-detail .tbl td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); font-size: 11px; }
.wo-detail .tbl td.num, .wo-detail .tbl th.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
.wo-detail .tbl tfoot td { border-top: 2px solid var(--line); }
/* Condensed WO header: action buttons share the page-head row with title + status */
.wo-detail .ph-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.wo-detail .ph-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.wo-detail h3.sec-pay { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.wo-detail h3.sec-pay .sec-pay-sum { font-weight: 400; font-size: 11px; color: var(--ink-soft); }
.wo-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; align-items: center; }
.timeline { list-style: none; padding-left: 0; margin: 0; }
.timeline li { padding: 5px 0; border-bottom: 1px dashed var(--line); font-size: 11px; }
/* State-change log: fixed-width columns — state change | who | when | what. */
.timeline.log { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); }
.timeline.log .lg-head,
.timeline.log .lg-row { display: grid; grid-template-columns: 190px 120px 150px 1fr; gap: 10px; padding: 7px 10px; border-bottom: 1px solid var(--line); font-size: 11px; align-items: baseline; }
.timeline.log .lg-row:last-child { border-bottom: none; }
.timeline.log .lg-head { background: var(--surface); color: var(--ink-soft); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
.timeline.log .lg-state { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.timeline.log .lg-who { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--ink-soft); }
.timeline.log .lg-when { font-family: var(--mono); font-variant-numeric: tabular-nums; letter-spacing: 0.2px; white-space: nowrap; color: var(--ink-soft); }
.timeline.log .lg-what { overflow-wrap: anywhere; }
.timeline.log .muted { padding: 7px 10px; border-bottom: none; }
.pill.init { background: #eef1f6; color: #6b7689; }
.pill.in_progress { background: var(--brand-tint); color: var(--brand-d); }
.pill.completed { background: #e6f6ec; color: var(--ok); }
.pill.void { background: #eef0f2; color: #6b7689; text-decoration: line-through; }
.pill.partial { background: #fff4d6; color: #b07400; }

/* Consistent summary panel for the WO create form (right-aligned, mirrors print) */
.wo-summary { margin: 10px 0 4px; border-top: 1px solid var(--line); padding-top: 8px; text-align: right; }
.wo-sum-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 14px; padding: 2px 0; }
.wo-sum-row .lbl { color: var(--ink-soft); }
.wo-sum-row .val { font-variant-numeric: tabular-nums; }
.wo-sum-row.discount .val { color: #b4232a; font-weight: 600; }   /* consistent discount colour (money off) */
.wo-sum-row.grand { border-top: 1px dashed var(--line); margin-top: 4px; padding-top: 6px; }
.wo-sum-row.grand .lbl { font-weight: 700; }
.wo-sum-row.grand .val { font-weight: 800; font-size: 14px; }
/* Unified discount colour, also used in view/print tables */
.amt-disc { color: #b4232a; font-weight: 600; }

/* Payment slip summary + pay-modal bits */
.wo-pay-summary { display: flex; gap: 18px; flex-wrap: wrap; margin: 4px 0 12px; font-size: 11px; }
.wo-pay-summary b { font-weight: 700; }
.pay-slip-info { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; font-size: 11px; }
.pay-meta {
  margin-top: 4px;
  font-size: 11px;
  color: #64748b;
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
}
.pay-meta .pay-row {
  display: grid;
  grid-template-columns: max-content 78px 1fr auto auto auto;
  column-gap: 6px;
  align-items: center;
  align-items: baseline;
  white-space: nowrap;
}
.pay-meta .pay-amt {
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.pay-meta .pay-date {
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  text-align: right;
  font-feature-settings: 'tnum' 1;
  white-space: nowrap;
}
.pay-meta .pay-ref, .pay-meta .pay-payee { color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; }
.pay-meta .pay-link, .pay-meta a { color: var(--brand-d); }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 14px; }

/* Column headers for the dynamic item/split rows */
.wo-row-head { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); margin: 2px 0 6px; padding: 0 2px; }
.wo-row-head.items { display: grid; grid-template-columns: 28px 1fr 70px 110px 34px; gap: 6px; }
.wo-row-head.items .wo-idx { text-align: right; padding-right: 2px; }
.wo-row-head.splits { display: grid; grid-template-columns: 28px 1.4fr 0.8fr 1fr 34px; gap: 6px; }

/* ===== CSQP: condense the quotation editor height (scoped to #qsheet only,
   so the PDF / condensed popup / other sheets are untouched) ===== */
#qsheet .sheet-body { padding: 10px; padding-bottom: 20px; }
#qsheet .q-top { padding: 8px 10px; margin: 2px 0 6px; }
#qsheet .q-top .row3 { gap: 6px 10px; }
#qsheet .q-top .field { margin-bottom: 5px; }
#qsheet .q-top .field label { margin-bottom: 2px; font-size: 11px; }
#qsheet .q-top input, #qsheet .q-top select { padding: 5px 6px; font-size: 11px; }
/* "To customer" preview line rides on the SAME line as the 客戶 caption, to its
   right. ONE line only — a long "name · company" never wraps; it clips with an
   ellipsis (…) and flex:1 + min-width:0 lets the ellipsis trigger at the column edge. */
#qsheet .field-head { display: flex; align-items: baseline; gap: 6px; }
#qsheet .field-head label { margin-bottom: 0; }
#qToPreview { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#qsheet .grp-add.compact { margin: 2px 0 0; }
#qsheet .grp-add.compact .grp-add-row select { padding: 4px 6px; font-size: 11px; }
#qsheet .disc-section { padding: 7px 10px; margin: 6px 0; }
#qsheet .disc-section .disc-head { font-size: 11px; margin-bottom: 4px; }
#qsheet .disc-controls select, #qsheet .disc-controls input { padding: 4px 6px; font-size: 11px; }
#qsheet .doc-sec.sec-head { margin: 8px 0 3px; }
#qsheet .sec-lite { margin: 8px 0 2px; }
#qsheet .qsec-items { margin: 2px 0 2px; }
#qsheet .qsec-items th, #qsheet .qsec-items td { padding: 3px 6px; font-size: 11px; }
#qsheet .qsec-items input { padding: 3px 4px; font-size: 11px; }
#qsheet .qsec-items select { padding: 4px 6px; font-size: 11px; }
#qsheet .qsec-items tfoot td { padding: 3px 6px; }
#qsheet .field { margin-bottom: 7px; }
#qsheet .field label { margin-bottom: 3px; font-size: 11px; }
#qsheet .field textarea { padding: 5px 6px; font-size: 11px; }
#qsheet .grid-totals { margin-top: 4px; padding-top: 5px; }
#qsheet .sub { margin: 1px 0 0; }
/* Remarks + Response notes side by side to halve their vertical footprint */
#qsheet .q-notes-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
#qsheet .q-notes-row .field { margin-bottom: 7px; }
@media (max-width: 540px) { #qsheet .q-notes-row { grid-template-columns: 1fr; } }

/* ===== Customer (and generic form) sheet: match the quotation editor design =====
   Adds the CSV pill + Edit/New toggle, and gives #formSheet the same compact
   field sizing as #qsheet so the customer editor is pixel-consistent with the
   quotation editor (no more distorted look). Mirrors preview_customer_form.html. */
.btn.csv { background: #eafaf1; color: var(--ok); border: 1px solid #bfe9cf; box-shadow: none; }
.btn.csv:active { transform: translateY(1px); }
.mode-toggle { display: flex; gap: 6px; }
.mode-toggle .btn { box-shadow: none; }
.mode-toggle .btn.on { background: var(--brand-d); color: #fff; }
#formSheet .sheet-body { padding: 10px; padding-bottom: 20px; }
#formSheet .q-top { padding: 8px 10px; margin: 2px 0 6px; }
#formSheet .q-top .row3 { gap: 6px 10px; }
#formSheet .q-top .field { margin-bottom: 5px; }
#formSheet .q-top .field label { margin-bottom: 2px; font-size: 11px; }
#formSheet .q-top input, #formSheet .q-top select { padding: 5px 6px; font-size: 11px; }
#formSheet .disc-section { padding: 7px 10px; margin: 6px 0; }
#formSheet .disc-section .disc-head { font-size: 11px; margin-bottom: 4px; }
#formSheet .field { margin-bottom: 7px; }
#formSheet .field label { margin-bottom: 3px; font-size: 11px; }
#formSheet .field textarea { padding: 5px 6px; font-size: 11px; }
#formSheet .btn.block { margin-top: 6px; padding: 9px; }
#formSheet .sub { margin: 1px 0 0; }
.field label .req { color: var(--bad); }
/* Product editor — recurring-times box (hidden unless category = recurring) */
#recurWrap[hidden] { display: none; }
.field .hint-inline { font-size: 11px; color: var(--ink-soft); font-weight: 400; margin-left: 6px; }

/* ---- Product Group items (pg-items): scoped overrides of .qsec-items ---- */
.g-add { display: flex; gap: 8px; align-items: center; margin: 4px 0 8px; }
.dup-warn { background: #fff0f0; border: 1px solid #f3bcbc; color: #a4262c; font-size: 11px; padding: 8px 12px; border-radius: 6px; margin: 8px 0; line-height: 1.55; }
.dup-warn.hidden { display: none; }
.qsec-items.pg-items { table-layout: fixed; }
.qsec-items.pg-items .qi-idx { width: 30px; text-align: right; color: #8b95a8; white-space: nowrap; }
.qsec-items.pg-items .qi-pick { width: 40%; }
.qsec-items.pg-items .qi-qty  { width: 9%; }
.qsec-items.pg-items .qi-orig { width: 12%; text-align: right; color: #7a5b00; font-weight: 600; white-space: nowrap; }
.qsec-items.pg-items .qi-unit { width: 12%; }
.qsec-items.pg-items .qi-amt  { width: 13%; text-align: right; font-weight: 700; white-space: nowrap; }
.qsec-items.pg-items .qi-del  { width: 30px; text-align: center; }
.qsec-items.pg-items tbody tr:nth-child(even) td { background: var(--amt-bg); }
.qsec-items.pg-items tbody tr:hover td { background: var(--row-hover, #d4e4ff); }
.qsec-items.pg-items tr.dup td { background: #ffe9e9 !important; }
.qsec-items.pg-items tr.dup td.qi-idx { box-shadow: inset 3px 0 0 #e0524f; }
.qsec-items.pg-items tfoot td { background: var(--amt-bg); border-top: 2px solid var(--brand, #1f53d0); vertical-align: top; padding: 8px 6px; }
.qsec-items.pg-items tfoot .ft-cap { font-size: 11px; font-weight: 600; color: #5b6b85; margin-bottom: 3px; }
.qsec-items.pg-items tfoot .qi-orig b { color: #7a5b00; }
.qsec-items.pg-items tfoot .qi-amt { text-align: right; }
.qsec-items.pg-items tfoot .qi-amt b { color: var(--brand-d, #1740a8); }
.qsec-items.pg-items select { width: 100%; border: 0; background: transparent; padding: 5px 4px; font-size: 11px; font-family: inherit; box-sizing: border-box; color: #1f2739; }
.qsec-items.pg-items select:focus { outline: 2px solid var(--brand, #1f53d0); outline-offset: -2px; border-radius: 2px; background: #fff; }

/* Sales Person — assigned-records block (delete protection) */
.sp-ref { margin-top: 14px; border: 1px solid #e3e8f0; border-radius: 6px; overflow: hidden; }
.sp-ref-head { background: var(--amt-bg); border-bottom: 1px solid #e3e8f0; padding: 7px 12px; font-size: 11px; font-weight: 600; color: #37476b; }
.sp-ref.is-linked .sp-ref-head { background: #fff4e5; border-bottom-color: #f0dcb8; color: #7a5b00; }
.sp-ref-sec + .sp-ref-sec { border-top: 1px solid #eef1f6; }
.sp-ref-sub { padding: 7px 12px 5px; font-size: 11px; font-weight: 700; color: #5b6b85; }
.sp-scroll { max-height: 220px; overflow: auto; }
.sp-ref-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.sp-ref-table th { text-align: left; font-size: 11px; font-weight: 700; color: #6b7689; padding: 4px 12px; background: #fafbfd; border-top: 1px solid #eef1f6; border-bottom: 1px solid #eef1f6; position: sticky; top: 0; }
.sp-ref-table td { padding: 5px 12px; border-bottom: 1px solid #f4f6fa; color: #1f2739; }
.sp-ref-table tr:last-child td { border-bottom: 0; }
.sp-ref-table tbody tr:nth-child(even) td { background: #fafcff; }
.sp-ref-table tbody tr:hover td { background: var(--row-hover); }
.sp-ref-table td.num, .sp-ref-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.sp-ref-table td.nowrap, .sp-ref-table th.nowrap { white-space: nowrap; }
.sp-none { color: #8b96a8; font-style: italic; padding: 2px 12px 8px; font-size: 11px; }

/* Explicit rows for the Special dashboard: each user-specified row holds exactly its tiles, sized evenly.
   Base .sp-card / .sp-gfx / .sp-txt styling lives in the v118 block further down. */
.sp-row { display: flex; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
/* All tiles share the 3-column width (row 3 defines it); 1- and 2-tile rows keep this width and do NOT stretch. */
.sp-row .sp-card { flex: 0 0 calc((100% - 24px) / 3); min-width: 0; }
/* ---- quotation tree embedded in the PIC (Sales Person) edit sheet ----
   Mirrors the Quotation list page .qtree exactly (base rows + nested revision children,
   unified .pill status, sortable headers). Only overrides what must differ: a NARROW
   leading # sequence column and a capped height with its own scrollbar (so a PIC with
   many quotations cannot stretch the sheet indefinitely). */
.sp-quote-grid { overflow: auto; max-height: 320px; margin: 2px 0 4px; }
.sp-quote-grid .qtree .ghead { position: sticky; top: 0; z-index: 1; }
.sp-quote-grid .qtree .ghead,
.sp-quote-grid .qtree .grow,
.sp-quote-grid .qtree .child {
  grid-template-columns: 28px minmax(96px,1fr) minmax(0,2fr) 92px 1fr 116px;
  gap: 8px;
}
/* # sequence column — narrow + muted (was "too wide") */
.sp-quote-grid .qtree .qseq,
.sp-quote-grid .qtree .qseq-head {
  color: #9aa6b8; font-size: 11px; font-variant-numeric: tabular-nums;
  text-align: right; padding-right: 2px;
}
.sp-quote-grid .qtree .qdate { color: var(--ink-soft); font-size: 11px; font-variant-numeric: tabular-nums; }
.sp-quote-grid .qtree .ghead > div[data-sort] { cursor: pointer; }
/* branch connectors sit under the Quote No column (col 2), since # now precedes it */
.sp-quote-grid .qtree .branch::before { left: 40px; }
.sp-quote-grid .qtree .child::before,
.sp-quote-grid .qtree .child::after { left: 40px; }
/* indent only col 2 (Quote No) for the connector — never the narrow # column */
.sp-quote-grid .qtree .child > div:first-child { padding-left: 0; }
.sp-quote-grid .qtree .grow.base > div:first-child { padding-left: 0; }
.sp-quote-grid .qtree .child > div:nth-child(2) { padding-left: 46px; }
/* Status header stays left-aligned (global .qtree justifies the last column to end) */
.sp-quote-grid .qtree .ghead > div:last-child { justify-self: start; }

/* ============ GRID PAGER (global, all list pages) ============ */
.grid-pager {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 12px 4px 8px; border-top: 1px solid var(--row-hover); margin-top: 8px;
  font-size: 11px; color: var(--ink-soft);
}
.grid-pager .pg-info strong { color: var(--ink); }
.grid-pager .pg-range { color: var(--ink-soft); }
.grid-pager .pg-size-label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.grid-pager .pg-size {
  font: inherit; padding: 4px 8px; border: 1px solid var(--row-hover); border-radius: 6px;
  background: var(--surface); color: var(--ink);
}
.grid-pager .pg-size:hover { border-color: var(--brand); }

/* ============ SETTINGS PAGE ============ */
.settings-grid { display: grid; gap: 16px; padding: 0; }
.setting-section {
  background: var(--surface); border: 1px solid var(--row-hover); border-radius: 10px;
  padding: 18px 20px;
}
.setting-section h3 { margin: 0 0 6px; font-size: 14px; color: var(--ink); }
.setting-section .muted.sm { display: block; margin: 0 0 10px; color: var(--ink-soft); font-size: 11px; }
.setting-section .row2,
.setting-section .row3 { display: grid; gap: 12px; margin-top: 8px; }
.setting-section .row2 { grid-template-columns: 1fr 1fr; }
.setting-section .row3 { grid-template-columns: 1fr 1fr 1fr; }
.setting-section .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.setting-section .field label { font-size: 11px; color: var(--ink-soft); font-weight: 600; }
.setting-section .field input,
.setting-section .field select {
  padding: 8px 10px; border: 1px solid var(--row-hover); border-radius: 8px;
  background: var(--surface); color: var(--ink); font: inherit; width: 100%; box-sizing: border-box;
}
.setting-section .field input:focus,
.setting-section .field select:focus { outline: 2px solid var(--brand); outline-offset: 0; border-color: var(--brand); }
.setting-section .field input::placeholder { color: var(--ink-soft); opacity: .6; }
.grid-perviews { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.setting-row {
  display: grid; grid-template-columns: 1fr 88px auto; gap: 12px; align-items: center;
  padding: 8px 12px; border-radius: 8px; background: var(--surface);
}
.setting-row:hover { background: var(--row-hover); }
.setting-row .setting-row-label { color: var(--ink); font-weight: 500; }
.setting-row select {
  padding: 4px 6px; border: 1px solid var(--row-hover); border-radius: 6px;
  background: var(--surface); color: var(--ink); font: inherit; text-align: center;
}
.setting-row .setting-row-info { text-align: right; }
/* Per-document watermark switches: same row look as the pagination rows, but the
   second column holds a Yes/No radio pair instead of a narrow select. */
.wm-docs { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }

/* Special / Operations dashboard (v118) — Supabase-style minimal tile grid.
   Namespaced .sp-* to avoid colliding with existing .card / .grid / .page-title. */
.sp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 14px; }
@media (max-width: 820px) { .sp-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .sp-grid { grid-template-columns: 1fr; } }
.sp-card {
  display: flex; flex-direction: row; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; transition: background .12s ease, box-shadow .12s ease;
  min-height: 62px;
}
.sp-card:hover { background: var(--row-hover); box-shadow: var(--shadow); }
.sp-gfx {
  width: 34px; height: 34px; border-radius: 9px; background: #f0f0f0;
  display: flex; align-items: center; justify-content: center;
  color: #9ca3af; font-size: 10px; font-weight: 600; overflow: hidden; flex: none;
}
.sp-gfx img { width: 100%; height: 100%; object-fit: contain; }
.sp-txt { min-width: 0; }
.sp-name { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.2; }
.sp-desc { margin: 2px 0 0; color: var(--ink-soft); font-size: 12px; line-height: 1.35; }
/* ---- Attention tone (v127) -------------------------------------------------
   The 7 modules that need special attention / push money out share ONE red tone
   (#EF4444 family). Recompute Status + Audit Log stay neutral on purpose.
   Hover is a PALER red (#FEF2F2) so the icon chip (#FEE2E2) stays visible. */
:root {
  --attn: #EF4444; --attn-chip: #FEE2E2; --attn-name: #B91C1C;
  --attn-hover: #FEF2F2; --nav-hl: #EEF1F6;
}
.sp-card.attn { box-shadow: inset 4px 0 0 var(--attn); }
.sp-card.attn .sp-gfx { background: var(--attn-chip); }
.sp-card.attn .sp-name { color: var(--attn-name); }
/* Hover must keep the inset stripe AND the lift shadow — a plain box-shadow
   here would silently drop the stripe. */
.sp-card.attn:hover { background: var(--attn-hover); box-shadow: inset 4px 0 0 var(--attn), var(--shadow); }
/* Module screens of the attention group carry the same stripe; their rows hover pale red. */
.attn-screen .qgrid tbody tr:hover td { background: var(--attn-hover); }
.attn-screen .sec h3 { color: var(--attn-name); border-left: 3px solid var(--attn); padding-left: 8px; }
/* Attention screens keep the canonical list design (no screen-edge bar) —
   only the per-row accent bars turn from brand blue to attention red. */
.attn-screen .qgrid tbody td:first-child { box-shadow: inset 3px 0 0 var(--attn); }
.attn-screen .qgrid tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--attn-name); }
/* Carry the red attention tone into the .qtree component as well. The Credit Note list is
   grouped via .qtree but is a money-out module, so it must share the #EF4444 family like
   the .qgrid attention screens — otherwise dashboard↔inner-page semantics break (the rule's
   explicit "同一色調要一路帶到模組內頁" requirement). Scoped to .attn-screen only so the
   non-attention Quotation list keeps its blue brand stripe/hover. */
.attn-screen .qtree .grow.base { box-shadow: inset 4px 0 0 var(--attn); }
.attn-screen .qtree .grow:hover,
.attn-screen .qtree .child:hover { background: var(--attn-hover) !important; }
/* Red section-title equivalent for .qtree attention pages: the in-content header bar is
   the .ghead (the page title sits in #viewHead, structurally OUTSIDE .attn-screen, so a
   descendant selector can never reach it — same as the other attn screens). */
.attn-screen .qtree .ghead { background: var(--attn-hover); color: var(--attn-name); }
.wm-docs .setting-row { grid-template-columns: 1fr auto; }
.wm-radios { display: flex; gap: 16px; align-items: center; }
.wm-radios label {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; color: var(--ink); cursor: pointer; white-space: nowrap;
}
.wm-radios input[type=radio] { accent-color: var(--brand); margin: 0; cursor: pointer; }

/* ---- Environment banner (TEST ONLY — NOT LIVE) ----
   Sticky so it stays visible on every page. Placed in <body> outside the print
   container, so it never appears in PDFs or CSV exports. */
.env-banner {
  position: sticky;
  top: 0;
  z-index: 9000;
  background: #c0152f;
  color: #fff;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-align: center;
  padding: 6px 12px;
}
.env-banner[hidden] { display: none; }

@media (max-width: 720px) {
  .setting-section .row2,
  .setting-section .row3 { grid-template-columns: 1fr; }
  .setting-row { grid-template-columns: 1fr 80px auto; }
  .grid-pager { flex-wrap: wrap; justify-content: center; }
}

/* Language switch (Settings page). HK = mixed 繁/EN, EN = English, SC = 简体. */
html[data-lang="sc"] {
  --doc-font: "Noto Sans SC", "Noto Sans TC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
}
.lang-switch { display: inline-flex; gap: 6px; margin-top: 4px; }
.lang-opt {
  font: 500 13px/1 inherit;
  padding: 8px 14px;
  border: 1px solid var(--line, #d8dde8);
  background: #fff;
  color: var(--ink, #1f2739);
  border-radius: 8px;
  cursor: pointer;
}
.lang-opt:hover { border-color: var(--brand, #2b6cb0); }
.lang-opt.active {
  background: var(--brand, #2b6cb0);
  border-color: var(--brand, #2b6cb0);
  color: #fff;
}

/* ---------- USER MAINTENANCE ---------- */
/* ONE MODULE PER ROW (dc, 2026-10-07: "each module shall fit into one row only").
   Was repeat(auto-fill, minmax(240px,1fr)) -- a wrapping matrix, so a module's
   actions split across lines and two modules shared one line. One column is
   the only layout that holds the rule at every viewport. */
.um-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 6px; }
.um-mod { border: 1px solid var(--line, rgba(0,0,0,.12)); border-radius: 8px; padding: 10px 12px; }
.um-mod-h { font-weight: 600; font-size: 12px; margin-bottom: 6px; }
.um-acts { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.um-chk { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; cursor: pointer; white-space: nowrap; flex: none; }
/* dc 2026-10-09 15:08: the native disabled checkbox draws a border around the
   grey fill -- draw our own boxes instead so the locked (grey) ones are plain
   grey with no ring; and the All box sits at the row's RIGHT, not left. */
.um-chk input {
  margin: 0;
  appearance: none; -webkit-appearance: none;
  width: 14px; height: 14px; border-radius: 3px;
  border: 1px solid rgba(15, 35, 80, .35); background: #fff;
  position: relative; vertical-align: -2px; cursor: pointer;
}
.um-chk input:checked { background: var(--brand); border-color: var(--brand); }
.um-chk input:checked::after {
  content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.um-chk input:disabled { background: #cfd4dc; border-color: #cfd4dc; cursor: default; }
.um-chk.um-all { margin-left: auto; }
