/* ==========================================================================
   styles.css  —  All styling for the Brightpearl Dashboard.
   --------------------------------------------------------------------------
   This was previously inside a <style> block in the single HTML file. It is
   split out here so the page markup (index.html) stays readable and the look
   of the app can be tweaked in one place.

   Rough layout of this file:
     1. Base + layout (body, container, header, nav)
     2. Cards, metrics, tables, badges, status messages
     3. Forms + autocomplete (search-as-you-type dropdowns)
     4. Order-line "row" grid
     5. Debug console panel
     6. The decorative animated header tree
   ========================================================================== */

/* ---- 1. Base + layout --------------------------------------------------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f4; color: #1a1a1a; padding: 24px; }
/* One wrapper holds the header, the nav tabs, every section AND the full-screen
   View-order page, so a single rule keeps them all the same width.
   DEFAULT (every list/search screen): the long-standing fixed 1200px. */
.container { width: 100%; max-width: 1200px; margin: 0 auto; }

/* ONLY while an individual sale / purchase order is open, the app may widen to
   1500px — those line-item rows (product + stock + costs + actions) are what
   need the room; the list screens are deliberately left at 1200.
   Keyed off `#order-page.active` with :has(), so it follows the ACTUAL view
   rather than a flag the router has to remember to set — the order page is
   opened and closed from several places (navigateTo, popstate, deep link).
   `fit-content` makes the header, nav and card hug the table instead of leaving
   the card hanging past it; 1500px is a CAP, not a target.
   The cap is min(1500px, 100%), not a bare 1500px: `fit-content` falls back to
   the content's MIN-content width when the window is smaller, which on a nowrap
   order table is huge — without the 100% the whole PAGE scrolled sideways on a
   small window. Clamping to 100% keeps the scrolling inside the table's own
   overflow-x wrapper where it belongs.
   The min-width is a FLOOR of the normal 1200: without it a short order (one
   line, no long names) shrank the whole app to ~992px, so opening an order
   jumped INWARDS — it may only ever grow, never shrink.
   These are the browser's own size checks, re-evaluated on every resize, zoom
   and monitor change — a JS listener could only approximate them. */
.container:has(> #order-page.active) {
  width: fit-content;
  min-width: min(1200px, 100%);
  max-width: min(1500px, 100%);
}

/* The SALES and PURCHASES list screens get the same 1500px headroom.
   ⚠ 1200px was the right cap when these lists were five columns. They are now eight
   — Order · Brightpearl status · Status · Customer · Created · Total · Paid · Review —
   and at 1200 the customer names wrap, the status pills stack and the whole row reads
   as squashed. The columns are all wanted, so the room has to come from the page.
   ⚠ `width: 100%` is INHERITED here rather than switched to `fit-content` like the
   order page above. That is deliberate: these sections also hold the status-button card
   and the search card, whose max-content width is every button on ONE line — under
   `fit-content` that card, not the table, would decide the page width. Filling up to a
   cap keeps the table in charge and the buttons wrapping as they always have.
   ⚠ `min(1500px, 100%)` for the same reason as the order page: below 1500 the page
   uses what there is instead of overflowing the window sideways.
   ⚠ Nothing else moves — Products, Customers, Settings and the rest stay at 1200. */
.container:has(> #sales.active),
.container:has(> #purchases.active) {
  max-width: min(1500px, 100%);
}

/* ---- The sales / purchases list table has a WORKING MINIMUM --------------- */
/* ⚠⚠ WIDENING THE PAGE ALONE DID NOT FIX THE SQUEEZE, and the reason is worth knowing:
   a table column is sized from its content's MIN-CONTENT width, and the min-content of
   a wrapping badge is only its longest WORD. So "ORDERS FOR PROCESSING" kept breaking
   across two lines however much room the page had — the column never asked for more.
   Two things fix it, and both are needed:
   1. `nowrap` on the badge, so the column's min-content becomes the whole status name
      and the table actually allocates the width.
   2. A `min-width` on the table, so the columns cannot be compressed below a usable
      size on a narrower window. Below that the card scrolls SIDEWAYS instead — the
      table's own overflow, never the page's, so the nav and the rest stay put. */
#sales-results, #purchases-results { overflow-x: auto; }
#sales-results > table, #purchases-results > table { min-width: 1400px; }
/* ⚠ The status name stays on ONE line. A status badge folded in half reads as broken,
   and it is the whole point of the column. */
.bp-status-col > span { white-space: nowrap; }

/* ---- The Order column, sized around the customer reference --------------- */
/* Left to itself the Order column took whatever its longest reference wanted — on this
   account that is sale 342380's "KM94 DUE MID SEPT VBCLUTCHES ORDERED BACK ORDER STIHL
   ORDERED", 61 characters, which is far too much of the table for an id and a note.
   ⚠ 226px is MEASURED, not guessed. The measurement is of the TEXT, and the cell adds
   8px of padding each side — so the column is the text width plus 16. That reference
   wraps to three lines below ~196px of text and to two from ~200px up, and the two
   lines are at their most even between 200 and 216 (169px / 197px, a 28px spread). Past
   220 the split degrades to 220/146, and past 250 to 249/116 — a long first line with a
   stub under it. 226 gives 210px of text: the middle of the even band.
   ⚠⚠ Getting the padding wrong is exactly how this was first set to 210 and rendered
   THREE lines — 210 − 16 = 194, which is in the three-line band.
   ⚠ NO `text-wrap: balance` here, and that is a measured decision rather than an
   oversight: it was tried, and at this width it changes nothing — 169/197 with it,
   without it, inline and as a block alike. The word boundaries simply do not allow an
   evener split, so the declaration would have been decoration that did no work.
   ⚠ It is a `width`, not a `max-width`: a table cell treats width as a preference and
   still grows for content it cannot break, so a single unbreakable long word can never
   be clipped — nothing is hidden, it just pushes. */
.sales-order-col { width: 226px; }
.sales-order-ref { font-size: 11px; color: #888780; }
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
h1 { font-size: 22px; font-weight: 600; }
.status { font-size: 12px; padding: 4px 10px; background: #e1f5ee; color: #0f6e56; border-radius: 6px; }
.nav { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 20px; background: white; padding: 6px; border-radius: 10px; border: 1px solid #e5e5e3; }
.nav-btn { padding: 8px 14px; border-radius: 6px; border: none; background: transparent; color: #6b6b66; font-size: 13px; cursor: pointer; transition: all 0.15s; font-weight: 500; }
.nav-btn.active { background: #1a1a1a; color: white; }
.nav-btn:not(.active):hover { background: #f0efe9; color: #1a1a1a; }
.section { display: none; }
.section.active { display: block; }
.row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px; }

/* ---- 2. Cards, metrics, tables, badges, messages ------------------------ */
.card { background: white; border: 1px solid #e5e5e3; border-radius: 10px; padding: 20px; margin-bottom: 12px; }
.metric { background: white; border: 1px solid #e5e5e3; border-radius: 10px; padding: 14px 18px; }
.metric-label { font-size: 11px; color: #888780; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.metric-value { font-size: 22px; font-weight: 600; color: #1a1a1a; }
.input-row { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; flex-wrap: wrap; }
.input-row input, .input-row select { flex: 1; min-width: 150px; padding: 9px 12px; border: 1px solid #d3d1c7; border-radius: 6px; font-size: 13px; background: white; }
.input-row input:focus, .input-row select:focus { outline: none; border-color: #378add; }
.input-row button { padding: 9px 18px; border: none; border-radius: 6px; background: #1a1a1a; color: white; font-size: 13px; cursor: pointer; font-weight: 500; }
.input-row button:hover { background: #2c2c2a; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 14px; }
table { width: 100%; font-size: 13px; border-collapse: collapse; }
th { text-align: left; font-weight: 500; font-size: 11px; color: #888780; padding: 10px 8px; border-bottom: 1px solid #e5e5e3; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 12px 8px; border-bottom: 1px solid #f0efe9; color: #1a1a1a; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafaf8; }
/* Receive Goods PO lines keep their colour on hover, a shade LIGHTER rather than the
   near-white above: fully received (green #e3f4ea) and scanned (purple #f5f3ff). */
tr.recv-row-done:hover td { background: #eef9f2; }
tr.recv-row-scanned:hover td { background: #faf9ff; }
/* Order line-item tables FILL their white .order-detail box, so the table's right
   edge always lines up with it. (An earlier version made them width:auto to stop
   the PRODUCT column hogging surplus width; that was right when .container was
   pinned at 1500px, but .container now hugs its content — and whenever something
   else sets the width, e.g. the signed-in nav, a content-sized table was left
   stranded 193px short of the box it sits in.)
   The three ACTION columns (Receive / Unreceive / Create) hold buttons, so they
   get real breathing room — which also claims some of the leftover width that
   would otherwise all land on PRODUCT (measured: product slack 90px -> 62px).
   `has-actions` marks the interactive tables; the read-only PO view has no action
   columns, and without it the padding would land on Received/Outstanding/Cost. */
table.line-items { width: 100%; }
table.line-items.has-actions th:nth-last-child(-n+3),
table.line-items.has-actions td:nth-last-child(-n+3) { padding-left: 18px; padding-right: 18px; }
/* ---- The PRODUCT column gives way BEFORE the table scrolls sideways ------- */
/* The order page may grow to the 1500px cap above, and the line-items table is
   white-space:nowrap inside an overflow-x:auto wrapper. When a product name is so
   long that even the cap is not enough, the table used to scroll sideways instead —
   with Reset / Swap / Prepare / Remove off the right-hand edge. Now the Product
   column is clamped by exactly the overflow (`fitLineItemsProductColumn` in read.js
   MEASURES it and sets --product-col-max), so the page still sits at its cap and
   the name WRAPS onto a second line inside the narrower column. Nothing is cut: an
   ellipsis was tried first and rejected — a name that disappears into "…" is a name
   nobody can read, and the whole point of the row is to say what the product is.
   ⚠ MEASURED IN JS, NOT `max-width: 0` — the usual CSS trick. max-width:0 zeroes the
   column's intrinsic width, so the page would never GROW for a long name at all: the
   column would take whatever the 1200px floor left over, when the whole point of the
   cap is that the page expands FIRST and wraps only as a last resort.
   ⚠ `.vo-product-name` wraps the name in every product cell (sales AND purchase
   views, bundle parents and children, fake lines) so ONLY the name is allowed to
   wrap: the table stays nowrap, and the variant line / stock figures beneath the name
   keep their single line (clipped by the cell if ever wider than it).
   ⚠ `white-space: normal` only under `.product-fit` — at its natural width the name
   must stay nowrap, or the table's natural width would be measured with the name
   already wrapped and the page would never expand for it. `overflow-wrap: anywhere`
   lets a single unbroken token (a long SKU-ish name) break rather than overflow. */
table.line-items.product-fit > tbody > tr > td:nth-child(2) { max-width: var(--product-col-max); overflow: hidden; }
table.line-items.product-fit .vo-product-name { white-space: normal; overflow-wrap: anywhere; }
.badge { display: inline-block; font-size: 11px; padding: 3px 9px; border-radius: 5px; font-weight: 500; }
.badge-green { background: #e1f5ee; color: #0f6e56; }
.badge-amber { background: #faeeda; color: #854f0b; }
.badge-red { background: #fcebeb; color: #a32d2d; }
.badge-info { background: #e6f1fb; color: #185fa5; }
.badge-gray { background: #f0efe9; color: #444441; }
.error { color: #a32d2d; font-size: 13px; padding: 12px; background: #fcebeb; border-radius: 8px; margin-top: 8px; border: 1px solid #f7c1c1; }
.loading { color: #888780; font-size: 13px; padding: 20px 0; text-align: center; }
.empty { color: #888780; font-size: 13px; padding: 20px 0; text-align: center; }
.detail-grid { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; font-size: 13px; padding: 8px 0; }
.detail-label { color: #888780; }
.detail-value { color: #1a1a1a; font-weight: 500; }
.order-detail { background: #fafaf8; border-radius: 8px; padding: 16px; margin-top: 12px; }
.pill-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }

/* ---- 3. Forms + autocomplete (type-ahead dropdowns) --------------------- */
.autocomplete { position: relative; }
.autocomplete input { width: 100%; padding: 9px 12px; border: 1px solid #d3d1c7; border-radius: 6px; font-size: 13px; background: white; }
.autocomplete input:focus { outline: none; border-color: #378add; }
.autocomplete-suggestions { position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #d3d1c7; border-radius: 6px; margin-top: 4px; max-height: 280px; overflow-y: auto; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.08); display: none; }
.autocomplete-suggestions.active { display: block; }
.suggestion { padding: 10px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid #f0efe9; }
.suggestion:last-child { border-bottom: none; }
.suggestion:hover, .suggestion.highlighted { background: #f5f5f4; }
.suggestion-main { font-weight: 500; color: #1a1a1a; }
.suggestion-meta { font-size: 11px; color: #888780; margin-top: 2px; }
.selected-pill { display: inline-flex; align-items: center; gap: 8px; background: #e6f1fb; color: #185fa5; padding: 6px 10px; border-radius: 6px; font-size: 13px; font-weight: 500; }
.selected-pill button { background: none; border: none; color: #185fa5; cursor: pointer; font-size: 14px; padding: 0 2px; }
.field-label { font-size: 11px; color: #888780; text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 4px; }
.form-input { width: 100%; padding: 9px 12px; border: 1px solid #d3d1c7; border-radius: 6px; font-size: 13px; }

/* Aggressive "this field is required" state — thick red border, red glow ring, a
   pale red fill, and a one-off shake to draw the eye. Used on the Reference field
   when a PO-required customer's sale is submitted without a reference. */
.field-error {
  border: 2px solid #d23c3c !important;
  box-shadow: 0 0 0 4px rgba(210, 60, 60, 0.28) !important;
  background: #fff5f5 !important;
  outline: none !important;
  animation: field-shake 0.4s cubic-bezier(.36, .07, .19, .97) both;
}
@keyframes field-shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}

/* The two ACCOUNT-customer tickboxes ("Require a Purchase Order number" and "Invoice
   after creation") flicker when the customer-details screen opens, so staff notice that
   these are decisions about THIS customer and check them.
   ⚠⚠ A DELIBERATELY SLOW, FINITE PULSE — 3 cycles at 0.8s (1.25Hz), then it settles and
   stops. Anything faster than 3 flashes a second is a photosensitive-seizure risk (WCAG
   2.3.1), and anything endless stops being a signal and becomes something staff learn to
   ignore. It is a colour/glow pulse, NOT a flash to white or a jump in position.
   ⚠ `both` keeps the final keyframe, so the row ends where it started rather than snapping. */
@keyframes pref-flicker {
  0%, 100% { background: #f6f5f1; border-color: #e5e5e3; box-shadow: 0 0 0 0 rgba(180, 83, 9, 0); }
  50%      { background: #fff4dc; border-color: #e0a83c; box-shadow: 0 0 0 4px rgba(224, 168, 60, 0.35); }
}
.pref-flicker { animation: pref-flicker 0.8s ease-in-out 3 both; }
/* Anyone who has asked for less motion gets a plain steady highlight instead — the point
   is to be NOTICED, and that does not require movement. */
@media (prefers-reduced-motion: reduce) {
  .pref-flicker {
    animation: none;
    background: #fff4dc;
    border-color: #e0a83c;
  }
}

/* An empty SERIAL NUMBER box on the create-sale basket, flashed red when someone tries
   to take payment with it still blank. Same rules as .pref-flicker above and for the
   same reason: 3 slow cycles at 0.8s (1.25Hz) then it settles and stops — faster than 3
   flashes a second is a photosensitive-seizure risk (WCAG 2.3.1), and an endless flash
   is something staff learn to ignore rather than a signal.
   ⚠ `both` keeps the final keyframe, so the box ends where it started. */
@keyframes serial-flash {
  0%, 100% { background: #fffaf0; border-color: #e0a83c; box-shadow: 0 0 0 0 rgba(192, 57, 43, 0); }
  50%      { background: #fdecec; border-color: #c0392b; box-shadow: 0 0 0 4px rgba(192, 57, 43, 0.35); }
}
.serial-flash { animation: serial-flash 0.8s ease-in-out 3 both; }
/* Less motion asked for → a steady red highlight. The point is to be NOTICED, and that
   does not require movement.
   ⚠⚠ `!important` is LOAD-BEARING here, unlike on .pref-flicker: the serial boxes carry
   their empty/filled colours as an INLINE style (they are built in JS), and a normal
   declaration in a class loses to an inline one. The animation above needs no such help
   — animation declarations already outrank inline styles — but this fallback replaces
   the animation entirely, so without it a reduced-motion user would see NOTHING. */
@media (prefers-reduced-motion: reduce) {
  .serial-flash {
    animation: none;
    background: #fdecec !important;
    border-color: #c0392b !important;
  }
}

/* The sale page's "Serial numbers" TAB while a serial on it is UNREGISTERED (serialTabSetPulse
   in serials.js). ⚠ CONTINUOUS, unlike the two above — asked for: it is a standing job that
   must not be missed, and it stops by itself once the last serial is registered. A SLOW
   0.5Hz pulse (2s), far under WCAG 2.3.1's 3 flashes/second.
   ⚠ The tab button carries `background:none` INLINE; the animation outranks it, but the
   reduced-motion fallback replaces the animation, so it needs `!important` (the
   .serial-flash rule). */
@keyframes serial-tab-pulse {
  0%, 100% { background: #fff8ec; color: #854f0b; box-shadow: inset 0 0 0 1px #f0d9ad; }
  50%      { background: #fde3b0; color: #7a3e00; box-shadow: inset 0 0 0 2px #e0a83c; }
}
.serial-tab-pulse { animation: serial-tab-pulse 2s ease-in-out infinite; border-radius: 6px 6px 0 0; }
@media (prefers-reduced-motion: reduce) {
  .serial-tab-pulse {
    animation: none;
    background: #fde3b0 !important;
    color: #7a3e00 !important;
  }
}

/* The customer's Brightpearl POP-UP MESSAGE on the create-sale items screen
   (soShowPopupMessage). Bold crimson, opacity FLOWING 100% ↔ 80% (±10% about 90%), asked
   for. A slow 2.4s fade, not a flash — nowhere near WCAG 2.3.1's 3/second; reduced motion
   gets it steady. `pre-line` keeps the line breaks staff typed into the message. */
@keyframes so-popup-flow {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.8; }
}
.so-popup-message {
  color: #a50e2d; font-weight: 700; font-size: 14px; line-height: 1.45;
  white-space: pre-line; overflow-wrap: anywhere;
  margin: -6px 0 14px;
  animation: so-popup-flow 2.4s ease-in-out infinite;
}
.so-popup-pending { color: #888780; font-size: 12px; margin: -6px 0 14px; }
/* …and on the View-order page, under the customer's name (voFillCustomerPopup): ONE quiet
   line — same dark crimson, bold, NO fade (that page may already have a pulsing tab), cut
   with an ellipsis; a click (`.expanded`) shows it all with the typed line breaks. */
.vo-popup-message {
  color: #a50e2d; font-weight: 700; font-size: 12px; line-height: 1.4; margin-top: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; cursor: pointer;
}
.vo-popup-message.expanded { white-space: pre-line; overflow-wrap: anywhere; }
/* ⚠ The header is a grid with a `1fr` column, whose minimum is its CONTENT — so a long
   nowrap line would widen the whole header instead of being cut. min-width:0 on THIS cell
   only (not every .detail-value) lets the ellipsis work. */
.detail-value:has(> .vo-popup-message) { min-width: 0; }
@media (prefers-reduced-motion: reduce) {
  .so-popup-message { animation: none; }
}

/* Pulsing "View Pending Collections" header button — a soft expanding ring to draw
   the eye to the customer's other open orders. Stops pulsing on hover. */
@keyframes vo-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(180, 83, 9, 0.5); }
  70%  { box-shadow: 0 0 0 9px rgba(180, 83, 9, 0); }
  100% { box-shadow: 0 0 0 0 rgba(180, 83, 9, 0); }
}
.vo-pulse-btn { animation: vo-pulse 1.5s ease-out infinite; }
.vo-pulse-btn:hover { animation: none; }

/* ---- 5. Debug console panel --------------------------------------------- */
.debug-toggle { position: fixed; bottom: 20px; right: 20px; background: #1a1a1a; color: white; border: none; border-radius: 50%; width: 44px; height: 44px; cursor: pointer; font-size: 18px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; }
.debug-panel { position: fixed; bottom: 80px; right: 20px; width: 480px; max-width: 90vw; height: 360px; background: #1a1a1a; color: #e5e5e3; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); z-index: 1001; display: none; flex-direction: column; font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 11px; }
.debug-panel.open { display: flex; }
.debug-header { padding: 10px 14px; background: #2a2a28; border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center; }
.debug-title { color: #5dcaa5; font-weight: 600; font-size: 12px; }
.debug-actions { display: flex; gap: 6px; }
.debug-btn { padding: 3px 8px; background: #444; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 10px; }
/* The title and the Request Feature button are ONE left-hand group: .debug-header is
   justify-content:space-between, so a third direct child would be spaced out into the
   middle of the bar rather than sitting next to the title. */
.debug-titlebar { display: flex; align-items: center; gap: 10px; }
/* The one button in this panel that does something for the USER rather than for
   debugging, so it is the one that is coloured. */
.debug-btn-feature { background: #7c3aed; font-weight: 600; }
.debug-btn-feature:hover { background: #6d28d9; }
.debug-log { flex: 1; overflow-y: auto; padding: 10px 14px; }
.debug-entry { margin-bottom: 4px; word-break: break-all; }
.debug-time { color: #888780; }
.debug-ok { color: #5dcaa5; }
.debug-err { color: #ff7a7a; }
.debug-warn { color: #ffd166; }
.debug-req { color: #6cb3ff; }
.debug-info { color: #c4c1b7; }

/* ---- 6. Decorative animated header tree --------------------------------- */
/* The tree emoji in the header sways like it is in the wind, and the wind
   strength changes with the time of day. Purely cosmetic. */
header { position: relative; overflow: visible; }
#header-tree {
  display: inline-block;
  transform-origin: 50% 92%;
  cursor: pointer;
  transition: filter 2s ease;
  user-select: none;
}
/* Calm dawn breeze */
#header-tree.breeze { animation: tree-breeze 4.5s ease-in-out infinite; }
@keyframes tree-breeze {
  0%, 100% { transform: rotate(-1deg); }
  50% { transform: rotate(2.5deg) skewX(1deg); }
}
/* Midday sway */
#header-tree.sway { animation: tree-sway 3s ease-in-out infinite; }
@keyframes tree-sway {
  0%, 100% { transform: rotate(-2deg) skewX(-1deg); }
  35% { transform: rotate(5deg) skewX(2.5deg); }
  55% { transform: rotate(3deg) skewX(1.5deg); }
  75% { transform: rotate(4.5deg) skewX(2deg); }
}
/* Golden-hour gusts: irregular, stronger */
#header-tree.gusty { animation: tree-gusty 2.4s cubic-bezier(.36,.07,.19,.97) infinite; }
@keyframes tree-gusty {
  0%, 100% { transform: rotate(-2deg); }
  12% { transform: rotate(8deg) skewX(5deg); }
  28% { transform: rotate(1deg) skewX(0.5deg); }
  44% { transform: rotate(10deg) skewX(6deg); }
  60% { transform: rotate(2.5deg) skewX(1deg); }
  78% { transform: rotate(7deg) skewX(4deg); }
}
/* Night storm: dramatic thrashing */
#header-tree.storm { animation: tree-storm 1.15s cubic-bezier(.36,.07,.19,.97) infinite; }
@keyframes tree-storm {
  0%, 100% { transform: rotate(-5deg) skewX(-3deg); }
  18% { transform: rotate(13deg) skewX(9deg); }
  32% { transform: rotate(2deg) skewX(1deg); }
  52% { transform: rotate(15deg) skewX(10deg); }
  68% { transform: rotate(5deg) skewX(3deg); }
  84% { transform: rotate(11deg) skewX(7deg); }
}
/* Time-of-day colour tints */
#header-tree.tint-dawn { filter: sepia(0.25) saturate(1.2) brightness(1.05); }
#header-tree.tint-golden { filter: sepia(0.45) saturate(1.5) hue-rotate(-12deg) brightness(1.02); }
#header-tree.tint-night { filter: brightness(0.72) saturate(0.75) hue-rotate(15deg); }

/* Drifting leaves that blow off the tree */
.leaf-particle {
  position: absolute;
  pointer-events: none;
  font-size: 13px;
  z-index: 50;
  animation: leaf-fly var(--dur, 3s) ease-in forwards;
}
@keyframes leaf-fly {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0.95; }
  60%  { opacity: 0.9; }
  100% { transform: translate(var(--dx, 300px), var(--dy, 50px)) rotate(var(--spin, 720deg)); opacity: 0; }
}

/* ---- Modal (barcode assignment) ----------------------------------------- */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 2000; display: flex; align-items: flex-start; justify-content: center; padding-top: 72px; }
.modal-box { background: white; border-radius: 12px; padding: 20px; width: 540px; max-width: 92vw; max-height: 80vh; overflow-y: auto; box-shadow: 0 12px 40px rgba(0,0,0,0.25); }
.modal-result { padding: 10px 12px; border: 1px solid #e5e5e3; border-radius: 6px; margin-bottom: 6px; cursor: pointer; font-size: 13px; }
.modal-result:hover { background: #f5f3ff; border-color: #c4b5fd; }
/* ---- A pick row's border repeats its On-hand verdict ---------------------- */
/* Set by fillProductStockLines (read.js) once the figures land, so the border says the
   same thing the coloured "On hand" figure says: green there is free stock, red none,
   blue the product cannot hold stock at all. A row whose stock could NOT be read keeps
   the ordinary grey border — that is "not known", not "out of stock".
   ⚠ These must stay AFTER .modal-result:hover (equal specificity, later wins), and each
   needs its own :hover (0,3,0) or the plain rule would swallow the hover feedback. */
.modal-result.stock-available { border-color: #0f6e56; }
.modal-result.stock-none      { border-color: #a32d2d; }
.modal-result.stock-nonstock  { border-color: #2563eb; }
.modal-result.stock-available:hover { background: #f0f7f4; border-color: #0b5442; }
.modal-result.stock-none:hover      { background: #fdf3f3; border-color: #7f2323; }
.modal-result.stock-nonstock:hover  { background: #eef3fd; border-color: #1d4ed8; }

/* ---- Full-screen order/create PAGES ------------------------------------- */
/* A create-order modal shown as an inline page (not a floating box): strip the
   fixed overlay + centering so it flows in the content area under the header/nav,
   just like a normal section. The inner box keeps rounded corners + padding and a
   light fill so the content sits inset, matching the View-order screen's
   .order-detail box. Toggled on by the router. */
.modal-overlay.page-view { position: static; inset: auto; background: transparent; z-index: auto; display: block; padding-top: 0; }
.modal-overlay.page-view .modal-box { max-height: none; overflow: visible; box-shadow: none; background: #fafaf8; border-radius: 8px; padding: 16px; }

/* ---- Receive: quantity stepper (− [n] +) -------------------------------- */
.recv-step { width: 26px; height: 32px; background: #f0efe9; border: none; border-radius: 4px; cursor: pointer; font-weight: 700; font-size: 15px; line-height: 1; color: #1a1a1a; }
.recv-step:hover { background: #e2e0d8; }
.recv-step-max { background: #ede9fe; color: #5b21b6; }
.recv-step-max:hover { background: #ddd2fb; }
/* Hide the browser's native number spinners — the − / + buttons replace them. */
.recv-input::-webkit-inner-spin-button, .recv-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.recv-input { -moz-appearance: textfield; }

/* ---- Sales list rows ---------------------------------------------------- */
/* Whole row is clickable (opens the order). The off-white base lives on the CELLS (not
   the <tr> — an author rule can't override an inline <tr> background-colour in Chrome,
   so hover couldn't darken it there). Hover darkens ONLY that cell base. The full-opacity
   left colour bar is a pseudo-element on the first cell, driven by the per-row --row-bar
   custom property — so it (and every tag/pill colour) stays exactly put on hover; nothing
   "lights up". No --row-bar → no bar (plain off-white row). */
.sales-row { cursor: pointer; }
.sales-row td { background-color: #faf9f6; transition: background-color 0.08s; }
.sales-row:hover td { background-color: #eeece3; }
.sales-row td:first-child { position: relative; }
.sales-row td:first-child::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--row-bar, transparent);
}

/* View-order Print / Email dropdown menu items */
.vo-menu-item { display: block; width: 100%; text-align: left; padding: 9px 14px; background: none; border: none; font-size: 13px; color: #1a1a1a; cursor: pointer; white-space: nowrap; }
.vo-menu-item:hover:not([disabled]) { background: #f0efe9; }
.vo-menu-item[disabled] { color: #b3b1a9; cursor: default; }

.scan-row { display: flex; align-items: center; gap: 10px; background: white; border: 1px solid #e5e5e3; border-radius: 6px; padding: 8px 12px; margin-bottom: 6px; }
.scan-row .name { flex: 1; font-weight: 500; font-size: 13px; }
.scan-row .bc { font-family: monospace; font-size: 11px; color: #888780; }

/* ---- Settings tab ------------------------------------------------------- */
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
.settings-field label { font-size: 12px; font-weight: 600; color: #1a1a1a; display: block; margin-bottom: 4px; }
.settings-field .hint { font-size: 11px; color: #888780; margin-bottom: 6px; }
.settings-field input { width: 100%; padding: 9px 12px; border: 1px solid #d3d1c7; border-radius: 6px; font-size: 13px; }
@media (max-width: 700px) { .settings-grid { grid-template-columns: 1fr; } }

/* Variant/option values sub-line shown under a product name (search lists, SO/PO
   lines, scan rows, stock-take rows). Sits between the name and the smaller
   "in stock" row in size. Filled in by hydrateVariantLines() in variants.js. */
.variant-line { font-size: 12px; color: #6b6a63; font-weight: 500; line-height: 1.3; margin: 1px 0 0; }
.variant-line .variant-opt { color: #a19f97; font-weight: 600; text-transform: none; letter-spacing: 0; }
.variant-line .variant-sep { color: #cdcbc2; }
/* Equivalent products on the same sub-line ("Replaced by 1148-200-0250") — variants.js formatProductEquivalents */
.variant-line .equivalent-note { color: #185fa5; }
.variant-line .equivalent-sku { font-family: monospace; font-size: 11.5px; }

/* ---- "+ Create new sales order", on every screen ------------------------ */
/* Sits directly under the nav so it is in the same place whatever you are doing.
   Hidden on Settings and Users (syncNewSaleButton in main.js). */
/* ONE row: the sales button plus whichever section's own create button applies, side
   by side. They used to stack, because each section carried its own button at the top
   of its panel — a second bar under the first. */
.new-sale-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 20px; }
.new-sale-btn {
  padding: 10px 20px; background: #0f6e56; color: white; border: none; border-radius: 6px;
  font-size: 13px; cursor: pointer; font-weight: 600;
}
.new-sale-btn:hover { background: #0d5c48; }
/* The Products tab's "Adjust stock" — BLUE, so it reads as a different kind of action from the
   green create buttons beside it; its own hover, or the shared rule would turn it green. */
.new-sale-btn.adjust-stock-btn { background: #1d4ed8; }
.new-sale-btn.adjust-stock-btn:hover { background: #1e40af; }
.new-sale-btn.training-btn { background: #0e7490; }   /* Send to Training — its own colour beside the green sales button */
.new-sale-btn.training-btn:hover { background: #155e75; }

/* ---- Create-sale POP-UP -------------------------------------------------- */
/* Properly centred, both ways. The app's other modals are top-aligned 72px down,
   which suits a short dialog; this form is the size of a page, so centring it reads
   as deliberate rather than dropped in a corner.
   ⚠ Safe to centre vertically because .modal-box is capped at 80vh and scrolls
   inside itself — a tall form can never push its own top off the screen. */
.modal-overlay.centred { align-items: center; padding: 16px; overflow: auto; }
/* ⚠ `margin:auto` on the flex child, not just align-items:center — when a box is
   TALLER than the viewport, a centred flex item has its top clipped and cannot be
   scrolled to. Auto margins centre it while keeping both ends reachable. */
.modal-overlay.centred > .modal-box { margin: auto; }
/* ---- The create-sale pop-up holds ONE height ----------------------------- */
/* The wizard's three screens are wildly different lengths — pick a customer, confirm
   their details, then a basket that grows with every scan — so the box used to jump
   about and re-centre under the reader on every step. A floor of 700px keeps it still.
   ⚠ Only the POP-UP: the side panel is already full height, and the routed page view
   flows down the content area. ⚠ `.modal-box` still caps at 80vh and scrolls inside
   itself, so a long basket never pushes its own top off screen. */
.modal-overlay.centred#new-sales-modal > .modal-box { min-height: 700px; }
/* The pop-up is ALWAYS wide enough for the pricing PREVIEW column (soPreviewPriceList) — it
   used to grow when a preview opened, and watching it expand was jarring (asked for).
   ⚠ !important because the box's width is an INLINE style (1100px, which the routed page and
   the side panel still rely on); its inline max-width:96vw still caps it on a small screen. */
.modal-overlay.centred#new-sales-modal > .modal-box { width: 1280px !important; }

/* ---- Create-sale SIDE PANEL --------------------------------------------- */
/* The third way a new sale can open (⚙ Settings ▸ General, per user): docked down
   the right so the page stays visible and usable beside it.
   ⚠ The form was built for a full-width page, so a plain narrow column would put
   its wide tables and 8-column line-item grid into a horizontal scroll. This
   restyles it for the narrower measure instead: the panel scrolls vertically, the
   inner box loses its page chrome, and the line-item table is allowed to scroll
   sideways INSIDE its own container rather than stretching the panel. */
.modal-overlay.side-panel {
  position: fixed; inset: 0 0 0 auto;      /* dock right, full height */
  width: 560px; max-width: 96vw;
  background: transparent; z-index: 9000;  /* under the real modals (10000+) */
  display: block; padding: 0;
  box-shadow: -8px 0 32px rgba(0,0,0,0.18);
  border-left: 1px solid #d9d7cd;
}
/* ⚠ !important is load-bearing here: the box carries an INLINE style="width:1100px"
   (which the full-width page view needs), and an inline width beats any stylesheet
   rule. Without this the form is 1100px wide inside a 560px panel — it spills off
   the right of the screen and half the form is unreachable. */
.modal-overlay.side-panel .modal-box {
  width: 100% !important; max-width: none !important; height: 100%; max-height: none;
  border-radius: 0; box-shadow: none; background: #fafaf8;
  padding: 16px 18px; overflow-y: auto; overflow-x: hidden;
}
/* The finished sale is shown INSIDE the panel too, and the View-order screen is a wide
   table — let it scroll sideways in its own box rather than stretching the panel. */
.modal-overlay.side-panel #order-page { overflow-x: auto; }
/* Wide content (the line-item table) scrolls inside itself, never widening the panel. */
.modal-overlay.side-panel .modal-box table { min-width: 520px; }
.modal-overlay.side-panel #so-items,
.modal-overlay.side-panel #so-bundle-parts { overflow-x: auto; }
/* Stack the field rows that sit side by side on a full-width page. */
.modal-overlay.side-panel .input-row { flex-wrap: wrap; }
.modal-overlay.side-panel .input-row > * { min-width: 0; }
/* The page is still usable beside the panel, so shift the whole app across rather
   than letting the panel sit on top of it. ⚠ .container is the ONE wrapper around
   header + nav + every section, so moving it moves the lot in one go. */
/* ⚠ `width: auto` matters as much as the margin: .container is `width: 100%`, and a
   100%-wide box ignores its own right margin and simply overflows — the panel would
   sit ON TOP of the page instead of beside it. */
body.side-panel-open .container { margin-right: 576px; margin-left: 0; max-width: none; width: auto; }
@media (max-width: 900px) {
  /* No room to work beside it — the panel takes the screen, like the pop-up would. */
  .modal-overlay.side-panel { width: 100vw; }
  body.side-panel-open .container { margin-right: 0; }
}

/* ---- The finished sale, shown on its own -------------------------------- */
/* After a sale is created, its order page is shown as a FINISHING screen: proof it
   went through, and a last chance to fix anything missed. The nav banner is hidden
   while it is up (body.sale-finished) — it is not a continuation of the main system,
   and the way out is the Done button on the page itself. */
body.sale-finished .nav,
body.sale-finished .new-sale-bar { display: none; }
.sale-finished-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: #e8f5f0; border: 1px solid #a9dbc9; border-radius: 8px;
  padding: 10px 14px; margin-bottom: 12px;
}
.sale-finished-bar .msg { font-size: 13px; color: #0f5c46; font-weight: 600; }
.sale-finished-bar .done {
  padding: 8px 18px; background: #0f6e56; color: white; border: none;
  border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer;
}

/* ---- "Here is where you were" — the row you came back from ----------------
   Back from a sale returns to the exact row that was clicked and flashes it, because
   on a list of two hundred orders the alternative is hunting for your place again.
   ⚠ Three slow cycles then STOP: faster than 3 flashes/second is a photosensitive-
   seizure risk (WCAG 2.3.1), and a highlight that never stops stops being a signal.
   ⚠ A row already carries a --row-bar gradient and a category tint, so this washes
   OVER them with a translucent amber rather than replacing a background it does not
   own. */
@keyframes sales-row-return {
  0%, 100% { background-color: transparent; }
  50%      { background-color: rgba(224, 168, 60, 0.32); }
}
.sales-row-return > td { animation: sales-row-return 0.9s ease-in-out 3 both; }
/* Less motion asked for → a steady tint that fades on its own when the class is
   removed. The point is to be FOUND, which does not require movement.
   ⚠ No !important needed: these cells carry no inline background (unlike the serial
   boxes), so a plain class declaration wins on its own. */
@media (prefers-reduced-motion: reduce) {
  .sales-row-return > td { animation: none; background-color: rgba(224, 168, 60, 0.28); }
}
/* The same finite pulse for anything that is not a table row (live updates, 🧪 Testing ▸
   Live updates previews). */
.live-flash { animation: sales-row-return 0.9s ease-in-out 3 both; }
@media (prefers-reduced-motion: reduce) {
  .live-flash { animation: none; background-color: rgba(224, 168, 60, 0.28); }
}

/* The View-order review pill opens a status picker when clicked (voOpenReviewPillPicker). */
#vo-review-status:not(:empty), #vo-stock-in > span { cursor: pointer; }
#vo-review-status:not(:empty):hover, #vo-stock-in > span:hover { filter: brightness(0.95); box-shadow: 0 0 0 2px rgba(24,95,165,0.25); }

/* 🧪 Shopify Images ▸ All of Shopify — one continuous list, THREE cards a row (§297); fewer only on a narrow window. */
.si-all-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: start; }
@media (max-width: 980px) { .si-all-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { .si-all-grid { grid-template-columns: minmax(0, 1fr); } }

/* 🏠 Dashboard (js/dashboard.js) — the header title is the way home */
.header-home { cursor: pointer; }
.header-home:hover { text-decoration: underline; text-underline-offset: 3px; }
.dash-btn { font-size: 15px; font-weight: 600; padding: 12px 18px; border-radius: 8px; border: 1px solid #c9c7c0; background: #f7f6f2; color: #1a1a1a; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; }
.dash-btn:hover { background: #efede6; }
.dash-responded-table { width: 100%; }
.dash-responded-table td { vertical-align: top; }
.dash-responded-table td.dash-said { font-size: 12px; color: #444; max-width: 420px; overflow-wrap: anywhere; }
.dash-btn-row { display: flex; flex-wrap: wrap; gap: 10px; }
a.dash-btn { text-decoration: none; }
.dash-range { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; font-size: 13px; color: #444; margin-bottom: 12px; }
.dash-range input[type=date] { margin-left: 4px; padding: 5px 8px; border: 1px solid #d3d1c7; border-radius: 6px; font: inherit; }
.dash-range-btn { padding: 6px 14px; border-radius: 6px; border: none; background: #0f6e56; color: white; font-weight: 600; cursor: pointer; }
.dash-range-btn.dash-range-today { background: #f7f6f2; color: #1a1a1a; border: 1px solid #c9c7c0; }
.dash-day { margin: 16px 0 6px; padding-bottom: 4px; border-bottom: 2px solid #e5e5e3; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 14px; }
.dash-day-total { font-weight: 700; }
.dash-sales-table { width: 100%; table-layout: fixed; }  /* every day's table lines up */
.dash-sales-table th:nth-child(1) { width: 16%; } .dash-sales-table th:nth-child(2) { width: 10%; } .dash-sales-table th:nth-child(3) { width: 34%; } .dash-sales-table th:nth-child(4) { width: 26%; } .dash-sales-table th:nth-child(5) { width: 14%; }
.dash-sales-table td { overflow-wrap: anywhere; }
.dash-sales-table td.num, .dash-sales-table th.num { text-align: right; white-space: nowrap; }

/* Product editor ▸ Details: the product's image (products.js pedPaintImage, §371) */
.ped-image-wrap { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.ped-image-frame { width: 140px; height: 140px; border: 1px solid #e5e5e3; border-radius: 8px; background: white; display: flex; align-items: center; justify-content: center; overflow: hidden; flex: 0 0 auto; color: #b4b2a9; font-size: 12px; }
.ped-image-frame img { max-width: 100%; max-height: 100%; object-fit: contain; }
.ped-image-side { flex: 1 1 220px; font-size: 12px; line-height: 1.6; }
.ped-image-btns { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.ped-image-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid #c9c7c0; background: #f7f6f2; color: #1a1a1a; font-size: 12px; cursor: pointer; }
.ped-image-btn.primary { background: #0f6e56; border-color: #0f6e56; color: white; font-weight: 600; }
.ped-image-btn:disabled { opacity: 0.6; cursor: default; }

/* Products list: each product's picture (products.js productThumbsFill, §373) */
.prod-thumb { width: 44px; height: 44px; border: 1px solid #e5e5e3; border-radius: 6px; background: white; display: flex; align-items: center; justify-content: center; overflow: hidden; color: #b4b2a9; font-size: 10px; text-align: center; line-height: 1.1; }
.prod-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }
.prod-thumb-none { color: #b4b2a9; font-size: 9px; }

/* ===========================================================================
   🌙 DARK MODE — base colours for what has no colour of its own (js/theme.js).
   Every rule is under :root[data-theme="dark"], so LIGHT MODE NEVER MATCHES ANY OF THIS — and wrapped in
   :where(), so it is the WEAKEST rule: any colour an element already has (white text on a green button) wins.
   The rest of the page (styles.css's own rules + every inline style) is converted by theme.js
   while dark mode is on, and put back exactly when it is switched off.
   =========================================================================== */
:root[data-theme="dark"] { color-scheme: dark; }                      /* scrollbars, date pickers, checkboxes */
:where(:root[data-theme="dark"]) body { color: #e6e5e1; }
:where(:root[data-theme="dark"]) input:not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="range"]),
:where(:root[data-theme="dark"]) select,
:where(:root[data-theme="dark"]) textarea { background-color: #1e1f22; color: #e6e5e1; border-color: #3d3e42; }
:where(:root[data-theme="dark"]) input::placeholder,
:where(:root[data-theme="dark"]) textarea::placeholder { color: #8a8983; }
:where(:root[data-theme="dark"]) button { color: #e6e5e1; }
:where(:root[data-theme="dark"]) a { color: #7fb2ea; }
:where(:root[data-theme="dark"]) option { background-color: #1e1f22; color: #e6e5e1; }
