/* ============================================================================
   JustKnits — "Mill" redesign theme  (ported from the Claude Design handoff
   assets/theme.css + assets/swatch.css)
   ----------------------------------------------------------------------------
   A warm, editorial operations system. Parchment surfaces, ink + terracotta +
   pine palette pulled from the yarn-swatch tones, Spectral display serif over a
   Hanken Grotesk UI grotesk. Reuses the knit-swatch texture + the existing
   censor mechanic (.jk-censored / .jk-money — see accounting-censor.js).

   Loaded globally from base.html (after base.css so Mill tokens win). Component
   classes are authored bare (.btn/.card/.pill/.field…) exactly as the prototype
   so templates read like the design source.
   ============================================================================ */

:root {
  /* ---- Surfaces (warm neutrals) ---- */
  --paper:        #F4F1EA;   /* app background — warm parchment */
  --paper-2:      #EDE8DC;   /* sunken wells, rails */
  --surface:      #FCFAF4;   /* cards, panels — paperwhite */
  --surface-2:    #FBF9F4;   /* tinted surface, table head */
  --border:       #E5DFD1;   /* hairline */
  --border-2:     #D8D0BD;   /* stronger / inputs */

  /* ---- Ink ramp (warm near-blacks) ---- */
  --ink:          #211C16;   /* headings, primary text */
  --ink-2:        #514A3E;   /* body */
  /* --ink-3 / --ink-4 carry real text (meta, captions, legends), so both clear
     WCAG AA 4.5:1 on --paper and --surface (ink-3 also on the --slate-50 row
     tint). ink-4 sits at the AA floor; ink-3 is one step darker to keep the
     hierarchy. Was #837A69 / #A89E89 (3.8 / 2.4:1 on paper). Same hue, lower
     lightness. tests/test_ink_contrast_tokens.py pins this. */
  --ink-3:        #6A6255;   /* muted labels — 5.33 paper / 5.76 surface / 5.06 slate-50 */
  --ink-label:    #706755;   /* form labels — the lightest step that clears 4.5:1 on
                                 --surface, --paper AND --paper-2 */
  --ink-4:        #776D58;   /* faint meta, placeholder — 4.53 paper / 4.89 surface */
  /* Non-text only: disabled or decorative ICONS (never text, which keeps ink-4).
     WCAG 1.4.11 asks 3:1 of graphics, not 4.5:1. The old #A89E89 is 2.35:1 on
     paper, so this is the lightest same-hue step that clears 3:1. Pinned by
     tests/test_ink_contrast_tokens.py. */
  --ink-disabled: #8C826D;   /* 3.37 paper / 3.64 surface / 3.20 slate-50 / 3.23 pine-50 row tints */

  /* ---- Brand accent: terracotta (knit-clay) ---- */
  --clay:         #B0492B;
  --clay-hover:   #97391F;
  --clay-50:      #FBEDE6;
  --clay-100:     #F4D8CA;
  --clay-700:     #7E3019;

  /* ---- Semantic (warmed) ---- */
  --pine:         #2F6B4F;   /* revenue / positive / in-stock */
  --pine-50:      #E5EFE8;
  --pine-100:     #CFE4D5;
  --pine-700:     #1F4F39;

  /* Sales Performance comparison palette — categorical hues indexed by age 1..4.
     All four clear 3:1 against --surface; the normal-vision adjacent floor is
     ΔE 15.8.

     ACCESSIBILITY, LOAD-BEARING: the worst adjacent CVD pair is --chart-past-3
     / --chart-past-4 at ΔE 7.9 under deuteranopia. That is inside the 6–8 legal
     band only with mandatory secondary encoding: direct year tags and the
     labelled legend. Keep the 2.4px current-year vs 1.6px comparison stroke as
     an additional current/past channel, and revalidate any palette/encoding change.
     A 6th simultaneous line would push adjacent-pair CVD below the legal band,
     which is why the year picker caps at 4 prior years.
     static/js/sales-charts.js repeats these as literals (SVG attributes cannot
     read custom properties); keep the two in step. */
  --chart-past-1: #C1782B;   /* last year — ochre */
  --chart-past-2: #6A5AA8;   /* violet */
  --chart-past-3: #B85C7A;   /* rose */
  --chart-past-4: #1B8EA6;   /* oldest offered — teal */

  /* The Sales by Country comparison bars reuse --chart-past-1..4 above, indexed
     by the year's AGE — never by its index in the selection, so adding a year
     never repaints the others. Deliberately NOT a second set of tokens: the bar
     and the chart line for a given year must be one colour by construction, and
     the CVD analysis noted above was done on these values. NOTE the "3:1" above
     is measured against --surface; the panel tracks its bars on --paper-2, where
     --chart-past-1 reaches only 2.87:1 at full opacity — which is why those bars
     carry a 1px inset edge rather than relying on the fill. TWIN: the PAST array
     in static/js/sales-charts.js (SVG presentation attributes cannot read custom
     properties, so that copy is literals). A test pins the two equal. */

  --brick:        #C13A37;   /* danger / negative / sale tag */
  --brick-50:     #FBE9E7;
  --brick-100:    #F5D2CE;
  --brick-700:    #8F2A28;

  --ochre:        #C0871A;   /* caution / low-stock */
  --ochre-50:     #F8EFD7;
  --ochre-100:    #EFDFB3;
  --ochre-700:    #8A5E0E;

  --plum:         #6D4A8A;   /* admin / re-orders */
  --plum-50:      #EFE8F3;
  --plum-100:     #DFD0E6;
  --plum-700:     #4E3266;

  --slate:        #3E5C7E;   /* info / calendar */
  --slate-50:     #E7ECF2;
  --slate-100:    #CFDAE6;
  --slate-700:    #2C4259;

  /* ---- Radii ---- */
  --r-sm:  6px;
  --r:     10px;
  --r-lg:  14px;
  --r-xl:  20px;
  --r-pill: 999px;

  /* ---- Shadows (warm, low) ---- */
  --sh-sm: 0 1px 2px rgba(60,48,30,.06);
  --sh:    0 1px 3px rgba(60,48,30,.08), 0 1px 2px rgba(60,48,30,.05);
  --sh-md: 0 6px 16px -6px rgba(60,48,30,.16), 0 2px 6px rgba(60,48,30,.06);
  --sh-lg: 0 18px 40px -12px rgba(50,40,24,.26), 0 6px 14px rgba(50,40,24,.08);

  /* ---- Motion ---- */
  --ease:       cubic-bezier(.22,.61,.36,1);
  --ease-press: cubic-bezier(.34,1.4,.5,1.01);
  --dur-fast: .16s;
  --dur:      .28s;

  /* ---- Type ---- */
  --font-ui:      'Hanken Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-display: 'Spectral', Georgia, 'Times New Roman', serif;
  --font-mono:    'Spectral', ui-monospace, monospace;
}

/* ============================ Base ============================ */
.jk-mill,
.jk-mill * { box-sizing: border-box; }
body.jk-mill {
  margin: 0;
  font-family: var(--font-ui);
  background: var(--paper);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: "tnum" 0;
}
.font-display { font-family: var(--font-display); font-weight: 600; letter-spacing: -.01em; }
.tnum { font-variant-numeric: tabular-nums; }
.font-mono { font-family: var(--font-mono); }

/* Eyebrow / uppercase label */
.eyebrow {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink-3);
}

.jk-mill ::selection { background: var(--clay-100); color: var(--clay-700); }

/* The `hidden` attribute must actually hide. The UA stylesheet's
   [hidden]{display:none} is beaten by ANY author rule that sets display, and
   eleven component classes here do (.pill, .chip, .dot, …). So an element written
   as `<a class="pill" hidden>` renders as an empty coloured nub, and JS toggling
   `el.hidden = true` silently fails to hide it. Caught on the Sales Performance
   drill pill, which sat visible-but-empty under the legend at all times. */
[hidden] { display: none !important; }

/* Focus ring — the shared palette ring. A :focus-visible-only selector that
   wants THIS ring joins this list (no :hover here, so an engine that cannot
   parse :focus-visible drops nothing it would otherwise have drawn); a rule
   that pairs the ring with a hover state must NOT be merged in. Two controls
   (.login-toggle, .jk-qp-grip) draw their own outline ring instead.
   .jk-rowlink's underline stays in its own rule below. */
.jk-mill .focusable:focus-visible,
.jk-mill .jk-rowlink:focus-visible,
.jk-mill .jk-row-iconbtn:focus-visible,
.jk-mill .jk-perm-jump:focus-visible,
.jk-mill .jk-popover-panel a:focus-visible,
.jk-mill .jk-menu a:focus-visible,
.jk-mill .jk-msub:focus-visible,
.jk-mill button.already-taken-info:focus-visible,
.jk-mill input:focus-visible, .jk-mill select:focus-visible,
.jk-mill textarea:focus-visible, .jk-mill button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--clay-50), 0 0 0 1px var(--clay);
}

/* ============================ Scrollbars ============================ */
.thin-scroll::-webkit-scrollbar { height: 8px; width: 8px; }
.thin-scroll::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 99px; }
.thin-scroll::-webkit-scrollbar-track { background: transparent; }

/* ============================ Buttons ============================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-ui); font-weight: 600; font-size: 14px;
  border-radius: var(--r); border: 1px solid transparent; cursor: pointer;
  padding: 9px 16px; transition: background var(--dur-fast) var(--ease),
    border-color var(--dur-fast), color var(--dur-fast), transform var(--dur-fast) var(--ease-press), box-shadow var(--dur-fast);
  white-space: nowrap; user-select: none; line-height: 1.1; text-decoration: none;
}
.btn:active { transform: scale(.97); }
.btn-primary { background: var(--clay); color: #fff; box-shadow: var(--sh-sm); }
.btn-primary:hover { background: var(--clay-hover); color: #fff; }
.btn-ink { background: var(--ink); color: var(--paper); }
.btn-ink:hover { background: #34291c; color: var(--paper); }
.btn-danger { background: var(--brick); color: #fff; box-shadow: var(--sh-sm); }
.btn-danger:hover { background: var(--brick-700); color: #fff; }
.btn-ghost { background: var(--surface); color: var(--ink-2); border-color: var(--border-2); box-shadow: var(--sh-sm); }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--ink-4); color: var(--ink-2); }
.btn-outline { background: var(--surface); color: var(--clay-700); border-color: var(--clay); box-shadow: var(--sh-sm); }
.btn-outline:hover { background: var(--clay-50); color: var(--clay-700); }
.btn-quiet { background: transparent; color: var(--ink-2); }
.btn-quiet:hover { background: var(--paper-2); color: var(--ink-2); }
.btn-sm { padding: 6px 12px; font-size: 13px; border-radius: var(--r-sm); }
.btn-lg { padding: 12px 22px; font-size: 15px; }
.btn-block { width: 100%; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* icon press */
.press { transition: transform var(--dur-fast) var(--ease-press), background var(--dur-fast), color var(--dur-fast); }
.press:active { transform: scale(.9); }

/* ============================ Cards ============================ */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--sh);
}
.card-hover { transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease), border-color var(--dur); }
.card-hover:hover { box-shadow: var(--sh-md); border-color: var(--border-2); }

/* ============================ Pills / chips ============================ */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: var(--r-pill);
  line-height: 1.4; white-space: nowrap;
}
/* Loads after Tailwind, so .pill's display out-orders .hidden: the chat inbox's
   reload hint rendered visible on every load. */
.pill.hidden { display: none; }
.dot { width: 6px; height: 6px; border-radius: 99px; flex: none; }

.chip {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-size: 13px; font-weight: 600; padding: 7px 14px; border-radius: var(--r-pill);
  border: 1px solid var(--border-2); background: var(--surface); color: var(--ink-2);
  cursor: pointer; transition: all var(--dur-fast) var(--ease); text-decoration: none;
}
.chip:hover { border-color: var(--ink-4); }
.chip.is-active { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* ---- Entity-type chips (balance-sheet supplier/client picker) ----
   The picker flattens FOUR entity types — Customer, Supplier, Account,
   Transport Company — into ONE ranked list, so two rows can carry the same
   name and mean different things. Warehouse staff worked around that by typing
   "-Transport" into every transport company's NAME, which corrupts the stored
   name for the shipment-batch PDF, the transport-costs report and the SNS
   reconciler (verify_amount_owed_gui.py). These chips replace that convention.

   Authored HERE, not as Tailwind utilities, on purpose: the chip markup is
   built inside a JS template literal in renderSuggestions(), and the Tailwind
   scanner only globs the template roots for whole class literals — a
   utility that exists only inside a JS string is invisible to it and ANY
   `npm run build` can purge it (that is how text-blue-700 was dropped from
   under dropdowns.js). A jk-mill.css class needs no build step at all.

   BOX METRICS COME FROM `.pill`. The chip renders as
   `class="pill jk-type-chip jk-type-chip--<type>"` and adds only colour, a
   border and the marker shapes. It used to re-declare every one of .pill's box
   properties at smaller values (10px / 700 / 1px 7px / gap 5px, plus uppercase
   and letter-spacing), which cost twice. 10px was the only non-media-query
   prose size in this file, i.e. a new desktop type step below every existing
   one. And the labels are TRANSLATED — the zh-hans set is a handful of CJK
   glyphs, where `text-transform: uppercase` is a no-op and `letter-spacing`
   only pushes 8-stroke characters apart — so a Chinese-reading operator paid
   12px to 10px on a money-entry screen for a small-caps effect they never see.
   Nothing here may re-introduce a Latin-only treatment a CJK reader pays for.

   THREE channels carry the type, never colour alone: the translated label
   text, the hue, and the .dot SHAPE (circle / square / diamond / bar).
   Every foreground/background pair below clears WCAG AA — measured 4.97:1
   (transport, the floor) to 8.88:1 (account) against the chip fill, and
   higher against the white dropdown panel behind it; inheriting .pill's 12px
   only widens the margin those figures were measured at.

   Background and colour are both stated explicitly and never inherited, so the
   chip is legible on whatever surface it lands on. There is deliberately NO
   prefers-color-scheme block: this app ships no dark theme (no `dark:` variant
   in any template, no prefers-color-scheme rule in output.css), and the
   dropdown panel these sit in is a hard-coded bg-white — so a chip that went
   dark on its own would be a dark chip on a white panel. */
.jk-type-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 3px; }
/* The transparent border is load-bearing, not a placeholder: it reserves the
   2px of box the AMBIGUOUS modifier below needs, so an ambiguous row is never
   2px taller than the rows it sits between — in a list whose whole job is
   side-by-side comparison, that jitter is the thing being compared. */
.jk-type-chip { border: 1px solid transparent; }
/* currentColor keeps the marker and its label one ink by construction, so a
   palette edit can never leave the two disagreeing. */
.jk-type-chip .dot { background: currentColor; }

.jk-type-chip--customer  { background: var(--slate-50);  color: var(--slate);      border-color: var(--slate-100); }
.jk-type-chip--supplier  { background: var(--pine-50);   color: var(--pine-700);   border-color: var(--pine-100); }
.jk-type-chip--account   { background: var(--plum-50);   color: var(--plum-700);   border-color: var(--plum-100); }
.jk-type-chip--transport { background: var(--ochre-50);  color: var(--ochre-700);  border-color: var(--ochre-100); }
/* Customer-group product ownership (docs/designs/customer-group-rule-sets.md,
   Issue 5): one pill per row, pin wins. JS swaps these four literals (the veto
   swap on untick, Unpin, un-veto); they are jk-mill classes, so they need no
   Tailwind build and no safelist entry (the safelist guard checks output.css). */
.jk-type-chip--pinned     { background: var(--pine-50);  color: var(--pine-700);  border-color: var(--pine-100); }
.jk-type-chip--from-rules { background: var(--slate-50); color: var(--slate);     border-color: var(--slate-100); }
.jk-type-chip--vetoed     { background: var(--ochre-50); color: var(--ochre-700); border-color: var(--ochre-100); }
.jk-type-chip--will-veto  { background: var(--ochre-50); color: var(--ochre-700); border-color: var(--ochre-700); border-style: dashed; }
.jk-type-chip--excluded   { background: var(--paper-2); color: var(--ink-3); border-color: var(--border); }

/* Shape is the second channel — it survives greyscale, colour-vision
   deficiency and a mis-rendered palette, none of which the hue does. */
.jk-type-chip--customer  .dot { border-radius: 99px; }
.jk-type-chip--supplier  .dot { border-radius: 1px; }
.jk-type-chip--account   .dot { border-radius: 1px; transform: rotate(45deg); }
.jk-type-chip--transport .dot { width: 10px; height: 3px; border-radius: 99px; }

/* Collision marker: this exact name is also carried by an entity of ANOTHER
   type in the same result set, so the operator must pick deliberately.

   It is a MODIFIER ON THE TYPE CHIP, never a second chip. A second chip
   carrying the English "Same name, different type" measured ~200px against a
   ~268px inner panel width — the panel floors at 300px wide and is capped at
   240px tall — leaving ~60px for a type chip that needs ~79px. So it wrapped
   to a second line on EVERY ambiguous row at the floor width, taking the row
   from 57px to 80px and the visible row count from ~4.2 to ~3.0, on exactly
   the rows the operator opened the list to compare. That wrap was English-only
   (the shorter CJK phrase fits), so a zh-hans check never reproduced it. As a
   modifier it costs zero extra vertical pixels.

   It states NEITHER background NOR color: the chip keeps its type hue, so
   ambiguity can never read as a fifth entity type. Both of its own visual
   channels are non-colour — a dashed border in the type's own ink, and a
   hollow ring after the label — and the phrase itself stays in the a11y tree
   through .jk-type-chip-note below. */
.jk-type-chip--ambiguous { border-style: dashed; border-color: currentColor; }
.jk-type-chip-amb {
  width: 8px; height: 8px; border-radius: 99px; flex: none;
  box-shadow: inset 0 0 0 2px currentColor;
}
/* The collision phrase in the accessibility tree at zero layout cost. `title`
   is an accessible DESCRIPTION, announced inconsistently and never on touch,
   so the phrase has to be real text somewhere. Deliberately NOT Tailwind's
   `sr-only`: that utility is emitted only while some template still carries
   the whole literal, and a purge would turn this text VISIBLE and wrap the row
   to two lines — the exact regression this modifier exists to remove, arriving
   silently. An absolutely-positioned child is not a flex item, so it takes no
   part in the chip's gap either. */
.jk-type-chip-note {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ============================ Inputs ============================ */
.field {
  width: 100%; font-family: var(--font-ui); font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--r);
  padding: 10px 13px; transition: box-shadow var(--dur-fast), border-color var(--dur-fast);
}
.field::placeholder { color: var(--ink-4); }
.field:focus { outline: none; border-color: var(--clay); box-shadow: 0 0 0 3px var(--clay-50); }
.select {
  appearance: none; -webkit-appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23837A69' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center; padding-right: 36px;
}
/* A `.select` inside a `.jk-fields` wrapper: the wrapper's `background` shorthand (specificity
   0,1,1) would otherwise wipe `.select`'s chevron image and override its padding-right. Re-assert
   them at `.jk-fields select.select` (0,2,1). `appearance:none` still comes from `.select`, so the
   native arrow stays hidden. Opt-in — only affects selects that carry the `.select` class. */
.jk-fields select.select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23837A69' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center; padding-right: 34px;
}
/* The picker POPUP (the option list) is OS-drawn — on Windows Chromium it is the flat
   Win32 white-and-blue list under whichever styled box sits above it (`.select`,
   `.jk-fields select`, `select.jk-input`, the inline-styled strays). `appearance:
   base-select` (Chromium 135+) hands the popup to CSS; every other engine ignores the
   whole block and keeps the native picker. Bare `select` so it reaches all of them —
   `.select` is restated because its own `appearance: none` (0,1,0) outranks the element
   selector. Fine-pointer only: on a phone the native sheet beats a 32px-row popup.
   `::picker-icon` is hidden ONLY on `.select`, the one shape that paints its own chevron
   via background-image; every other select keeps the UA arrow as its affordance.
   Mirrors `.jk-menu`. */
@supports (appearance: base-select) {
  @media (pointer: fine) {
    /* base-select's UA box is transparent with a wrapping value; floor it at (0,0,1) so
       every classed rule still wins. Centred selects: the box is inline-flex now, where
       text-align is inert — centre the flex row instead (and pull the UA arrow in). */
    select { appearance: base-select; background-color: var(--surface); white-space: nowrap; }
    .select { appearance: base-select; }
    select.text-center { justify-content: center; }
    select.text-center::picker-icon { margin-inline-start: 0; }
    .select::picker-icon { display: none; }
    select::picker(select) {
      appearance: base-select; margin: 4px 0; padding: 6px; min-width: anchor-size(width);
      background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
      box-shadow: var(--sh-lg); max-width: calc(100vw - 16px);
    }
    select option {
      padding: 8px 12px; border-radius: var(--r-sm); font-family: var(--font-ui);
      font-size: 14px; color: var(--ink); cursor: pointer;
      white-space: normal; overflow-wrap: anywhere;
    }
    select option::checkmark { display: none; }
    select option:disabled { color: var(--ink-disabled); cursor: default; }
    select option:enabled:hover { background: var(--paper-2); }
    select option:focus-visible { background: var(--paper-2); outline: 2px solid var(--clay); outline-offset: -2px; }
    select option:checked { background: var(--clay-50); font-weight: 600; }
  }
}

/* qty stepper */
.stepper { display: inline-flex; align-items: stretch; border: 1px solid var(--border-2); border-radius: var(--r); overflow: hidden; background: var(--surface); }
.stepper button { width: 32px; display: grid; place-items: center; color: var(--ink-2); background: transparent; border: none; cursor: pointer; }
.stepper button:hover { background: var(--surface-2); }
.stepper input { width: 40px; border: none; border-left: 1px solid var(--border); border-right: 1px solid var(--border); text-align: center; font-weight: 600; font-size: 14px; color: var(--ink); font-family: var(--font-ui); }
.stepper input:focus { outline: none; background: var(--clay-50); }

/* ============================ Tables ============================ */
.th {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); text-align: center; padding: 10px 14px; white-space: nowrap;
}
.trow { transition: background var(--dur-fast); }
.trow:hover { background: var(--surface-2); }
/* Quiet list-row link (the customer name on the sales dashboard's Top Customers /
   Top Re-Order Customers rows): ink, no decoration, underline only on hover and
   keyboard focus. The base state lives HERE, not inline on the anchor, so the
   hover/focus rules win by the cascade with no !important. `display: block` lets
   `.truncate` sit on the anchor itself rather than on an overflow:hidden parent
   that would clip the focus ring. Scoped to the class, not `.trow a`: several
   dashboard rows wrap their whole content in an <a class="trow flex …"> that must
   not underline. `:hover` and `:focus-visible` are SEPARATE rules on purpose: an
   engine that does not parse `:focus-visible` drops the whole selector list it
   sits in, which would take the hover underline with it. The focus RING is not
   written here: `.jk-mill .jk-rowlink:focus-visible` sits in the shared
   "Focus ring" list above, so the palette ring has one definition; this rule
   owns only the underline and the radius the ring is clipped to. */
.jk-rowlink { color: inherit; text-decoration: none; display: block; }
.jk-rowlink:hover { text-decoration: underline; text-underline-offset: 2px; }
.jk-rowlink:focus-visible {
  text-decoration: underline;
  text-underline-offset: 2px;
  border-radius: 2px;
}
/* Group edit form: section jump chips in the sticky permission toolbar
   (templates/management/group_form.html). Rendered as `chip jk-perm-jump`:
   .chip owns the pill; this only compacts it for a toolbar row. .is-empty =
   the search left that section with no match — the chip stays (the row does
   not reflow as you type) and a click clears the search, so it is dimmed by
   a dashed border and --ink-label (AA on --surface), not by opacity.
   Two-class selectors so these beat .chip on specificity, not source order. */
.chip.jk-perm-jump { flex: none; padding: 5px 11px; font-weight: 500; }
.chip.jk-perm-jump [data-app-counter] { font-size: 12px; font-weight: 400; color: var(--ink-label); }
.chip.jk-perm-jump [data-app-counter]:empty { display: none; }
.chip.jk-perm-jump.is-empty { border-style: dashed; background: transparent; color: var(--ink-label); }

/* Row cells: content sits in the center of the cell, both axes (mirrors .th).
   Scoped to .jk-mill (the <body> class from base.html) so standalone print/PDF
   documents keep their own layout. Specificity (0,1,2) intentionally beats
   per-cell text-left/right utilities (0,1,0). */
.jk-mill table td { text-align: center; vertical-align: middle; }
/* ---- Editor tables --------------------------------------------------------
   The rule above is right for read-only lists and wrong for editors. Its
   specificity (0,1,2) deliberately beats per-cell text-left/right utilities
   (0,1,0), so an editor table cannot opt out one cell at a time — every one
   would need an inline style, and the first person to forget gets a centred
   column of product codes.

   Opt the whole table in with .jk-table-editor: text-heavy cells go left, and
   a column marks itself numeric with data-align="right" (or re-centres with
   data-align="center") on BOTH its <th> and its <td>. Specificity here is
   (0,2,2), so it wins without any !important.

       <table class="jk-no-stack jk-table-editor">
         <th class="th">Product</th>            <!-- left, the default -->
         <th class="th" data-align="right">Boxes</th>
         ...
         <td data-align="right">12</td>

   Editor tables also want .jk-no-stack: the ≤900px block below turns tables
   into labelled cards, which is right for a list and wrong for a form. */
.jk-mill table.jk-table-editor td,
.jk-mill table.jk-table-editor th.th { text-align: left; }
.jk-mill table.jk-table-editor td[data-align="right"],
.jk-mill table.jk-table-editor th.th[data-align="right"] { text-align: right; }
.jk-mill table.jk-table-editor td[data-align="center"],
.jk-mill table.jk-table-editor th.th[data-align="center"] { text-align: center; }
/* Block-level flex wrappers directly inside cells ignore the cell's text-align —
   center them too. Deeper flexes need no help: their wrappers are inline-flex
   (centered by text-align) or content-sized flex items. .justify-between is
   spared: it spans the cell on purpose. */
.jk-mill table td > .flex:not(.justify-between) { justify-content: center; }
/* Documents printed from inside the app shell (e.g. the D.D.T. transport
   document) opt out of the centered-cell default: their layout tables read as
   classic left-aligned paper forms. Wrap the document in .jk-doc-flow. */
.jk-mill .jk-doc-flow table td { text-align: left; vertical-align: middle; }

/* ---- Mobile table density (≤640px) ----
   The tables that still scroll horizontally on a phone — the .jk-no-stack editor
   tables — get tighter cell padding + typography here (plus momentum scrolling) so
   more fits at once. Read-only list tables instead collapse into labeled cards (see
   the ≤900px block below), so this density no longer applies to them. .jk-table-wrap
   / .overflow-x-auto are the reusable horizontal-scroll containers. */
.jk-table-wrap { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.jk-mill .overflow-x-auto { -webkit-overflow-scrolling: touch; }
@media (max-width: 640px) {
  .jk-mill table.jk-no-stack { font-size: 12px; }
  .jk-mill table.jk-no-stack .th { padding: 7px 9px; font-size: 10px; letter-spacing: .04em; }
  .jk-mill table.jk-no-stack td { padding: 7px 9px; }
}

/* ---- Mobile: stack wide tables into compact cards (≤900px screens) ----
   Below 900px every read-only list table collapses from a horizontally-scrolling
   grid into a vertical stack of compact cards: <thead> is hidden and each
   tbody/tfoot row becomes a card. mobile-tables.js copies the header text onto
   each cell as data-label="…", wraps its content in .jk-cellval, and classifies
   the leading cells of each row:
     .jk-card-title  the first cell holding TEXT (the record's name/number) —
                     spans the card's first line, bold, no label.
     .jk-card-lead   text-less cells BEFORE the title (row checkbox, pin star,
                     the clients-owed risk strip) — pinned in a left gutter,
                     which the row opens with .jk-has-lead.
   Every other labelled cell is a "LABEL over value" slot in a 2-column grid
   (3 on tablets), so a 10-column record costs ~5 short lines. Position decides
   nothing: a photo or a disabled "Checked" box in column 1 is not the title.
   Rows the labeler skips (colspan totals / empty-state / header-less tables)
   fall back to full-width centered lines. Desktop (≥901px) and print are
   untouched (`screen` in the query — an A4 print area is under 900px). Opt out
   with .jk-no-stack — the interactive editor tables and the .jk-doc-flow
   transport document both carry it. NOTE: do NOT add a blanket
   `.overflow-x-auto { overflow-x: visible }` here — that class is the horizontal
   scroller for every sub-nav and the shop category row. */
.jk-only-stacked { display: none !important; }   /* shown only inside the ≤900px block */
@media screen and (max-width: 900px) {
  #reorderPickRoot .rm-pickall-m { margin-bottom: 10px; }
  #reorderPickRoot .rm-pickall-m label { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; font-size: 14px; color: var(--ink-2); cursor: pointer; }
  #reorderPickRoot .btn, #reorderPickRoot .jk-seg-item { min-height: 44px; }
  #reorderPickRoot table:not(.jk-no-stack) tr.jk-has-lead { padding-left: 62px; }
  #reorderPickRoot table:not(.jk-no-stack) tr.is-picked { background: var(--clay-50); }
  @media (max-width: 640px) {
    #reorderPickBar { padding-bottom: env(safe-area-inset-bottom); }
    #reorderPickBar > div { flex-wrap: wrap; gap: 8px; padding-inline: 12px; }
    #reorderPickBar .rm-offpage { display: none; }
    #reorderPickModal { padding: 8px; }
    #reorderPickModal #reorderMatchForm { max-width: 100%; max-height: calc(100dvh - 16px); }
    #reorderPickModal #reorderMatchForm .rm-pick-fields { grid-template-columns: 1fr; }
    #reorderPickModal .rm-pick-footer .btn { flex: 1; }
  }
  .jk-mill table:not(.jk-no-stack) { display: block; width: 100%; min-width: 0 !important; }
  .jk-mill table:not(.jk-no-stack) > thead { display: none; }
  .jk-mill table:not(.jk-no-stack) > tbody,
  .jk-mill table:not(.jk-no-stack) > tfoot { display: block; }

  .jk-mill table:not(.jk-no-stack) > tbody > tr,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); position: relative;
    gap: 6px 14px; margin: 0 0 6px; padding: 8px 11px;
    border: 1px solid var(--border); border-radius: var(--r);
    background: var(--surface); box-shadow: var(--sh-sm);
  }
  .jk-mill table:not(.jk-no-stack) > tbody > tr:last-child,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr:last-child { margin-bottom: 0; }
  .jk-mill table:not(.jk-no-stack) > tbody > tr.jk-has-lead,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr.jk-has-lead { padding-left: 36px; }

  /* base cell: a full-width centered line. Rows the labeler skips (colspan totals,
     empty-state, header-less tables) keep this; consecutive ones keep a hairline
     so a totals row's figures don't run together. */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td {
    display: block; grid-column: 1 / -1; text-align: center; padding: 0 !important;
    border: 0 !important; white-space: normal; font-size: 13px; line-height: 1.35;
  }
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td:not([data-label]):not(:empty) ~ td:not([data-label]):not(:empty),
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td:not([data-label]):not(:empty) ~ td:not([data-label]):not(:empty) {
    border-top: 1px solid var(--border) !important; padding-top: 6px !important;
  }

  /* a row header (<th scope="row">, e.g. the dashboard's chart-data table) is
     the card's first line */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > th {
    display: block; grid-column: 1 / -1; grid-row: 1; text-align: left !important; padding: 0 !important;
    border: 0 !important; font-size: 14px; font-weight: 600;
  }

  /* labeled cells: one grid slot each, small label stacked over its value */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td[data-label],
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td[data-label] {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
    grid-column: auto; min-width: 0; text-align: left;
  }
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td[data-label]::before,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td[data-label]::before {
    content: attr(data-label); max-width: 100%;
    font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    color: var(--ink-3); white-space: normal; overflow-wrap: anywhere;
  }
  /* drop empty cells (placeholders in totals rows, blank optional columns;
     .jk-cell-empty = whitespace-only, tagged by the JS). After the [data-label]
     rule: equal specificity, so source order decides. */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td:empty,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td:empty,
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td.jk-cell-empty,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td.jk-cell-empty { display: none; }

  /* card title: first line, full width, no label. grid-row:1 lifts it above any
     text-less cell (a photo) that precedes it in the markup. */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td.jk-card-title,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td.jk-card-title {
    grid-column: 1 / -1; grid-row: 1; font-size: 14px; font-weight: 600; color: var(--ink);
  }
  /* lead controls: pinned in the row's left gutter, full card height (so the
     clients-owed risk strip draws as a rail, a checkbox sits at the top) */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td.jk-card-lead,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td.jk-card-lead {
    display: block; position: absolute; top: 8px; bottom: 8px; left: 11px;
  }   /* display beats the :empty rule above (same specificity, later): the strip is an empty <td> */
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td.jk-card-title::before,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td.jk-card-title::before,
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td.jk-card-lead::before,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td.jk-card-lead::before,
  .jk-mill table:not(.jk-no-stack) > tbody > tr > td[data-label=""]::before,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr > td[data-label=""]::before { content: none; }

  /* value wrapper (added by mobile-tables.js): stacks a multi-child cell's content.
     min-width:0 + overflow-wrap let a long unbreakable value break instead of
     spilling into the neighbouring grid slot; badge groups wrap for the same reason. */
  .jk-mill table:not(.jk-no-stack) .jk-cellval {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
    min-width: 0; max-width: 100%; text-align: left;
    white-space: normal; overflow-wrap: anywhere; word-break: break-word;
  }
  /* children are content-sized in the column flex; cap them so a .truncate inside
     (avatar + email rows) has a width to truncate against */
  .jk-mill table:not(.jk-no-stack) .jk-cellval > * { max-width: 100%; }
  .jk-mill table:not(.jk-no-stack) .jk-cellval .inline-flex { flex-wrap: wrap; max-width: 100%; }
  /* avatar + truncating name rows keep their one-line truncate */
  .jk-mill table:not(.jk-no-stack) .jk-cellval .flex:not(:has(.truncate)) { flex-wrap: wrap; max-width: 100%; }
  .jk-mill table:not(.jk-no-stack) .jk-cellval > .flex { justify-content: flex-start; }
  /* thumbnails fit their slot. !important because product_list sizes its <img>
     inline (max-width:200px), wider than a half-width phone slot. */
  .jk-mill table:not(.jk-no-stack) .jk-cellval img { max-width: 100% !important; }

  .jk-mill .jk-only-stacked { display: inline-flex !important; }   /* mobile-only controls */
}
@media screen and (min-width: 561px) and (max-width: 900px) {
  .jk-mill table:not(.jk-no-stack) > tbody > tr,
  .jk-mill table:not(.jk-no-stack) > tfoot > tr { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---- Sales work forms on phones ----
   The form pages (sale/return create+edit, delete confirms, customer group, and the core
   customer form/detail that reuse this shell) wrap their body in a centred
   `mx-auto px-*` column inside #jkRoot, which already has a 20px
   gutter — on a phone the two stack into a 36px margin. Drop the inner one there.
   Item rows pair their short fields two-up under a full-width product picker instead
   of one tall field per line (screen only: print keeps the desktop grid). */
@media screen and (max-width: 639px) {
  .jk-sales-shell > .mx-auto[class*="px-"] { padding-left: 0; padding-right: 0; }
}
@media screen and (max-width: 767px) {
  .sale-item > .grid,
  .return-item > .grid:first-child { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 10px; row-gap: 8px; }
  .sale-item > .grid > .jk-row-product,
  .return-item > .grid:first-child > :first-child { grid-column: 1 / -1; }
  .return-item { padding: 12px; }
}
/* Touch: the sales shell's plain checkboxes (13-16px desktop defaults) grow to a
   thumb-sized 20px. This selector is (0,6,1), so a page rule only overrides it from an
   id, an inline width, or by joining the exclusion list (.jk-sale-check 22px, reorders'
   .jk-pick); hidden, sr-only and .jk-seg (visually hidden) inputs are left alone. */
@media (pointer: coarse) {
  .jk-sales-shell input[type="checkbox"]:not(.hidden):not(.sr-only):not(.jk-seg-input):not(.jk-sale-check):not(.jk-pick) { width: 20px; height: 20px; flex: none; }
}
/* .jk-doc-flow paper documents (transport_document) never stack: their tables carry
   .jk-no-stack in the template, and mobile-tables.js also tags any .jk-doc-flow table
   .jk-no-stack at mobile width — so the cell rules above never touch their inline
   borders/padding. (A CSS revert can't restore inline styles past the !important
   cell rules, hence the class-based opt-out instead.) */

/* ---- Phone: editor tables as one small form per row (.jk-edit-stack) ----
   Editors carry .jk-no-stack, so the card rules above leave them alone and they
   side-scroll — fine on a tablet, but on a phone a 7–8-input row squeezed into a
   348px scroller clips the very field being typed in ("JK2…", "Produ"). Below
   640px a table that also carries .jk-edit-stack drops its <thead> and lays each
   row out as a form: mobile-tables.js copies every header onto its cells as
   data-edit-label (re-labelling rows the page clones in later) and the ::before
   paints it as the field label. Default arrangement: 2 columns, first cell full
   width; a page re-maps its own row with grid-template-areas. Inputs go to 16px
   so iOS Safari does not zoom the page on focus, and to 44px for a gloved thumb.
   A header with data-mobile-label="" (row-remove buttons) gets no label. */
@media screen and (max-width: 640px) {
  .jk-mill table.jk-edit-stack { display: block; width: 100% !important; min-width: 0 !important; }
  .jk-mill table.jk-edit-stack > thead { display: none; }
  .jk-mill table.jk-edit-stack > tbody,
  .jk-mill table.jk-edit-stack > tfoot { display: block; border: 0; }
  .jk-mill table.jk-edit-stack > tbody > tr,
  .jk-mill table.jk-edit-stack > tfoot > tr {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
    align-items: end; padding: 14px 14px 16px; border: 0; border-bottom: 1px solid var(--border);
  }
  .jk-mill table.jk-edit-stack > tbody > tr:last-child { border-bottom: 0; }
  /* Tailwind's .hidden (0,1,0) loses to the row's display:grid above; pages
     toggle placeholder rows with the class (batch detail's empty-state row) */
  .jk-mill table.jk-edit-stack > tbody > tr.hidden { display: none; }
  .jk-mill table.jk-edit-stack > tbody > tr > td,
  .jk-mill table.jk-edit-stack > tfoot > tr > td {
    display: block; min-width: 0; width: auto !important; padding: 0 !important;
    border: 0 !important; text-align: left !important; font-size: 14px;
  }
  .jk-mill table.jk-edit-stack > tbody > tr > td:first-child,
  .jk-mill table.jk-edit-stack > tfoot > tr > td:first-child,
  .jk-mill table.jk-edit-stack > tbody > tr > td[colspan] { grid-column: 1 / -1; }
  .jk-mill table.jk-edit-stack td[data-edit-label]:not([data-edit-label=""])::before {
    content: attr(data-edit-label); display: block; margin-bottom: 5px;
    font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
    color: var(--ink-3); overflow-wrap: anywhere;
  }
  .jk-mill table.jk-edit-stack > tfoot > tr > td:first-child[data-edit-label]::before { content: none; }   /* beats the label rule above (0,4,3) */
  .jk-mill table.jk-edit-stack input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
  .jk-mill table.jk-edit-stack select {
    width: 100% !important; min-height: 44px; font-size: 16px; text-align: left !important; text-align-last: auto !important; margin-inline: 0;
  }
  /* inline groups (L × W × H) share their row instead of each claiming 100%;
     the selector repeats the :not() list so it outranks the width rule above */
  .jk-mill table.jk-edit-stack .flex > input:not([type=checkbox]):not([type=radio]):not([type=hidden]) { width: auto !important; flex: 1 1 0; min-width: 0; }
  .jk-mill table.jk-edit-stack td > .btn,
  .jk-mill table.jk-edit-stack td > form > .btn,
  .jk-mill table.jk-edit-stack details .btn { min-height: 44px; }
}

/* ---- Phone: KPI tiles two-up (.jk-kpi-grid) ----
   One tile per row spent a whole phone screen on four numbers before the page
   began. Two-up halves that; the decorative icon chip goes (the label already
   names the figure) so a half-width tile keeps its room for the number. An odd
   last tile takes the full row rather than leaving a hole. !important because
   the tiles size their padding/figure inline. */
@media screen and (max-width: 640px) {
  .jk-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap: 10px !important; }
  .jk-kpi-grid > .card { padding: 14px !important; }
  .jk-kpi-grid .jk-kpi-icon { display: none !important; }   /* inline display:grid on the chip */
  .jk-kpi-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .jk-kpi-grid .font-display.tnum { font-size: 20px !important; }
  /* a 7-digit € figure is one unbreakable token wider than a half tile */
  .jk-kpi-grid .min-w-0 > * { overflow-wrap: anywhere; }
}

/* ---- Touch hit areas the inventory pages were missing ----
   A stacked card's title link (the product name on Stock Levels, Products,
   Volume…) is the card's way in and measured a 19px line. A positioned ::after
   grows the hit box to ~44px without moving the layout; being positioned, it
   paints (and takes taps) above the in-flow label text it overlaps, which is not
   a control. Not on a .truncate link: its overflow:hidden clips the ::after back
   to the text line. Card layout only (≤900px): a tablet rotated past 900px keeps the
   jk-card-title class on a plain table cell. The product page's movement-type
   checkboxes were bare 13px boxes in 20px labels. */
@media screen and (pointer: coarse) and (max-width: 900px) {
  .jk-mill table:not(.jk-no-stack) td.jk-card-title .jk-rowlink:not(.truncate) { position: relative; }
  .jk-mill table:not(.jk-no-stack) td.jk-card-title .jk-rowlink:not(.truncate)::after { content: ""; position: absolute; inset: -12px 0; }
  /* a line under the link (product list's barcode) stays selectable: grow up only */
  .jk-mill table:not(.jk-no-stack) td.jk-card-title .jk-rowlink:not(.truncate):not(:last-child)::after { bottom: 0; }
}
@media (pointer: coarse) {
  #movement-type-filter label,
  #movement-type-filter a { min-height: 44px; display: inline-flex; align-items: center; }
  #movement-type-filter input[type=checkbox] { width: 18px; height: 18px; }
}

/* ============================ Censor (hide figures) ============================
   Reuses the app's existing mechanic: accounting-censor.js toggles .jk-censored
   on #jkRoot and .revealed on individual .jk-money spans. We restyle the blur. */
/* padding enlarges the hover/click hit area (~matching the 7px blur bleed) so the
   cursor needn't be pixel-perfect on the text; the equal negative margin keeps
   layout unchanged (horizontal cancels exactly; inline boxes ignore the vertical
   margin, but the small vertical padding doesn't grow the line box). */
.jk-censored .jk-money { filter: blur(7px); cursor: pointer; user-select: none; border-radius: 4px; padding: 0.2em 0.5em; margin: -0.2em -0.5em; }
/* Once a figure is revealed (click-to-reveal, or "Show amounts" off), it must be
   selectable so prices/costs can be copied — the blur rule above kills user-select. */
.jk-censored .jk-money.revealed { filter: none; user-select: text; cursor: auto; }
/* Hover-to-peek: on pointer devices, moving the cursor over a fogged figure
   reveals it transiently (no click needed); it re-fogs on mouse-out. Click still
   toggles a persistent reveal. Guarded to (hover: hover) so touch taps keep the
   click-to-toggle behaviour instead of a sticky hover. Loaded after output.css so
   this full reveal wins over that layer's partial-blur hover rule. opacity:1 is
   set explicitly so the reveal is self-contained (not reliant on output.css). */
@media (hover: hover) {
  .jk-censored .jk-money:hover { filter: none; opacity: 1; user-select: text; }
  .censored .money:hover { filter: none; opacity: 1; user-select: text; }
}
/* Keyboard parity: reveal a fogged figure when a link/control inside it gains
   focus, so tab users can read what they're about to activate (hover is mouse-
   only). Not gated on (hover: hover) — keyboard focus applies on every device. */
.jk-censored .jk-money:focus-within { filter: none; opacity: 1; user-select: text; }
.censored .money:focus-within { filter: none; opacity: 1; user-select: text; }
.jk-money { transition: none; }
/* prototype-native aliases (bare .money inside .censored) */
.censored .money { filter: blur(7px); cursor: pointer; user-select: none; border-radius: 4px; padding: 0.2em 0.5em; margin: -0.2em -0.5em; }
.censored .money.revealed { filter: none; user-select: text; cursor: auto; }

/* ============================ Avatars ============================ */
.avatar { border-radius: 999px; display: grid; place-items: center; font-weight: 700; flex: none; letter-spacing: .01em; }

/* ============================ Motion ============================ */
@keyframes jk-view-in { from { transform: translateY(8px); } to { transform: none; } }
.view-in { animation: jk-view-in var(--dur) var(--ease); }
@keyframes jk-reveal-up { from { transform: translateY(12px); } to { transform: none; } }
@keyframes jk-pop { 0% { transform: scale(.4); opacity: 0; } 60% { transform: scale(1.12); } 100% { transform: scale(1); opacity: 1; } }
.pop { animation: jk-pop .42s var(--ease-press) both; }
@keyframes jk-spin { to { transform: rotate(360deg); } }
.spin { animation: jk-spin .7s linear infinite; }
@keyframes jk-toast-in { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
.toast-in { animation: jk-toast-in .3s var(--ease) both; }

/* staggered card reveal — visible state is the base style (print/reduced-motion safe) */
.stagger > * { animation: jk-reveal-up .5s var(--ease); }

/* ---- Sales shell: the page settles, the ledger draws itself ----
   Every animation here fills `backwards` only, so nothing lingers once it ends; the
   base style is the visible state, so print, no-animation and reduced-motion all
   show content. The settle is opacity ONLY: a transform, even mid-animation, makes a
   wrapper the containing block for every position:fixed popup/bar inside it.
   `.jk-entering` is removed by _base_sales.html after first load, so a panel shown
   later (display:none -> block) does not replay this fade (the bar wipes below do,
   by design). */
/* from-only: each element ends at its OWN opacity (an SVG opacity="0.4" tail, an
   inline opacity on a block), never forced to 1 mid-animation. */
@keyframes jk-settle { from { opacity: 0; } }
.jk-entering > :not(script, style, [role="dialog"]):not(.fixed):not(.jk-modal-layer):not(.jk-popover-layer):not(.jk-float) {
  animation: jk-settle .3s var(--ease) var(--jk-settle-delay, 0s) backwards;
}
/* Short capped cascade, 160ms ceiling. Positions assume _base_sales.html's #jkRoot
   children: 1 = censor_init script, 2 = page header, 3+ = body blocks. A custom
   property, so excluded overlays never pick up a delay. */
.jk-entering > :nth-child(3) { --jk-settle-delay: 40ms; }
.jk-entering > :nth-child(4) { --jk-settle-delay: 80ms; }
.jk-entering > :nth-child(5) { --jk-settle-delay: 120ms; }
.jk-entering > :nth-child(n+6) { --jk-settle-delay: 160ms; }

/* Bars fill left to right along their track — same reading direction as the figure.
   Not gated on .jk-entering, on purpose: bars revealed later (the "Show N more"
   fold) sweep in on open, like the ones above them did on load. The client-bands
   strip carries the class on its track, so the whole strip sweeps in. */
@keyframes jk-wipe { from { clip-path: inset(0 100% 0 0 round 99px); } to { clip-path: inset(0 0 0 0 round 99px); } }
@keyframes jk-wipe-flat { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0); } }
.jk-sales-shell .jk-bar-fill { animation: jk-wipe .7s var(--ease) .18s backwards; }
.jk-sales-shell .jk-bar-past { animation-delay: .3s; }
.jk-sales-shell .jk-spark-wipe [data-jk-sparkline] svg { animation: jk-wipe-flat .8s var(--ease) .22s backwards; }

/* Performance chart: past years trace first, then the current year runs to today.
   pathLength="1" on the path makes the dash maths unit-free; the 2-unit gap keeps
   the next dash off the path end (a round cap would paint it as a dot). Keep
   .jk-draw-cur delay + duration under DRAW_IN_SETTLE_MS in sales-charts.js. */
/* Starts a hair past 1 so no zero-length dash sits on the path start (a round cap
   would paint it as a dot during the delay). */
@keyframes jk-draw { from { stroke-dashoffset: 1.02; } to { stroke-dashoffset: 0; } }
.jk-draw-in .jk-draw { stroke-dasharray: 1 2; animation: jk-draw .7s var(--ease) .1s backwards; }
.jk-draw-in .jk-draw-cur { animation-duration: .95s; animation-delay: .3s; }
/* Faded after-today tails and year tags wait for the lines to reach them. */
.jk-draw-in .jk-after-draw { animation: jk-settle .3s var(--ease) 1s backwards; }

@media (prefers-reduced-motion: reduce) {
  .jk-mill *, .jk-mill *::before, .jk-mill *::after { animation-duration: .001ms !important; animation-delay: 0ms !important; transition-duration: .001ms !important; }
}

/* ============================ Sticky frosted bars ============================ */
.glass { background: color-mix(in srgb, var(--paper) 78%, transparent); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }

/* hairline divider */
.hr { height: 1px; background: var(--border); border: 0; }

/* link */
.link { color: var(--clay); font-weight: 600; cursor: pointer; }
.link:hover { color: var(--clay-hover); text-decoration: underline; text-underline-offset: 2px; }
/* A .link button can be disabled (the customer-group picker disables bulk-select
   while availability loads). Without this it keeps full clay colour and the
   pointer cursor, so it reads as live while doing nothing when clicked. */
.link:disabled { opacity: .5; cursor: not-allowed; }
.link:disabled:hover { color: var(--clay); text-decoration: none; }

/* detail page main + side layout */
.detail-2col { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(260px, 1fr); gap: 16px; align-items: start; }
@media (max-width: 880px) { .detail-2col { grid-template-columns: 1fr; } }
.kv-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border); }
.kv-row:last-child { border-bottom: 0; }
.kv-row .kv-k { font-size: 13px; color: var(--ink-3); }
.kv-row .kv-v { font-size: 13.5px; font-weight: 600; color: var(--ink); text-align: right; }

/* number flair: ledger column */
.ledger { font-variant-numeric: tabular-nums; font-family: var(--font-ui); }

/* ============================ Responsive helpers ============================ */
.desktop-only { display: flex; }
.mobile-only { display: none; }
@media (max-width: 880px) {
  .desktop-only { display: none !important; }
  .mobile-only { display: grid !important; place-items: center; }
}

/* Staff section nav: condense labels → icon-only before the bar overflows. */
.secbtn { padding: 7px 9px; }
.staff-sections .nav-label { font-size: 13px; }
@media (max-width: 1180px) {
  .staff-sections .secbtn { padding: 8px 8px; }
  .staff-sections .nav-label { display: none !important; }
}

/* cart layout: two columns on wide, stacked on narrow */
.cart-grid { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 860px) {
  .cart-grid { grid-template-columns: 1fr 330px; }
  .cart-summary { position: sticky; top: 84px; }
}

/* ============================================================================
   Overlay stacking ladder — the whole app's z-index budget, in one place.
   ============================================================================
     1800  .jk-toast-layer    toasts + fly-to-cart feedback, above everything;
                              transient feedback must stay visible and does not
                              accept pointer events
     1700  .jk-tip            tooltips explain whatever sits under them
     1650  .jk-dialog-layer   global confirm dialog; may open from any overlay
     1600  .drawer            mobile right drawer
     1500  .drawer-scrim
     1400  .jk-modal-layer    modals: above ALL page chrome
     1300  .jk-popover-layer  page-root menus and suggestion popups: above nav,
                              below every modal surface. Deliberately outranks
                              the mobile drawer (1002): its consumers are
                              focus-anchored popups that close on blur, so a
                              drawer can never open under a live popup
     1002  mobile sidebar     (templates/base.html)
     1001  mobile overlay     (templates/base.html)
     1000  nav.navbar-fixed   (static/css/base.css) — .navdrop's 1200 lives
                              INSIDE this stacking context and never competes
                              at the root, so 1000 is the nav's real ceiling
      990  .jk-float          fixed utility controls (scroll-to-top buttons):
                              above the app sub-navs, below the navbar and the
                              mobile drawer — page furniture, not an overlay
      900  app sub-navs       (including inventory/_components/subnav.html)

   Every modal in this app used to be Tailwind's `z-50`, i.e. 950 below the
   navbar it was supposed to cover: the nav painted on top of the modal and
   stayed undimmed. Twelve overlay sites shared that bug, so the fix is one
   class, not twelve numbers.

   Modals sit BELOW the drawer deliberately — the drawer IS the mobile nav
   surface and is mutually exclusive with a modal in practice.

   Do NOT write `z-[1400]` in a template instead of this class. Tailwind only
   emits literals it finds at build time and that one is not in
   static/dist/output.css, so it would resolve to no rule at all until
   `npm run build` runs — silently, with no error and no 404. */
.jk-toast-layer { z-index: 1800; }
.jk-dialog-layer { z-index: 1650; }
.jk-modal-layer { z-index: 1400; }
.jk-popover-layer { z-index: 1300; }
.jk-float { z-index: 990; }
.jk-subnav-layer { z-index: 900; }   /* app sub-navs (900 rung above) */

/* mobile drawer */
.drawer-scrim { position: fixed; inset: 0; background: rgba(33,28,22,.45); z-index: 1500; animation: jk-view-in .2s var(--ease) both; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(320px, 86vw); z-index: 1600;
  background: var(--surface); box-shadow: var(--sh-lg); padding: 18px;
  display: flex; flex-direction: column; gap: 4px;
  animation: jk-drawer-in .28s var(--ease) both;
}
@keyframes jk-drawer-in { from { transform: translateX(100%); } to { transform: none; } }
.drawer-item {
  display: flex; align-items: center; gap: 12px; padding: 13px 14px; border-radius: var(--r);
  font-size: 15px; font-weight: 600; color: var(--ink-2); background: transparent; border: none;
  cursor: pointer; text-align: left; width: 100%; transition: background var(--dur-fast); text-decoration: none;
}
.drawer-item:hover { background: var(--paper-2); }
.drawer-item.is-active { background: var(--clay-50); color: var(--clay-700); }

/* nav dropdown */
.navdrop {
  position: absolute; top: calc(100% + 6px); left: 0; min-width: 200px; z-index: 1200;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--sh-lg); padding: 6px; animation: jk-view-in .16s var(--ease) both;
}
.navdrop a, .navdrop button {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 9px 11px; border-radius: var(--r-sm); border: none; background: transparent;
  font-size: 13.5px; font-weight: 500; color: var(--ink-2); cursor: pointer; white-space: nowrap; text-decoration: none;
}
.navdrop a:hover, .navdrop button:hover { background: var(--paper-2); color: var(--ink); }
.navdrop a.is-active, .navdrop button.is-active { color: var(--clay); font-weight: 600; }

/* lang switch */
.lang-switch { display: inline-flex; border: 1px solid var(--border-2); border-radius: var(--r-sm); overflow: hidden; }
.lang-switch button { padding: 5px 10px; font-size: 12px; font-weight: 600; border: none; cursor: pointer; background: var(--surface); color: var(--ink-3); }
.lang-switch button.is-active { background: var(--ink); color: var(--paper); }

/* ============================ Login (centered card) ============================ */
.login-page { min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: 40px 20px; background: var(--paper); }
.login-card { width: 100%; max-width: 392px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 18px 40px -24px rgba(50,40,24,.45); overflow: hidden; }
/* 6px three-band knit strip — the lone brand signal in the minimal card. Wool and
   moss are strip-only hues; the middle band reuses the brand --clay on purpose. */
.login-strip { display: flex; height: 6px; }
.login-strip > span { flex: 1; transform-origin: left center; }
.login-strip > span:nth-child(1) { background: #c2a079; }
.login-strip > span:nth-child(2) { background: var(--clay); }
.login-strip > span:nth-child(3) { background: #4f6651; }
/* The page's one authored moment: the three bands knit across on arrival, each
   band picking up as the last lands. Static strip under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .login-strip > span { animation: login-knit .7s cubic-bezier(.16,1,.3,1) both; }
  .login-strip > span:nth-child(2) { animation-delay: .12s; }
  .login-strip > span:nth-child(3) { animation-delay: .24s; }
}
@keyframes login-knit { from { transform: scaleX(0); } }
.login-body { padding: 32px 32px 28px; }
.login-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 28px; }
/* overflow visible so the focus ring isn't clipped; the end buttons carry the radius instead */
.login-head .lang-switch { overflow: visible; }
.login-head .lang-switch button { min-height: 32px; min-width: 40px; }
.login-head .lang-switch button:first-of-type { border-radius: calc(var(--r-sm) - 1px) 0 0 calc(var(--r-sm) - 1px); }
.login-head .lang-switch button:last-of-type { border-radius: 0 calc(var(--r-sm) - 1px) calc(var(--r-sm) - 1px) 0; }
/* lift the focused button so its neighbour's background doesn't cover its ring */
.login-head .lang-switch button:focus-visible { position: relative; z-index: 1; }
@media (pointer: coarse) { .login-head .lang-switch button { min-height: 44px; min-width: 48px; } }
.login-title { font-size: 28px; font-weight: 700; line-height: 1.15; color: var(--ink); margin: 0; letter-spacing: -.02em; text-wrap: balance; }
.login-sub { font-size: 14px; color: var(--ink-3); margin: 8px 0 24px; line-height: 1.5; }
.login-flashes { margin-bottom: 18px; }
.login-flashes .jk-flash:last-child { margin-bottom: 0; }
.login-field { margin-bottom: 16px; }
.login-field--last { margin-bottom: 22px; }
.login-card--wide { max-width: 440px; }
.login-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.login-title--solo { margin-bottom: 22px; }
.login-meta { font-size: 12.5px; color: var(--ink-3); margin: 0 0 24px; }
.login-sub:has(+ .login-meta) { margin-bottom: 10px; }
.login-rules { font-size: 12.5px; color: var(--ink-3); margin: 0 0 22px; padding-left: 18px; line-height: 1.7; list-style: disc; }
/* outcome steps (email sent / reset done / link expired): badge + copy, centred */
.login-state { text-align: center; }
.login-state .login-sub { max-width: 32ch; margin-inline: auto; }
.login-badge { width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; margin: 4px auto 18px; }
.login-badge.is-pine { background: var(--pine-50); color: var(--pine); }
.login-badge.is-brick { background: var(--brick-50); color: var(--brick); }
.login-badge.is-done { border-radius: var(--r-pill); background: var(--pine); color: var(--surface); }
/* every signed-out text input shares the password field's 46px height (it hosts a 44px toggle) */
.login-body.jk-fields input[type=text], .login-body.jk-fields input[type=password],
.login-body.jk-fields input[type=email], .login-body.jk-fields input[type=tel] { min-height: 46px; }
.login-body.jk-fields input[aria-invalid=true] { border-color: var(--brick); }
.login-body.jk-fields input[aria-invalid=true]:focus { border-color: var(--brick); box-shadow: 0 0 0 3px var(--brick-50); }
.login-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 0 18px; }
.login-remember { display: inline-flex; align-items: center; gap: 9px; min-height: 44px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
.login-body.jk-fields .login-remember input[type=checkbox] { width: 17px; height: 17px; margin: 0; }
.login-forgot { font-size: 13px; padding: 12px 0; }
.login-foot { text-align: center; margin: 22px 0 0; padding-top: 18px; border-top: 1px solid var(--border); font-size: 13px; line-height: 1.6; color: var(--ink-3); }
/* password field + show/hide toggle */
.login-pw { position: relative; }
.login-body.jk-fields .login-pw input { padding-right: 48px; }
.login-toggle { position: absolute; right: 1px; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; min-width: 44px; display: grid; place-items: center; border: none; background: transparent; color: var(--ink-3); cursor: pointer; border-radius: 0 calc(var(--r) - 1px) calc(var(--r) - 1px) 0; }
.login-toggle:hover { color: var(--ink-2); background: var(--paper-2); }
.login-toggle:focus-visible { outline: 2px solid var(--clay); outline-offset: 1px; }
/* caps-lock note — ochre caution, not an error (caps lock isn't a failure) */
.login-caps { margin-top: 6px; font-size: 12px; color: var(--ochre-700); align-items: center; gap: 6px; display: none; }
.login-caps.is-on { display: flex; }
/* submit spinner — swaps in for the arrow while the form posts */
.login-spin { display: none; width: 15px; height: 15px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: login-spin .6s linear infinite; }
.btn.is-loading .login-spin { display: inline-block; }
.btn.is-loading .login-arrow { display: none; }
.btn.is-loading:disabled { opacity: 1; cursor: progress; }
@keyframes login-spin { to { transform: rotate(360deg); } }
/* the global reduced-motion rule shortens durations with !important; drop the spin itself instead */
@media (prefers-reduced-motion: reduce) { .login-spin { animation-name: none; } }
@media (max-width: 480px) {
  .login-page { padding: 24px 16px; }
  .login-body { padding: 26px 22px 22px; }
  .login-title { font-size: 26px; }
  /* 16px stops iOS Safari zooming the page on focus */
  .login-body.jk-fields input[type=text], .login-body.jk-fields input[type=password],
  .login-body.jk-fields input[type=email], .login-body.jk-fields input[type=tel] { font-size: 16px; }
  .login-pair { grid-template-columns: 1fr; gap: 16px; }
}

/* ============================ Forced mobile (device=phone preview) ============================ */
body.force-mobile { max-width: 402px; margin: 0 auto; min-height: 100vh; box-shadow: 0 0 0 1px var(--border), 0 30px 60px -20px rgba(50,40,24,.4); background: var(--paper); overflow-x: hidden; }
html:has(body.force-mobile) { background: #E4DDCD; }
body.force-mobile .desktop-only { display: none !important; }
body.force-mobile .mobile-only { display: grid !important; place-items: center; }
body.force-mobile .cart-grid { grid-template-columns: minmax(0, 1fr) !important; }
body.force-mobile .cart-summary { position: static !important; }

/* ============================================================================
   Generated knit-swatch texture (from assets/swatch.css)
   A muted heathered yarn color overlaid with two crossing rib lines + a darker
   ribbed hem band. Fills shop product tiles with no photo. Set base tone via the
   inline `--swatch` var:  <div class="jk-swatch" style="--swatch:#a8593f"></div>
   ============================================================================ */
.jk-swatch {
  position: relative;
  background-color: var(--swatch, #6b7f99);
  background-image:
    radial-gradient(120% 75% at 50% -8%, rgba(255, 255, 255, .42), transparent 58%),
    radial-gradient(110% 70% at 50% 120%, rgba(0, 0, 0, .18), transparent 55%),
    repeating-linear-gradient(63deg, rgba(255, 255, 255, .13) 0 2px, transparent 2px 9px),
    repeating-linear-gradient(-63deg, rgba(255, 255, 255, .13) 0 2px, transparent 2px 9px),
    repeating-linear-gradient(0deg, rgba(0, 0, 0, .05) 0 1px, transparent 1px 18px);
  background-size: 100% 100%, 100% 100%, 18px 18px, 18px 18px, 100% 18px;
  overflow: hidden;
}
.jk-swatch::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 26px;
  background-image: repeating-linear-gradient(90deg, rgba(0, 0, 0, .10) 0 2px, rgba(255, 255, 255, .06) 2px 6px);
  opacity: .6;
}
.jk-swatch--thumb::after { height: 12px; }

/* ============================================================================
   Global chrome — top nav + mobile sidebar (base.html)
   Restyles the existing nav structure (dropdown hooks for dropdowns.js are
   preserved in the markup; these classes only re-skin it to Mill).
   ============================================================================ */
.jk-topnav { background: var(--surface); border-bottom: 1px solid var(--border); box-shadow: var(--sh-sm); }
/* The nav holds a lot (8 top-level links for admins + the user cluster). Stop the
   two groups (links / user cluster) from shrinking into each other: with both
   non-shrinking the links can never spill over the user controls — at worst the
   row overflows the (already minimal) gutters instead of overlapping. */
.jk-topnav > div > .flex.justify-between > div { flex-shrink: 0; }
/* Top-level links are text-only so all eight app labels fit without overlapping;
   the per-section icons still appear in the mobile sidebar. Dropdown chevrons are
   kept (.jk-chev) so the menus still read as expandable. */
.jk-navrail .jk-navlink > .jk-ic { display: none; }
/* ...except a navlink that IS its icon (Quick Pages has no text label): the rule
   above would blank it entirely, leaving an empty 16px clickable box (the 8px
   padding either side, with zero content between). Pad it out to a 38x38 square
   to match .jk-star, the other icon-only control in this feature — the rail is
   `hidden md:flex`, so it renders at exactly 768px, a touch width, with only a
   3px gap to the next link. Mobile is unaffected: the sidebar uses .jk-mlink
   with a full "Manage Quick Pages" label.
   Both classes are named in the selector on purpose: .jk-navlink-icon alone ties
   with the rule above at (0,3,0) and would win only by sitting later in the file,
   so reordering these lines would silently reinstate the blank box. */
.jk-navrail .jk-navlink.jk-navlink-icon > .jk-ic { display: block; }
.jk-navrail .jk-navlink.jk-navlink-icon { padding: 0 11px; }
/* .jk-navlink.is-active differs from :hover only by font-weight, which is inert
   on an SVG — so this link, the one that IS active on the Manage page, would show
   no "you are here" at all. Carry it on colour instead, reusing the clay accent
   .jk-star.is-on already uses for the on-state of the same feature. */
.jk-navrail .jk-navlink.jk-navlink-icon.is-active { color: var(--clay); }
.jk-navrail { gap: 3px; }
.jk-wordmark { display: inline-flex; align-items: baseline; gap: 1px; text-decoration: none; line-height: 1; }
.jk-wordmark .w1 { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -.02em; }
.jk-wordmark .w2 { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--clay); letter-spacing: -.02em; }

.jk-navlink {
  display: inline-flex; align-items: center; gap: 5px; height: 38px; padding: 0 8px;
  border-radius: var(--r); font-size: 13.5px; font-weight: 500; color: var(--ink-3);
  text-decoration: none; white-space: nowrap; flex-shrink: 0;
  transition: background var(--dur-fast), color var(--dur-fast);
}
/* The top nav holds many items; keep its chevron tight so labels have room. */
.jk-navlink .jk-chev { margin-left: -1px; }
.jk-navlink:hover { background: var(--paper-2); color: var(--ink); }
.jk-navlink.is-active { background: var(--paper-2); color: var(--ink); font-weight: 700; }

/* dropdown menu card (keeps .{section}-dropdown-menu + hidden for the JS) */
.jk-menu { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); box-shadow: var(--sh-lg); padding: 6px; }
.jk-menu .jk-menu-label { padding: 8px 11px 4px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--ink-4); }
.jk-menu a { display: flex; align-items: center; gap: 9px; padding: 8px 11px; border-radius: var(--r-sm); font-size: 13.5px; font-weight: 500; color: var(--ink-2); text-decoration: none; white-space: nowrap; }
.jk-menu a:hover { background: var(--paper-2); color: var(--clay); }
.jk-menu .jk-menu-sep { height: 1px; background: var(--border); margin: 5px 0; }
/* Toolbar popovers — the <details> menus under a toolbar button (ledger Download +
   Show, sale Download + Fattura, product-list Download). The panel keeps
   `card jk-popover-layer` beside .jk-popover-panel so its z-index rung stays in the
   ladder above; this block owns geometry and row styling only. Rows are <a> on the
   download menus and <label> on the Show menu, so one rule serves both. The row
   recipe is a deliberately LARGER sibling of .jk-menu a (14px/8px 12px against
   13.5px/8px 11px, no font-weight, no nowrap): it reproduces the px-3 py-2 text-sm
   the five menus carried inline, so the extraction changed no page's appearance.
   Only the :hover is shared verbatim. Its :focus-visible ring lives in the shared
   Focus ring list, never beside :hover. */
.jk-popover-menu { position: relative; display: inline-block; }
.jk-popover-menu > summary { list-style: none; cursor: pointer; }
.jk-popover-menu > summary::-webkit-details-marker { display: none; }
.jk-popover-panel { position: absolute; right: 0; top: calc(100% + 4px); min-width: 200px; padding: 6px; }
.jk-popover-panel a, .jk-popover-panel label { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 14px; line-height: 20px; color: var(--ink-2); text-decoration: none; border-radius: var(--r-sm); cursor: pointer; }
.jk-popover-panel a:hover, .jk-popover-panel label:hover { background: var(--paper-2); color: var(--clay); }

/* "Show N more" fold (Sales by Country): native <details>, label swaps on [open].
   The <summary> keeps its native display (a restyled one can lose its disclosure
   role in WebKit); the flex row is the inner .jk-more-label. */
.jk-more > summary { list-style: none; cursor: pointer; margin-left: -6px; width: max-content; border-radius: var(--r-sm); font-size: 13px; font-weight: 500; color: var(--ink-label); transition: color .12s ease, background .12s ease; }
.jk-more > summary::-webkit-details-marker { display: none; }
.jk-more > summary:hover { color: var(--clay); background: var(--paper-2); }
.jk-more-label { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px 6px 6px; }
.jk-more > summary svg { transform: rotate(90deg); transition: transform .15s ease; }
.jk-more[open] > summary svg { transform: rotate(-90deg); }
.jk-more[open] > summary .jk-more-show, .jk-more:not([open]) > summary .jk-more-hide { display: none; }
@media (prefers-reduced-motion: reduce) { .jk-more > summary svg { transition: none; } }
@media (pointer: coarse) { .jk-more-label { min-height: 44px; } }

/* Quick Pages star toggle (sits next to page titles on list/index pages) */
.jk-star-form { margin: 0; display: inline-flex; line-height: 0; }
.jk-star { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border: none; background: transparent; color: var(--ink-4); cursor: pointer; border-radius: var(--r-sm); transition: color .12s ease, background .12s ease; }
.jk-star:hover { color: var(--clay); background: var(--clay-50); }
.jk-star svg { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linejoin: round; }
.jk-star.is-on { color: var(--clay); }
.jk-star.is-on svg { fill: currentColor; stroke: currentColor; }
/* Quick Pages keep the menu's flex row and gap; only the desktop label truncates
   inside the fixed-width menu. Mobile labels wrap beside a first-line icon. */
.jk-pin-ic { flex: 0 0 16px; display: block; color: var(--ink-3); }
.jk-menu a.jk-menu-pin .jk-pin-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jk-menu a.jk-menu-pin.is-active { color: var(--clay); font-weight: 700; }
.jk-menu a.jk-menu-pin:hover .jk-pin-ic,
.jk-menu a.jk-menu-pin.is-active .jk-pin-ic { color: var(--clay); }
.jk-msub .jk-pin-label { flex: 1 1 auto; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
.jk-msub .jk-pin-ic { align-self: flex-start; margin-top: 2px; }
.jk-msub:hover .jk-pin-ic, .jk-msub.is-active .jk-pin-ic { color: var(--clay); }
.jk-msub.is-active, .jk-msub.is-active:hover { color: var(--clay-700); font-weight: 600; }

/* nav badge (cart count / unread) */
.jk-navbadge { background: var(--brick); color: #fff; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: inline-grid; place-items: center; padding: 0 4px; line-height: 1; }
/* jk-mill.css loads after Tailwind, so this component class would out-order .hidden;
   re-assert hiding so the count badges collapse to nothing at zero. */
.jk-navbadge.hidden { display: none; }
.pill.hidden { display: none; }   /* same trap: .pill { display:inline-flex } out-orders Tailwind's .hidden */

/* icon button (mobile hamburger / anon controls) */
.jk-iconbtn { width: 40px; height: 40px; border-radius: var(--r); border: none; background: var(--paper-2); color: var(--ink); cursor: pointer; display: grid; place-items: center; }
/* 44px touch floor for the phone-bar icon buttons (hamburger, phone cart) — base.html is their only user */
@media (pointer: coarse) { .jk-iconbtn { width: 44px; height: 44px; } }
.jk-iconbtn:hover { background: var(--border); }
/* after :hover (same specificity) so a tapped phone link keeps its active state */
.jk-iconbtn[aria-current="page"] { background: var(--clay-50); color: var(--clay-700); box-shadow: inset 0 0 0 1px var(--clay-100); }
/* Ghost icon action in table rows / cards (View, Edit, Delete...): 36px hit area, 44px on touch.
   Colour is set per use (inline color: or a page class); a destructive action uses the shared
   .jk-iconbtn-danger modifier below. Focus joins the shared ring list above. */
.jk-row-iconbtn { display: inline-grid; place-items: center; min-width: 36px; min-height: 36px; padding: 0; border: none; border-radius: var(--r-sm); background: none; color: inherit; cursor: pointer; text-decoration: none; }
.jk-row-iconbtn:hover { background: var(--paper-2); }
/* Destructive row action: quiet at rest (a column of red icons is noise), red on intent. */
.jk-iconbtn-danger { color: var(--ink-3); }
.jk-iconbtn-danger:hover, .jk-iconbtn-danger:focus-visible { color: var(--brick-700); background: var(--brick-50); }
/* [data-off] marks a disabled placeholder icon (not a control): no pointer, no hover. */
.jk-row-iconbtn[data-off] { cursor: default; }
.jk-row-iconbtn[data-off]:hover { background: none; }
.jk-row-iconbtn:disabled, .jk-row-iconbtn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; background: none; }
/* A primary action kept focusable while unavailable (its reason sits beside it): must LOOK unavailable. */
.btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
@media (pointer: coarse) { .jk-row-iconbtn { min-width: 44px; min-height: 44px; } }
/* Inside a chip (country sheet city pills): the hit area overhangs the pill vertically
   instead of stretching it, so the chip row keeps its rhythm. */
.jk-chip-iconbtn { margin-block: -10px; }
@media (pointer: coarse) { .jk-chip-iconbtn { margin-block: -14px; } }
/* Keyboard-armed row of a suggestion list (sale-form pickers, jk-sale-combobox.js): the row
   Enter will pick. Distinct from the grey hover wash, and it wins over hover so a resting
   pointer never hides which row is armed: the .jk-sugg-qualified pair out-specifies
   `.jk-sugg:hover` (declared later in this file). */
.jk-sugg-armed, .jk-sugg-armed:hover,
.jk-sugg.jk-sugg-armed, .jk-sugg.jk-sugg-armed:hover { background: var(--clay-50); box-shadow: inset 0 0 0 1px var(--clay-100); }

/* mobile sidebar (Mill) */
.jk-msidebar { background: var(--surface); }
.jk-mlink { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 12px; border-radius: var(--r); font-size: 15px; font-weight: 600; color: var(--ink-2); text-decoration: none; }
.jk-mlink:hover { background: var(--paper-2); }
.jk-mlink.is-active { background: var(--clay-50); color: var(--clay-700); }
.jk-msub { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-radius: var(--r-sm); font-size: 13.5px; color: var(--ink-3); text-decoration: none; }
.jk-msub:hover { background: var(--paper-2); color: var(--ink); }

/* form fields — restyle native/Django-widget inputs inside a .jk-fields wrapper
   (reused by auth + every create/edit form so we never touch the Python forms) */
.jk-fields input[type=text], .jk-fields input[type=email], .jk-fields input[type=password],
.jk-fields input[type=number], .jk-fields input[type=tel], .jk-fields input[type=date],
.jk-fields input[type=search], .jk-fields input[type=url], .jk-fields textarea, .jk-fields select {
  width: 100%; font-family: var(--font-ui); font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--r);
  padding: 10px 13px; transition: box-shadow var(--dur-fast), border-color var(--dur-fast);
  box-shadow: none;
}
.jk-fields input::placeholder, .jk-fields textarea::placeholder { color: var(--ink-4); }
.jk-fields input:focus, .jk-fields textarea:focus, .jk-fields select:focus {
  outline: none; border-color: var(--clay); box-shadow: 0 0 0 3px var(--clay-50);
}
.jk-fields input[type=checkbox], .jk-fields input[type=radio] { accent-color: var(--clay); width: 15px; height: 15px; }
.jk-field-label { display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--ink-label); margin-bottom: 6px; }
/* Sub-label under a legend (From / To on a date pair): the system's small-text size,
   body ink — NOT .jk-field-label (uppercase + tracked, would compete with the legend). */
.jk-caption { display: block; font-size: 12px; font-weight: 500; color: var(--ink-2); margin-bottom: 4px; }
.jk-field-err { margin-top: 6px; font-size: 12.5px; color: var(--brick-700); }

/* App-wide de-blue: color-only field treatment for inputs that are NOT inside a
   .jk-fields wrapper (forms across accounting/sales/inventory). Element-qualified
   (specificity 0,1,1) so it deterministically beats `border-gray-300` and the
   colorless `focus:ring-2` default-blue ring regardless of stylesheet load order.
   Sets ONLY border-color + the terracotta focus ring — never width/padding/
   background — so existing Tailwind sizing utils (w-24, px-2, bg-gray-100,
   text-center) are preserved. Apply by adding `jk-input` to a blue input and
   stripping its border-gray-300 / focus:ring-* / *-blue-* utils. */
input.jk-input, textarea.jk-input, select.jk-input { border-color: var(--border-2); }
input.jk-input:focus, textarea.jk-input:focus, select.jk-input:focus {
  outline: none; border-color: var(--clay); box-shadow: 0 0 0 3px var(--clay-50);
}
/* checkboxes/radios carry a blue check via text-blue-600; clay accent replaces it */
input.jk-input[type=checkbox], input.jk-input[type=radio] { accent-color: var(--clay); }

/* Typeahead input holding a typed name with NO dropdown selection behind it.
   The hidden .supplier-client-id is wiped on every keystroke, so a name sitting
   in the box with no id is a row the server refuses (or, clear-only, silently
   ignores). Stated for :focus too so it beats input.jk-input:focus on
   SPECIFICITY (0,3,1 vs 0,2,1) rather than on source order — reordering two
   lines must not silently un-paint it. */
input.jk-input.is-unlinked {
  background: var(--brick-50);
  border-color: var(--brick);
}
input.jk-input.is-unlinked:focus {
  border-color: var(--brick);
  box-shadow: 0 0 0 3px var(--brick-100);
}

/* period filter chips (dashboards: Today / This Week / This Month / Custom)
   inline-flex + centring is load-bearing, not cosmetic: the executive dashboard's
   facet chips carry an inline `min-height: 44px` for the touch target, and on a
   plain block box every pixel it adds past the content collects at the BOTTOM,
   so the label sits high in its own pill. Measured in a browser with output.css
   and this file loaded in page order, chip "Fit": 9px above the label and 20px
   below; centred, 12.63px both sides. Every other .jk-period sizes to its content
   (measured identical at 101.56x37 link / 84.41x37 button), so its box is
   unchanged. Both numbers need Tailwind's border-box preflight to reproduce — on
   a bare page min-height applies to the CONTENT box and the chip is 60px, not 44. */
.jk-period { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; font-size: 14px; font-weight: 600; border-radius: var(--r); background: var(--paper-2); color: var(--ink-2); text-decoration: none; border: none; cursor: pointer; transition: background var(--dur-fast), color var(--dur-fast); white-space: nowrap; }
.jk-period:hover { background: var(--border); }
.jk-period.is-active { background: var(--ink); color: var(--paper); }

/* Executive dashboard (/dashboard/) phone layout. The four period chips wrapped
   onto two lines at 390px, and the four lead KPI cards stacked 1-up at ~165px each
   (two full screens before the first chart). Phones get one equal-width chip row
   and the lead cards 2-up: icon dropped, the change chip back in DOM order under
   the figure (wider screens lift it top-right via the grid areas). */
.jk-dash-periods { display: flex; flex-wrap: wrap; gap: 8px; }
.jk-dash-pill { margin-left: 6px; }
/* Each card spans four rows of the lead grid and subgrids them, so label, figure
   and foot line up across a row even when one label wraps. Without subgrid the
   rows fall back to the card's own, packed to the top by align-content. */
.jk-kpi { padding: 18px; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "icon delta" "label label" "value value" "foot foot"; align-content: start; align-items: start; }
/* Spanning four lead-grid rows only pays off with subgrid; without it the span would
   add three row gaps of empty height to every card. */
@supports (grid-template-rows: subgrid) {
  .jk-kpi { grid-row: span 4; grid-template-rows: subgrid; row-gap: 0; }
}
a.jk-kpi { text-decoration: none; }
.jk-kpi-icon { grid-area: icon; display: grid; place-items: center; width: 38px; height: 38px; border-radius: 11px; }
.jk-kpi-delta { grid-area: delta; }
.jk-kpi-label { grid-area: label; margin-top: 14px; align-self: end; letter-spacing: .08em; }  /* hugs its figure when the pair's label row is taller */
/* The figure (|eur keeps cents: "-€1,234,567.89") is one token and must never wrap
   mid-number. 24px fits ~14 tabular chars in every card from 640px up. (Not sized
   with container units: container-type implies layout containment, and a contained
   card can no longer be a subgrid.) --len, set by the template, is its character count;
   the phone size reads --n, which is --len except while fogged: then any figure up to
   14 chars is sized as 14, so the font size no longer separates €999 from €999,999
   (the blurred width still roughly does, as for every fogged figure on the site).
   Longer figures keep their own length so a revealed one still fits its card. */
.jk-kpi-value { --n: var(--len, 14); grid-area: value; min-width: 0; white-space: nowrap; font-size: 24px; font-weight: 700; line-height: 1.1; margin-top: 3px; }
#jkRoot.jk-censored .jk-kpi-value { --n: max(var(--len, 14), 14); }
/* Secondary-tier figures sit under the lead's: 18 vs 24px, and on phones 14px against
   the lead's fitted size, which stays at or above 14px for figures up to 14 chars from
   320px up (320px is the supported minimum). */
.jk-kpi2-value { font-size: 18px; font-weight: 700; color: var(--ink); line-height: 1.15; margin-top: 2px; }
.jk-kpi-foot { grid-area: foot; margin-top: 8px; font-size: 12px; color: var(--ink-4); }
.jk-dash-range input[type=date] { width: auto; }
/* 639.98, not 640: Tailwind's sm: starts AT 640px, so the two never both apply. */
@media (max-width: 639.98px) {
  .jk-dash-periods { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; width: 100%; }
  .jk-dash-pill { margin-left: 0; }
  .jk-dash-periods .jk-period { min-height: 44px; padding: 6px 4px; font-size: 13px; line-height: 1.2; white-space: normal; text-align: center; }
  /* One column, icon hidden: the four subgrid rows read label, figure, change, note
     (DOM order), shared with the card beside it so the two figures line up. */
  .jk-kpi { grid-template-columns: minmax(0, 1fr); grid-template-areas: none; padding: 14px; }
  .jk-kpi > * { grid-area: auto; }
  .jk-kpi-icon { display: none; }
  /* Fit the figure's own length (--n chars, from --len, ~0.5em each in tabular Spectral; 0.47
     measured) to the card's content width on a phone, 50vw - 56px (20px page gutters,
     12px gap, 14px padding, 1px border). "€123,456.78" is ~19px at 320, 22px at 390;
     the 13px floor binds only past ~16 chars at 320px. Different-length figures in a
     pair get different sizes, so they share a baseline rather than a top edge. */
  .jk-kpi-value { font-size: clamp(13px, calc((50vw - 56px) / (var(--n, 14) * 0.5)), 22px); align-self: last baseline; }
  .jk-kpi2-value { font-size: 14px; }
  .jk-kpi-label { margin-top: 0; }
  .jk-kpi-delta { margin-top: 6px; }
  .jk-kpi-foot { margin-top: 2px; }
  /* From | To over Apply | Cancel. The shared "→ Start" quick button (dropdowns.js,
     absolutely positioned elsewhere) joins the flow under the To input here, so it
     takes room only while dropdowns.js shows it and can never cover Cancel. */
  .jk-dash-range { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
  .jk-dash-range input[type=date] { width: 100%; }
  .jk-dash-range .date-range-quick-btn { position: static; min-height: 44px; }
  .jk-dash-range > .btn { width: 100%; min-height: 44px; justify-content: center; }
}
/* Below 360px a dd/mm/yyyy date input no longer fits half the card: stack them. */
@media (max-width: 359.98px) {
  .jk-dash-range { grid-template-columns: minmax(0, 1fr); }
}

/* Segmented control: one bordered track, the active segment filled. Links on
   list pages (aria-current="page"); native radios + labels on form filters AND
   on the sale forms' Draft | Confirmed control (`.jk-seg-input:checked +`,
   wrapped in `fieldset.jk-seg-field`). `.jk-seg-item.is-active` is the JS-driven
   button shape — kept in the active-state selector list below with no live
   consumer since the sale forms moved to radios (2026-09-17); removing it is a
   deliberate later change, not an oversight. */
.jk-seg { display: inline-flex; padding: 3px; gap: 2px; border: 1px solid var(--border); border-radius: var(--r); background: var(--paper-2); }
.jk-seg-item { display: inline-flex; align-items: center; gap: 6px; min-height: 38px; padding: 8px 14px; font-size: 13.5px; font-weight: 600; color: var(--ink-2); border: none; background: transparent; border-radius: var(--r-sm); text-decoration: none; cursor: pointer; white-space: nowrap; transition: background var(--dur-fast), color var(--dur-fast); }
.jk-seg-item > svg { width: 16px; height: 16px; flex: none; }
/* Radio shape (form filters): the input is visually hidden but stays in the tree, so
   arrow keys, Tab-to-the-checked-one and the AT radiogroup all come from the UA.
   `.jk-fields input[type=radio]` (0,2,1) still wins width/height (15px) over this
   (0,1,0) rule — harmless: absolutely positioned, clipped and transparent, the box
   paints nothing and takes no flow space. Do not try to out-specify it. */
.jk-seg-input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.jk-seg-item:hover { background: var(--surface); }
/* .jk-seg-input:checked + .jk-seg-item is (0,3,0) — one class above .is-active — so
   checked beats :hover by specificity. */
.jk-seg-item[aria-current="page"], .jk-seg-item[aria-pressed="true"], .jk-seg-item.is-active, .jk-seg-input:checked + .jk-seg-item { background: var(--clay); color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.12); }
/* Radio shape ring: plain :focus, not :focus-visible — the focusable element is the
   clipped 1px input, so on an engine without :focus-visible (legacy Chromium here)
   there is no UA outline to fall back on. :focus matches whenever :focus-visible
   would, so a :focus-visible twin of this ring would add nothing; the pair below
   only SUBTRACTS it on pointer focus. */
.jk-seg-input:focus + .jk-seg-item { box-shadow: 0 0 0 3px var(--clay-50), 0 0 0 1px var(--clay); }
/* Modern engines: no ring on pointer focus; an engine without :focus-visible drops
   both rules and keeps the plain :focus ring above. The second restores the active
   state's own shadow, which the first would otherwise strip from a clicked segment. */
.jk-seg-input:focus:not(:focus-visible) + .jk-seg-item { box-shadow: none; }
.jk-seg-input:checked:focus:not(:focus-visible) + .jk-seg-item { box-shadow: 0 1px 2px rgba(0,0,0,.12); }
/* The ring sits AFTER the active rule: both are (0,2,0) and both set box-shadow, so
   source order decides — the other way round a Tab onto the current segment shows
   nothing (the global button:focus-visible never covers the <a> segments). */
.jk-seg-item:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--clay-50), 0 0 0 1px var(--clay); }
.jk-seg[aria-busy] .jk-seg-item:not([aria-current="page"]) { opacity: .6; }
/* Wrapper for the radio shape: a fieldset is the row's flex ITEM, and a fieldset's UA
   min-inline-size: min-content stops it shrinking inside flex-wrap. */
fieldset.jk-seg-field { min-width: 0; }
/* Sales dashboard chart toggles (series + compare years): keep the 44px touch floor on
   any touch screen, not only at <=640px where every .jk-seg-item already gets it. */
@media (pointer: coarse) { #perfSeries .jk-seg-item, .jk-year-chip { min-height: 44px; } }
/* ≤640px: full-width content-proportional segments, 44px touch targets (one-handed at the dock).
   `flex: 1 1 100%` is what makes it fill the row: every .jk-seg is itself a flex ITEM
   (the shipment header row, the sale forms' centred wrapper), and display:flex alone
   sizes it to its content. Kept
   HERE, after the base rules, not in the file's earlier mobile block: @media adds no
   specificity, so a mobile override that precedes its base rule silently loses. */
@media (max-width: 640px) {
  .jk-seg { display: flex; flex: 1 1 100%; }
  .jk-seg-item { flex: 1 1 auto; justify-content: center; min-height: 44px; min-width: 0; padding: 8px 6px; }
  /* Three nowrap English labels WITH icons need ~282px; a 320px phone gives the
     shipment header ~234px and the card is overflow:clip, so the third segment was
     cut off. Labels alone fit (~212px); the icon is decoration on a phone. */
  .jk-seg-item > svg { display: none; }
  /* flex: 1 1 auto, not the shorthand `flex: 1` (= 1 1 0%): a 0% basis gives every
     segment an equal THIRD regardless of its label, and the reorders filter bar's
     third segment is "Out of Stock" — 76px of text at 13.5px/600 — with 0px of
     side padding left once split three ways at 320px. An auto basis grows all three
     equally from their CONTENT width instead, so the long label keeps its room and
     the short ones give theirs up; min-width: 0 still lets the row itself shrink. */
  /* The full-width bar needs the WRAPPER to be full width: `flex: 1 1 100%` on .jk-seg
     is inert when its parent is a content-sized fieldset. */
  .jk-seg-field { width: 100%; }
}
/* forced-colors (Windows High Contrast) strips backgrounds and box-shadows; the
   radio itself is clipped, so without this the checked segment and the focus
   ring have no indication at all. */
@media (forced-colors: active) {
  .jk-seg-item[aria-current="page"], .jk-seg-item.is-active, .jk-seg-input:checked + .jk-seg-item { forced-color-adjust: none; background: Highlight; color: HighlightText; }
  .jk-seg-input:focus + .jk-seg-item { outline: 2px solid Highlight; outline-offset: 2px; }
  /* nav menu rows (Quick Pages pins + Quick Actions): the shared ring is box-shadow only */
  .jk-mill .jk-menu a:focus-visible, .jk-mill .jk-msub:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; }
  /* the select picker's fills flatten too: restore the UA checkmark and use system Highlight */
  select option::checkmark { display: inline; }
  select option:checked, select option:enabled:hover { forced-color-adjust: none; background: Highlight; color: HighlightText; }
  /* /sales/reorders/ applied-filter tint: it is border-color + background, and HCM replaces
     BOTH, so applied and unapplied controls render identically. Eight of the nine carry their
     own evidence anyway (a typed value, a pill, the checked segment above) — the Date range is
     the exception and it is structural: its From box always renders the 180-day default, so the
     tint is the ENTIRE difference between "default" and "someone set this". outline is the one
     property HCM does not override; -2px keeps it inside the box, clear of the focus ring.
     The text-box selector is a CHILD combinator on purpose: #productSearch is a direct child
     of its .is-applied wrapper, while a tag picker's [data-tp-input] is ALSO type=text but sits
     a level deeper inside .jk-tp-box — a descendant selector drew a second outline inside the
     first on all three pickers. Bare `input` was worse still: it hit the clipped .jk-seg-input
     radios, the range thumbs and the hidden [data-tp-hidden] inputs (/ship 2026-09-22). */
  #filterForm .is-applied > input[type=text], #filterForm .is-applied input[type=number],
  #filterForm .is-applied input[type=date], #filterForm .is-applied .jk-tp-box,
  #filterForm .is-applied .jk-seg { outline: 2px solid Highlight; outline-offset: -2px; }
}

/* autocomplete suggestion dropdown items (sales/inventory search boxes) */
.jk-sugg { padding: 8px 14px; cursor: pointer; }
.jk-sugg:hover, .jk-sugg.is-active { background: var(--paper-2); }

/* sticky section sub-nav tabs (staff sections) */
.jk-subtab { color: var(--ink-3); border-bottom: 2px solid transparent; font-weight: 500; text-decoration: none; transition: color var(--dur-fast), border-color var(--dur-fast); }
.jk-subtab:hover { color: var(--ink); }
.jk-subtab.is-active { color: var(--clay); border-bottom-color: var(--clay); font-weight: 700; }

/* flash messages (Mill tones) */
.jk-flash { border-radius: var(--r); padding: 13px 16px; margin-bottom: 12px; font-size: 13.5px; display: flex; gap: 10px; align-items: flex-start; border: 1px solid var(--border); background: var(--surface); color: var(--ink-2); }
/* jk-mill.css loads after Tailwind, so .jk-flash's display:flex would out-order
   .hidden; re-assert so a JS-toggled flash (e.g. #uploadError) can actually hide. */
.jk-flash.hidden { display: none; }
.jk-flash.is-error { background: var(--brick-50); border-color: var(--brick-100); color: var(--brick-700); }
.jk-flash.is-success { background: var(--pine-50); border-color: var(--pine-100); color: var(--pine-700); }
.jk-flash.is-warning { background: var(--ochre-50); border-color: var(--ochre-100); color: var(--ochre-700); }
.jk-flash.is-info { background: var(--slate-50); border-color: var(--slate-100); color: var(--slate-700); }

/* App-wide de-blue (Tier 3) ----------------------------------------------------
   jk-mill.css loads after Tailwind output.css, so these win on source order.
   (1) Warm the neutral Tailwind border utility to the Mill hairline in ONE rule
       (replaces ~60 cool-gray card/container borders without per-site edits).
   (2) .jk-info — a Mill "info" surface (slate), mirroring .jk-flash.is-info, for
       the blue info callouts. Children inherit the slate text/icon color. */
.border-gray-300 { border-color: var(--border); }
.jk-info { background: var(--slate-50); border: 1px solid var(--slate-100); color: #34465c; }
.jk-info h1, .jk-info h2, .jk-info h3, .jk-info h4, .jk-info strong { color: #2c3e54; }
/* Balance-sheet status bar — JS toggles these instead of Tailwind bg-blue/red/gray.
   Balanced was BLUE (off-brand); de-blued to pine (positive). */
.jk-status { border-top-width: 4px; border-top-style: solid; }
.jk-status-neutral { background: var(--paper-2); border-top-color: var(--border-2); }
.jk-status-balanced { background: var(--pine-50); border-top-color: var(--pine); }
.jk-status-unbalanced { background: var(--brick-50); border-top-color: var(--brick); }
/* purple -> Mill plum (the account / admin accent), incl. JS-built dropdown rows */
.jk-plum { color: var(--plum); }
.jk-plum-surface { background: var(--plum-50); }

/* ============================================================================
   Compatibility shim — re-color legacy Tailwind utilities to Mill on pages
   still authored in the old blue palette (applied via .jk-shim on the section
   #jkRoot). Hand-converted pages use inline Mill styles + .card/.btn and don't
   carry these color utilities, so the shim is a no-op there. Lets large form
   pages adopt the warm look without a full markup rewrite.
   ============================================================================ */
.jk-shim h1, .jk-shim h2, .jk-shim h3 { font-family: var(--font-display); }
/* surfaces / text ramp */
.jk-shim .bg-white { background-color: var(--surface) !important; }
.jk-shim .bg-gray-50 { background-color: var(--surface-2) !important; }
.jk-shim .bg-gray-100 { background-color: var(--paper-2) !important; }
.jk-shim .bg-gray-200 { background-color: var(--border) !important; }
.jk-shim .text-gray-900, .jk-shim .text-gray-800 { color: var(--ink) !important; }
.jk-shim .text-gray-700, .jk-shim .text-gray-600 { color: var(--ink-2) !important; }
.jk-shim .text-gray-500 { color: var(--ink-3) !important; }
.jk-shim .text-gray-400, .jk-shim .text-gray-300 { color: var(--ink-4) !important; }
.jk-shim .border-gray-100, .jk-shim .border-gray-200 { border-color: var(--border) !important; }
.jk-shim .border-gray-300 { border-color: var(--border-2) !important; }
.jk-shim .divide-gray-200 > :not([hidden]) ~ :not([hidden]) { border-color: var(--border) !important; }
.jk-shim .hover\:bg-gray-50:hover { background-color: var(--surface-2) !important; }
.jk-shim .hover\:bg-gray-100:hover { background-color: var(--paper-2) !important; }
.jk-shim .hover\:bg-gray-200:hover { background-color: var(--border) !important; }
/* brand blue → terracotta */
.jk-shim .bg-blue-600 { background-color: var(--clay) !important; }
.jk-shim .bg-blue-700, .jk-shim .hover\:bg-blue-700:hover { background-color: var(--clay-hover) !important; }
.jk-shim .bg-blue-50 { background-color: var(--clay-50) !important; }
.jk-shim .hover\:bg-blue-50:hover { background-color: var(--clay-50) !important; }
.jk-shim .bg-blue-100 { background-color: var(--clay-100) !important; }
.jk-shim .text-blue-600, .jk-shim .hover\:text-blue-600:hover { color: var(--clay) !important; }
.jk-shim .text-blue-700, .jk-shim .text-blue-800, .jk-shim .hover\:text-blue-700:hover, .jk-shim .hover\:text-blue-800:hover { color: var(--clay-700) !important; }
.jk-shim .border-blue-200 { border-color: var(--clay-100) !important; }
.jk-shim .border-blue-500, .jk-shim .border-blue-600 { border-color: var(--clay) !important; }
/* semantics */
.jk-shim .text-green-600, .jk-shim .text-green-700, .jk-shim .text-emerald-600 { color: var(--pine) !important; }
.jk-shim .bg-green-600, .jk-shim .hover\:bg-green-700:hover { background-color: var(--pine) !important; }
.jk-shim .bg-green-50 { background-color: var(--pine-50) !important; }
.jk-shim .bg-green-100 { background-color: var(--pine-50) !important; }
.jk-shim .text-green-800 { color: var(--pine-700) !important; }
.jk-shim .border-green-200 { border-color: var(--pine-100) !important; }
.jk-shim .text-red-600, .jk-shim .text-red-700, .jk-shim .hover\:text-red-700:hover { color: var(--brick) !important; }
.jk-shim .bg-red-600, .jk-shim .hover\:bg-red-700:hover { background-color: var(--brick) !important; }
.jk-shim .bg-red-50, .jk-shim .hover\:bg-red-50:hover { background-color: var(--brick-50) !important; }
.jk-shim .bg-red-100 { background-color: var(--brick-50) !important; }
.jk-shim .text-red-800 { color: var(--brick-700) !important; }
.jk-shim .border-red-200, .jk-shim .border-red-300 { border-color: var(--brick-100) !important; }
.jk-shim .text-amber-600, .jk-shim .text-amber-700, .jk-shim .text-amber-800, .jk-shim .text-amber-900, .jk-shim .text-yellow-600, .jk-shim .text-yellow-800 { color: var(--ochre-700) !important; }
.jk-shim .bg-amber-50, .jk-shim .bg-yellow-50 { background-color: var(--ochre-50) !important; }
.jk-shim .bg-amber-100, .jk-shim .bg-yellow-100 { background-color: var(--ochre-50) !important; }
.jk-shim .border-amber-200, .jk-shim .border-yellow-200 { border-color: var(--ochre-100) !important; }
.jk-shim .bg-purple-100, .jk-shim .bg-indigo-100 { background-color: var(--plum-50) !important; }
.jk-shim .text-purple-800, .jk-shim .text-indigo-800, .jk-shim .text-purple-600, .jk-shim .text-indigo-600 { color: var(--plum-700) !important; }
.jk-shim .bg-purple-50, .jk-shim .bg-indigo-50 { background-color: var(--plum-50) !important; }
.jk-shim .bg-teal-50 { background-color: var(--pine-50) !important; }
.jk-shim .text-teal-600 { color: var(--pine) !important; }
.jk-shim .bg-orange-50 { background-color: var(--clay-50) !important; }
.jk-shim .text-orange-600, .jk-shim .text-orange-700 { color: var(--clay-700) !important; }
.jk-shim .border-orange-300 { border-color: var(--clay-100) !important; }
/* native inputs → Mill fields */
.jk-shim input[type=text], .jk-shim input[type=email], .jk-shim input[type=password], .jk-shim input[type=number], .jk-shim input[type=date], .jk-shim input[type=month], .jk-shim input[type=search], .jk-shim input[type=tel], .jk-shim input[type=url], .jk-shim select, .jk-shim textarea {
  background-color: var(--surface) !important; border-color: var(--border-2) !important; color: var(--ink) !important;
}
.jk-shim input:focus, .jk-shim select:focus, .jk-shim textarea:focus { border-color: var(--clay) !important; box-shadow: 0 0 0 3px var(--clay-50) !important; outline: none !important; }
.jk-shim input[type=checkbox], .jk-shim input[type=radio] { accent-color: var(--clay); }
/* focus rings that used blue */
.jk-shim .focus\:ring-blue-500:focus, .jk-shim .focus\:ring-blue-600:focus { --tw-ring-color: var(--clay) !important; }
.jk-shim .focus\:border-blue-500:focus { border-color: var(--clay) !important; }

/* ---- Sale form: out-of-stock line ----
   Toggled by checkInventoryAvailability() on the sale create/edit pages: the
   Boxes quantity input of an out-of-stock line goes bold red alongside the red
   product field, so the quantity that will turn into a Sale ReOrder is
   unmissable. Declared after the .jk-shim input overrides on purpose so the
   red border wins over the shim's neutral field styling. */
input.oos-boxes, .jk-shim input.oos-boxes {
  border-color: var(--brick) !important;
  border-width: 2px !important;
  color: var(--brick-700) !important;
  font-weight: 700;
}
input.oos-boxes:focus, .jk-shim input.oos-boxes:focus {
  border-color: var(--brick) !important;
  box-shadow: 0 0 0 3px var(--brick-50) !important;
}
/* Companion to .oos-boxes: the legacy inline red background on the product
   search field is defeated by the .jk-shim "input background !important"
   override above, so the same JS toggles this class to keep the product field
   visibly red as well (the inline style remains the JS-readable state). */
input.oos-product, .jk-shim input.oos-product {
  background-color: rgb(222, 171, 175) !important;
}

/* ============================================================================
   Permission tooltips (group create/edit form)
   ============================================================================
   Positioned fixed by static/js/jk-permission-tips.js. Fixed is mandatory:
   the permission card and every app section carry overflow:hidden, which
   would clip a CSS-only ::after tooltip inside its own box. */
.jk-tip {
  position: fixed;
  z-index: 1700;                  /* top of the overlay ladder above.
                                     Was 50 ("above the sticky save bar at
                                     z-10"), which put every tooltip BEHIND
                                     any .jk-modal-layer modal — this node is
                                     appended to <body>, so it competes at the
                                     root, not inside the modal it explains. */
  max-width: 320px;
  padding: 8px 11px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  box-shadow: 0 6px 20px -6px rgba(60, 48, 30, .28);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-2);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity .12s ease;
}
.jk-tip.is-open { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: reduce) { .jk-tip { transition: none; } }

/* Sale-list variant: compact product names and quantities share the existing
   fixed, non-interactive tooltip surface. */
.jk-tip--table {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 12px;
  row-gap: 2px;
  max-width: 340px;
  max-height: 360px;
  overflow: hidden;
}
/* Sale-form history: date and boxes above the sale number and box sizes. */
.already-taken-info {
  background: none;
  border: 0;
  font: inherit;
  cursor: help;
  padding: 0;
}
.jk-tip--taken {
  min-width: 220px;
  max-width: min(340px, calc(100vw - 16px));
  /* ponytail: non-interactive surface, so a very short viewport clips the
     oldest rows instead of scrolling; 10 sales fit any desktop height. */
  max-height: calc(100vh - 16px);
  overflow: hidden;
}
.jk-tip--taken .jk-taken-head {
  margin-bottom: 4px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-label);
}
.jk-tip--taken .jk-taken-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 12px;
  row-gap: 1px;
}
.jk-tip--taken .jk-taken-row + .jk-taken-row { margin-top: 6px; }
.jk-tip--taken .jk-taken-date { color: var(--ink); font-weight: 500; }
.jk-tip--taken .jk-taken-boxes {
  color: var(--ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.jk-tip--taken .jk-taken-detail {
  grid-column: 1 / -1;
  color: var(--ink-3);
  font-size: 11.5px;
  overflow-wrap: anywhere;
}
.jk-tip--taken .jk-taken-draft {
  margin-left: 4px;
  font-size: 10.5px;
  color: var(--ink-3);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  padding: 0 4px;
}
.jk-tip--taken .jk-taken-foot {
  margin-top: 6px;
  border-top: 1px solid var(--border);
  padding-top: 6px;
  color: var(--ink-3);
}
/* Sale-detail product image preview (same jk-row-tip.js surface). Fixed size so
   the tip is measured at its final box on open, before the image arrives; the
   alt text is the fallback a missing image shows in the slot. Scoped under
   .jk-tip--table: jk-image-tip.js (sale forms) reuses .jk-tip-img with its own
   .jk-tip--img sizing, and a bare rule would leak into it. */
.jk-tip--table .jk-tip-img {
  grid-column: 1 / -1;
  display: block;
  width: 200px;
  height: 200px;
  object-fit: contain;
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-size: 12px;
  color: var(--ink-3);
}
/* --ink-4 is "faint meta" (4.89:1 on --surface). These three are not meta: the
   head states the true product count, the foot is the only disclosure that rows
   were truncated, and the empty state is the whole content an OLD- sale has, so
   they step up a tier: --ink-3 (5.76:1) like the muted labels
   elsewhere on this page, and the empty state carries body ink outright. */
.jk-tip-head,
.jk-tip-foot,
.jk-tip-empty { grid-column: 1 / -1; color: var(--ink-3); }
.jk-tip-head {
  margin-bottom: 3px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 11.5px;
}
.jk-tip-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);
}
.jk-tip-qty {
  text-align: right;
  white-space: nowrap;
  color: var(--ink-3);
}
.jk-tip-foot {
  margin-top: 3px;
  padding-top: 4px;
  border-top: 1px solid var(--border);
}
.jk-tip-empty { color: var(--ink-2); font-style: italic; }

/* Sale-form photo preview (static/js/jk-image-tip.js): a 240px thumbnail plus
   a one-line product caption, on the same fixed non-interactive surface.
   The image box is sized here rather than left to the file, so the panel is
   already its final height when the module measures it for edge clamping --
   an unsized <img> measures 0 until the bytes land, and the card would be
   positioned for a box it is about to outgrow.
   No max-width of its own: 240px + the base's padding and border is 264px,
   inside `.jk-tip`'s 320px, so the panel shrink-to-fits there already. */
.jk-tip--img .jk-tip-img {
  display: block;
  width: 240px;
  height: 180px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.jk-tip--img .jk-tip-cap {
  margin-top: 6px;
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);
}

/* ---- Quick Pages Manage: drag-reorder rows + click-to-edit rename ---------
   The row is the same <form> the page always used; at rest it shows a grip, the
   pin's name, and Remove. The label input and its path only appear while you are
   actually renaming, which is what let the URL line come off every row without
   making two same-named pins (a list and a filtered view of it share a default
   label) impossible to tell apart. */
.jk-qp-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: nowrap;
  padding: 10px 12px 10px 6px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.jk-qp-group .jk-qp-row:last-child { border-bottom: none; }
.jk-qp-main { flex: 1; min-width: 0; }

/* The grip. touch-action:none is load-bearing, not polish: without it the browser
   claims a vertical drag for page scrolling and the whole feature is dead on a
   phone, even though every pointer event still fires. This page is reachable at
   768px, which is a touch width. */
.jk-qp-grip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 34px; flex-shrink: 0;
  padding: 0; border: none; background: transparent;
  color: var(--ink-4); cursor: grab; border-radius: var(--r-sm);
  touch-action: none;
  transition: color .12s ease, background .12s ease;
}
.jk-qp-grip:hover { color: var(--clay); background: var(--clay-50); }
.jk-qp-grip:active { cursor: grabbing; }
.jk-qp-grip svg { fill: currentColor; }
/* Focus ring: jk-mill scopes the global ring to .focusable/input/button, and this
   IS a button, but the grip is the one control here where position is the only
   cue — make it unmistakable. */
.jk-qp-grip:focus-visible { outline: 2px solid var(--clay); outline-offset: 1px; }

/* The name at rest: reads as text, behaves as a button. */
.jk-qp-name {
  display: block; width: 100%; text-align: left;
  padding: 4px 6px; margin: -4px -6px;
  border: none; background: transparent;
  font-size: 14px; font-weight: 500; color: var(--ink);
  cursor: text; border-radius: var(--r-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.jk-qp-name:hover { background: var(--paper-2); }

.jk-qp-path { margin-top: 4px; font-size: 12px; color: var(--ink-4); }
.jk-qp-hint { margin-top: 2px; font-size: 11.5px; color: var(--ink-4); }

/* Dragging. The row is lifted with a transform only — it is never reparented
   mid-drag, so the pointer capture stays attached to a node that never left the
   tree; the single DOM move happens once, on drop. */
.jk-qp-row.is-dragging {
  position: relative; z-index: 5;
  box-shadow: var(--sh-lg);
  border-radius: var(--r-sm);
  opacity: .95;
}
.jk-qp-group.jk-qp-dragging { user-select: none; }
.jk-qp-group.jk-qp-dragging .jk-qp-row:not(.is-dragging) {
  transition: transform var(--dur-fast, .12s) ease;
}

/* Active-filter state layer.
   `!important` is load-bearing and NOT just for the inline strays: `.jk-fields
   input[type=text]` (:877) is (0,2,1) — an attribute selector scores in the class
   column — so it outranks a doubled `.is-filtered.is-filtered` (0,2,0) and sets the
   `background` SHORTHAND. Inline `style="background:var(--surface)"` on the
   multiselect buttons (_components/factory_filter.html:19, product_list.html:77)
   outranks every author rule anyway. One `!important` settles both, so the selector
   stays single — a doubled class would be cargo.
   `background-color`, never the `background` shorthand — the shorthand would wipe
   `.select`'s chevron background-image (:334, :341).
   The two declarations are SPLIT: `:not(:focus)` on the border only. On the
   background too it would drop the tint while the control is focused, so tabbing
   through filters would make each one stop looking filtered in turn.
   No .trow label is stamped today — every one sits inside a <details> or a
   [data-multiselect], and factory_export is opted out. If wrapperFor() is ever
   loosened, .trow:hover loses to this !important and has to be handled here. */
.is-filtered { background-color: var(--clay-100) !important; }   /* NOT clay-50 — see Premise 3 */
.is-filtered:not(:focus) { border-color: var(--clay) !important; }

/* On a <label> the ring is an OUTLINE. A label computes `border-style: none`, so a
   border-color paints zero pixels — and a border would need padding to sit on plus a
   negative margin to cancel it, which then (a) overlaps its neighbour in
   sale_list.html:36's 14px `gap-3.5` and (b) has to out-!important the inline
   `margin-bottom:9px` on qr_photos.html:121, destroying an alignment offset in an
   items-end row. An outline is outside the box model: zero layout space, nothing to
   cancel, nothing to measure. Legacy Chromium draws it square (outline ignores
   border-radius there); visible, not wrong. */
label.is-filtered {
  outline: 1px solid var(--clay);
  outline-offset: 2px;
  border-radius: var(--r-sm);          /* jk-mill.css:95 — 6px */
}

/* forced-colors strips backgrounds and colours; keep a width channel. `!important`
   or the inline `border:1px solid` on the multiselect buttons wins. Outlines stay
   visible in forced-colors, so the label shape needs nothing extra here. */
@media (forced-colors: active) {
  .is-filtered { border-width: 2px !important; }
}

@media (max-width: 640px) {
  #filterForm .jk-sort-field { width: 100%; flex-direction: column; align-items: stretch; }
  #filterForm .jk-sort-field select { width: 100%; min-height: 44px; }
}

/* Re-Orders selection: a textless hit area keeps the product as the mobile card title. */
#reorderPickRoot .rm-pick-hit { display: grid; place-items: center; min-width: 44px; min-height: 44px; cursor: pointer; }
#reorderPickRoot .rm-pick-hit input, #reorderPickRoot [data-pick-all-mirror] { width: 18px; height: 18px; accent-color: var(--clay); }
#reorderPickRoot tr.is-picked > td { background: var(--clay-50); }
#jkRoot.has-pickbar { padding-bottom: 88px; }
#reorderPickBar[hidden] { display: none; }
#reorderPickModal:not(.hidden) { display: flex; }
#reorderPickModal #reorderMatchForm { max-width: 560px; max-height: calc(100dvh - 32px); display: flex; flex-direction: column; }
#reorderPickModal .rm-pick-body { padding: 18px 20px; overflow: auto; flex: 1; }
#reorderPickModal .rm-picklist { max-height: min(40vh, 320px); overflow: auto; list-style: none; margin: 0 0 16px; padding: 0; }
#reorderPickModal .rm-picklist li { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--border); font-size: 14px; color: var(--ink); overflow-wrap: anywhere; }
#reorderPickModal .jk-iconbtn { min-width: 44px; min-height: 44px; flex-shrink: 0; }
#reorderPickModal .rm-pick-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }

/* Client-match view switch: .jk-cm-views wraps .jk-seg radios #cmViewClient /
   #cmViewProduct and both .jk-cm-view--client / .jk-cm-view--product sections.
   Client is the default, including when neither radio is checked. */
.jk-cm-views:not(:has(#cmViewProduct:checked)) .jk-cm-view--product { display: none; }
.jk-cm-views:has(#cmViewProduct:checked) .jk-cm-view--client { display: none; }

/* Re-Orders album (batch results) */
.sp-album-export { flex-basis:100%; border-top:1px solid var(--border); padding-top:16px; margin-top:8px; }
.sp-pdf-options { border:0; padding:0; margin:0; }
.sp-pdf-options legend { font-weight:600; margin-bottom:8px; }
.sp-pdf-options label { display:inline-flex; align-items:center; gap:6px; margin-right:16px; }
.sp-album-downloads { display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin-top:16px; }
.sp-album-downloads > span { font-size:12px; color:var(--ink-3); }
.sp-album-exclusions { margin-bottom:16px; }
.sp-album-exclusions p { margin-top:8px; }
@media (max-width:640px) {
  .sp-album-export .sp-pdf-options label { display:flex; min-height:44px; margin-right:0; }
  .sp-album-downloads { flex-direction:column; align-items:stretch; }
  .sp-album-downloads .btn { width:100%; min-height:44px; }
}

/* Previously ordered pill */
.sp-ordered {
  display:inline-flex; flex-wrap:wrap; gap:2px 4px; margin-top:4px;
  white-space:normal; font-size:11px; line-height:1.35;
  background:var(--slate-50); color:var(--slate-700); border:1px solid var(--slate-100);
  padding:2px 8px; border-radius:var(--r-pill);
}

/* ============================ Customer chat widget (shop/_chat_widget.html) ============================
   Panel geometry lives here, not inline, so the phone rule can override it, and
   not in the widget's <style> because that ships in every customer page's HTML. */
/* --chat-lift: how far a page raises the launcher above its 24px (shop.css, cart bar), so the card shrinks instead of running off the top */
#chat-panel { bottom: 72px; right: 0; width: 360px; max-width: calc(100vw - 3rem); height: 460px; max-height: calc(100vh - 120px - var(--chat-lift, 0px)); transform-origin: bottom right; }
/* phone: a bottom sheet across the screen instead of a 360px card squeezed into
   the corner; it covers the launcher, so the header's close is the way out.
   dvh tracks browser chrome (vh is the fallback), NOT the on-screen keyboard:
   ChatClient.fitToVisualViewport (chat_client.js) pins the sheet while one is up.
   Any viewport under 500px tall takes the sheet too (landscape phones, short
   desktop windows): the corner card is ~270px there, less once the cart lifts
   the launcher, and its message list collapses. */
@media (max-width: 640px), (max-height: 500px) {
  #chat-panel { position: fixed; left: 8px; right: 8px; bottom: 8px; width: auto; max-width: none; height: calc(100vh - 88px); height: calc(100dvh - 88px); max-height: 640px; transform-origin: bottom center; }
}
/* under 16px iOS Safari zooms the page on focus; phones only, same query as the shop fields.
   #chat-input/#chat-send also exist on the staff thread view, which gets these too. */
@media (max-width: 640px), (max-height: 500px) and (pointer: coarse) { #chat-input { font-size: 16px; } }
/* Staff thread composer: the Enter/Shift+Enter hint only means something with a hardware keyboard. */
@media not all and (pointer: fine) { .jk-chat-hint { display: none; } }
@media (pointer: coarse) {
  #chat-close, #chat-send { min-width: 44px; min-height: 44px; }   /* min-* beats the inline 32/40px size without !important */
}

/* Shared table row checkboxes: sales list bulk download and the stock page
   consolidation selection. The card-lead rule applies to any stacked table. */
.jk-sale-check { accent-color:var(--clay); width:16px; height:16px; }
.jk-sale-check.is-sm { width:14px; height:14px; }
/* Touch: a 22px box inside a 44px tap area; the negative margin keeps the box where it was. */
@media (pointer: coarse) {
  .jk-sale-check, .jk-sale-check.is-sm { width:22px; height:22px; }
  .jk-sale-check-hit { display:inline-grid; place-items:center; min-width:44px; min-height:44px; margin:-11px; cursor:pointer; }
  /* In a phone card the box sits in a 36px left gutter: stop the tap area at the gutter so it never covers the Sale # link. */
  td.jk-card-lead .jk-sale-check-hit { min-width:36px; margin:-11px -3px -11px -11px; padding-left:11px; justify-items:start; }
}
