/* ============================================================
   TALLYRACK — desktop inventory catalogue
   Palette : paper grey canvas, ledger-green accent, ochre signal
   Type    : Archivo (expanded, display) / Public Sans (UI)
             / IBM Plex Mono (all data: price, SKU, counts)
   ============================================================ */

:root{
  --canvas:#F1F2F0;
  --surface:#FFFFFF;
  --surface-2:#FAFAF8;
  --ink:#14171A;
  --ink-2:#474D52;
  --muted:#7C848B;
  --line:#E0E2DF;
  --line-2:#C9CCC8;
  --accent:#0E4F42;
  --accent-ink:#0A3A31;
  --accent-soft:#E2EDE9;
  --signal:#8A5A08;
  --signal-soft:#F6EEDD;
  --danger:#8C2F22;

  --r-sm:3px;
  --r-md:5px;
  --r-lg:8px;

  --bar-h:46px;
  --status-h:30px;
  --rail-w:274px;

  --shadow-1:0 1px 2px rgba(20,23,26,.06);
  --shadow-2:0 6px 20px -6px rgba(20,23,26,.18), 0 1px 2px rgba(20,23,26,.06);

  --font-display:"Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-ui:"Public Sans", "Helvetica Neue", Arial, sans-serif;
  --font-mono:"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
}

*,*::before,*::after{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;
  background:var(--canvas);
  color:var(--ink);
  font-family:var(--font-ui);
  font-size:13px;
  line-height:1.45;
  -webkit-font-smoothing:antialiased;
  overflow:hidden;
}
button,input,select{font:inherit;color:inherit}
button{background:none;border:0;padding:0;cursor:pointer}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}

/* --- shared atoms ------------------------------------------------ */
.eyebrow{
  font-family:var(--font-mono);
  font-size:10px;
  font-weight:600;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--muted);
}
.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.display{
  font-family:var(--font-display);
  font-variation-settings:"wdth" 112;
  font-weight:700;
  letter-spacing:-.01em;
}

.btn{
  display:inline-flex;align-items:center;gap:6px;
  text-decoration:none;
  height:30px;padding:0 11px;
  border:1px solid var(--line-2);border-radius:var(--r-md);
  background:var(--surface);
  font-size:12.5px;font-weight:500;color:var(--ink-2);
  white-space:nowrap;transition:background .12s,border-color .12s,color .12s;
}
.btn:hover{background:var(--surface-2);border-color:#B4B8B3;color:var(--ink)}
.btn[aria-pressed="true"]{background:var(--accent-soft);border-color:#A8C6BD;color:var(--accent-ink)}
.btn--primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn--primary:hover{background:var(--accent-ink);border-color:var(--accent-ink);color:#fff}
.btn--ghost{border-color:transparent;background:transparent}
.btn--ghost:hover{background:rgba(20,23,26,.05);border-color:transparent}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn--lg{height:34px;padding:0 15px;font-size:13px}
/* Was an inline style, which no media query could override. */
.btn--tiny{height:22px;padding:0 7px;font-size:11.5px}
.icon{width:14px;height:14px;flex:none;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}

/* ============================================================
   APP SHELL
   ============================================================ */
.app{height:100vh;display:flex;flex-direction:column}

.appbar{
  height:var(--bar-h);flex:none;
  display:flex;align-items:center;gap:18px;
  padding:0 14px;
  background:var(--surface);
  border-bottom:1px solid var(--line);
}
.brand{display:flex;align-items:center;gap:8px}
.brand__mark{
  width:20px;height:20px;border-radius:var(--r-sm);
  background:var(--accent);
  display:grid;place-items:center;
}
.brand__mark span{
  display:block;width:9px;height:9px;
  border:1.6px solid #fff;border-radius:1px;
  border-bottom-width:4px;
}
.brand__name{
  font-family:var(--font-display);
  font-variation-settings:"wdth" 118;
  font-weight:700;font-size:14px;letter-spacing:.06em;text-transform:uppercase;
}
.brand__sub{
  font-family:var(--font-mono);font-size:10px;color:var(--muted);
  letter-spacing:.06em;padding-left:8px;margin-left:2px;border-left:1px solid var(--line);
}

.nav{display:flex;gap:2px;margin-left:6px}
/* Anchors now, not buttons — each tab is a real page — so the default
   link styling has to be turned off explicitly. */
.nav__item{
  position:relative;height:var(--bar-h);padding:0 13px;
  display:flex;align-items:center;gap:7px;
  text-decoration:none;
  font-size:13px;font-weight:500;color:var(--muted);
}
.nav__item:hover{color:var(--ink)}
.nav__item[aria-current="page"]{color:var(--ink);font-weight:600}
.nav__item[aria-current="page"]::after{
  content:"";position:absolute;left:9px;right:9px;bottom:-1px;height:2px;background:var(--accent);
}
.nav__step{
  font-family:var(--font-mono);font-size:10px;color:var(--line-2);
}
.nav__item[aria-current="page"] .nav__step{color:var(--accent)}

.appbar__right{margin-left:auto;display:flex;align-items:center;gap:8px}
.sync{
  display:flex;align-items:center;gap:9px;
  height:30px;padding:0 5px 0 10px;
  border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--surface-2);
}
.sync__dot{width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none}
.sync.is-stale .sync__dot{background:var(--signal)}
.sync__text{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);white-space:nowrap}
.sync__text b{font-weight:600;color:var(--ink)}
.sync.is-stale .sync__text{color:var(--signal)}
.sync__btn{
  display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 8px;
  border-radius:var(--r-sm);background:var(--surface);border:1px solid var(--line-2);
  font-size:11.5px;font-weight:500;color:var(--ink-2);
}
.sync__btn:hover{background:var(--accent-soft);border-color:#A8C6BD;color:var(--accent-ink)}
.is-syncing .sync__btn .icon{animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* view host */
.views{flex:1;min-height:0;position:relative}
.view{position:absolute;inset:0;display:none;min-height:0}
.view.is-active{display:flex}

/* status bar */
.statusbar{
  height:var(--status-h);flex:none;
  display:flex;align-items:center;gap:14px;
  padding:0 12px;
  background:var(--surface);border-top:1px solid var(--line);
  font-family:var(--font-mono);font-size:11px;color:var(--muted);
}
.statusbar b{color:var(--ink-2);font-weight:600}
.statusbar__sep{width:1px;height:13px;background:var(--line)}
.statusbar__demo{margin-left:auto;display:flex;align-items:center;gap:6px}
.demo-btn{
  height:20px;padding:0 8px;border:1px solid var(--line);border-radius:var(--r-sm);
  font-family:var(--font-mono);font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);
}
.demo-btn:hover{color:var(--ink);border-color:var(--line-2)}
.demo-btn[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:#fff}

/* ============================================================
   1 · UPLOAD
   ============================================================ */
#view-upload{overflow-y:auto;display:none}
#view-upload.is-active{display:block}
.upload{max-width:1080px;margin:0 auto;padding:30px 28px 56px}
.page-head{margin-bottom:22px}
.page-head h1{
  font-family:var(--font-display);font-variation-settings:"wdth" 116;
  font-size:25px;font-weight:700;letter-spacing:-.015em;margin:6px 0 5px;
}
.page-head p{margin:0;color:var(--ink-2);max-width:62ch}

.drops{display:grid;grid-template-columns:1.55fr 1fr;gap:16px;align-items:stretch}

.drop{
  position:relative;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:20px;
  display:flex;flex-direction:column;
  transition:border-color .15s,background .15s;
}
.drop__zone{
  flex:1;min-height:184px;
  border:1.5px dashed var(--line-2);border-radius:var(--r-md);
  background:var(--surface-2);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:9px;text-align:center;padding:20px;
  transition:border-color .15s,background .15s;
}
.drop.is-over .drop__zone{border-color:var(--accent);background:var(--accent-soft)}
.drop.is-over{border-color:var(--accent)}
.drop__title{display:flex;align-items:baseline;gap:9px;margin-bottom:13px}
.drop__title h2{font-size:14px;font-weight:600;margin:0;letter-spacing:-.005em}
.drop__icon{width:26px;height:26px;stroke:var(--muted);fill:none;stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round}
.drop.is-over .drop__icon{stroke:var(--accent)}
.drop__cue{font-size:13.5px;font-weight:500}
.drop__hint{font-size:12px;color:var(--muted);max-width:34ch}

/* filename convention legend */
.convention{margin-top:16px;padding-top:14px;border-top:1px dashed var(--line)}
/* Three filenames side by side. SKUs are dash-separated words now
   (1E001-DGJAPI-GMC-2.jpg), which is long enough that the row wraps to
   two lines on a narrow panel rather than squeezing — and a filename
   must never break mid-name, because the break landed between `GMC-`
   and `1.jpg`, hiding the one character the panel exists to explain. */
.convention__row{
  display:flex;align-items:stretch;flex-wrap:wrap;gap:0;margin-top:9px;
  border:1px solid var(--line);border-radius:var(--r-md);overflow:hidden;
}
.token{flex:1 1 auto;min-width:0;padding:7px 10px;background:var(--surface-2);border-right:1px solid var(--line)}
.token:last-child{border-right:0}
.token__k{display:block;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:2px}
.token__v{
  display:block;font-family:var(--font-mono);font-size:11.5px;font-weight:500;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.token--sep{flex:none;width:34px;display:grid;place-items:center;background:var(--surface);color:var(--line-2);font-family:var(--font-mono);font-size:13px}

/* queue */
.queue{margin-top:16px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden}
.queue__head{
  display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);
}
.queue__head h3{margin:0;font-size:13px;font-weight:600}
.queue__list{max-height:230px;overflow-y:auto}
.qrow{display:grid;grid-template-columns:24px 1fr 96px 104px 74px;gap:12px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--line)}
.qrow:last-child{border-bottom:0}
.qrow__thumb{width:24px;height:24px;border-radius:var(--r-sm);border:1px solid var(--line)}
.qrow__name{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.qrow__meta{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-2);text-align:right}
.tag{
  display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 7px;border-radius:100px;
  font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;
}
.tag--ok{background:var(--accent-soft);color:var(--accent-ink)}
.tag--warn{background:var(--signal-soft);color:var(--signal)}
.tag--idle{background:#EFF0EE;color:var(--muted)}

.notice{
  display:flex;gap:10px;align-items:flex-start;
  margin-top:16px;padding:12px 14px;border-radius:var(--r-md);
  background:var(--signal-soft);border:1px solid #E6D5B0;color:#5E3D05;
}
.notice__t{font-weight:600;font-size:12.5px;margin:0 0 2px}
.notice p{margin:0;font-size:12.5px;color:#6B4907}
.notice .icon{stroke:var(--signal);margin-top:1px}

/* sheet columns detected */
.cols{display:flex;flex-wrap:wrap;gap:5px;margin-top:12px}
.col-pill{
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
  padding:3px 7px;border-radius:var(--r-sm);background:var(--accent-soft);color:var(--accent-ink);
}
.col-pill--key{background:var(--ink);color:#fff}

.sync-panel{
  margin-top:16px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:16px;display:flex;align-items:center;gap:18px;flex-wrap:wrap;
}
.sync-panel__meta{display:flex;flex-direction:column;gap:3px}
.sync-panel__meta strong{font-size:13px}
.sync-panel__meta span{font-family:var(--font-mono);font-size:11.5px;color:var(--muted)}
.sync-panel__actions{margin-left:auto;display:flex;gap:8px}

/* ============================================================
   2 · CATALOGUE
   ============================================================ */
#view-catalogue{display:none}
#view-catalogue.is-active{display:flex}

/* ---- left filter rail ---- */
.rail{
  width:var(--rail-w);flex:none;
  background:var(--surface);border-right:1px solid var(--line);
  display:flex;flex-direction:column;min-height:0;
}
.rail__head{
  padding:11px 13px;border-bottom:1px solid var(--line);
  display:flex;align-items:center;gap:8px;
}
.rail__head .eyebrow{flex:1}
.rail__source{
  padding:8px 13px;border-bottom:1px solid var(--line);background:var(--surface-2);
  font-family:var(--font-mono);font-size:10.5px;color:var(--muted);line-height:1.5;
}
.rail__source b{color:var(--ink-2);font-weight:500}
.rail__body{flex:1;overflow-y:auto;min-height:0}

.group{border-bottom:1px solid var(--line)}
.group__head{
  width:100%;display:flex;align-items:center;gap:8px;
  padding:9px 13px;text-align:left;
}
.group__head:hover{background:var(--surface-2)}
.group__chev{width:9px;height:9px;stroke:var(--muted);fill:none;stroke-width:2;transition:transform .15s}
.group.is-open .group__chev{transform:rotate(90deg)}
.group__name{flex:1;font-family:var(--font-mono);font-size:10.5px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-2)}
.group__badge{
  font-family:var(--font-mono);font-size:10px;font-weight:600;
  min-width:16px;height:16px;padding:0 4px;border-radius:100px;
  display:grid;place-items:center;background:var(--accent);color:#fff;
}
.group__list{display:none;padding:0 6px 8px}
.group.is-open .group__list{display:block}

.opt{
  display:grid;grid-template-columns:14px 1fr auto;gap:8px;align-items:center;
  padding:5px 7px;border-radius:var(--r-sm);cursor:pointer;
}
.opt:hover{background:var(--surface-2)}
.opt__box{
  width:14px;height:14px;border:1px solid var(--line-2);border-radius:3px;background:var(--surface);
  display:grid;place-items:center;
}
.opt__box svg{width:9px;height:9px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;opacity:0}
.opt.is-on .opt__box{background:var(--accent);border-color:var(--accent)}
.opt.is-on .opt__box svg{opacity:1}
.opt__label{font-size:12.5px;color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.opt.is-on .opt__label{color:var(--ink);font-weight:500}
.opt__n{font-family:var(--font-mono);font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}
/* signature: inventory distribution bar under each value */
.opt__bar{grid-column:2 / -1;height:2px;background:#EDEEEC;border-radius:2px;overflow:hidden;margin-top:-1px}
.opt__bar i{display:block;height:100%;background:var(--line-2);border-radius:2px;transition:width .25s ease}
.opt.is-on .opt__bar i{background:var(--accent)}
.opt.is-empty{opacity:.42;cursor:not-allowed}
.opt__swatch{width:10px;height:10px;border-radius:2px;border:1px solid rgba(20,23,26,.18);flex:none}
.opt__label-wrap{display:flex;align-items:center;gap:7px;min-width:0}

.rail__foot{padding:9px 13px;border-top:1px solid var(--line);background:var(--surface-2)}

/* ---- main column ---- */
.main{flex:1;min-width:0;display:flex;flex-direction:column}

.toolbar{
  flex:none;display:flex;align-items:center;gap:8px;
  padding:9px 14px;background:var(--surface);border-bottom:1px solid var(--line);
}
.search{position:relative;flex:0 1 288px;width:288px;min-width:150px}
.search .icon{position:absolute;left:9px;top:50%;transform:translateY(-50%);stroke:var(--muted);pointer-events:none}
.search input{
  width:100%;height:30px;padding:0 28px 0 30px;
  border:1px solid var(--line-2);border-radius:var(--r-md);background:var(--surface-2);
  font-size:12.5px;
}
.search input:focus{background:var(--surface);border-color:var(--accent);outline:none;box-shadow:0 0 0 3px rgba(14,79,66,.1)}
.search input::placeholder{color:var(--muted)}
.search__clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:18px;height:18px;display:none;place-items:center;border-radius:3px;color:var(--muted)}
.search__clear:hover{background:rgba(20,23,26,.06);color:var(--ink)}
.search.has-value .search__clear{display:grid}

.select-wrap{position:relative;display:flex;align-items:center}
.select-wrap select{
  appearance:none;height:30px;padding:0 26px 0 10px;
  border:1px solid var(--line-2);border-radius:var(--r-md);background:var(--surface);
  font-size:12.5px;color:var(--ink-2);cursor:pointer;
}
.select-wrap .icon{position:absolute;right:8px;pointer-events:none;stroke:var(--muted);width:11px;height:11px}
.toolbar__spacer{flex:1}
.count{font-family:var(--font-mono);font-size:11.5px;color:var(--muted);white-space:nowrap}
.count b{color:var(--ink);font-weight:600}
.vsep{width:1px;height:20px;background:var(--line)}

/* ---- products per row ----------------------------------------
   The rail speaks the card's own language: a hairline with a punched
   detent at every stop, and a clip you slide between them — the same
   ticket construction as the card footer's notches.
   Deliberately NOT a filled progress bar: 6-up is not "more" than 3-up,
   it is a different way of looking, so nothing here implies a scale.
   --stops / --thumb are read by app.js to place the detents exactly. */
.density{
  --thumb:14px;
  display:flex;align-items:center;gap:9px;
  height:30px;padding:0 10px 0 9px;
  border:1px solid var(--line-2);border-radius:var(--r-md);background:var(--surface);
  transition:border-color .12s;
}
.density:hover{border-color:var(--muted)}
.density:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(14,79,66,.1)}
.density__glyph{color:var(--muted);flex:none}

.density__rail{position:relative;display:block;width:104px;height:26px;flex:1 1 auto;min-width:64px}
/* the rail itself */
.density__rail::before{
  content:"";position:absolute;left:0;right:0;top:50%;height:1px;
  background:var(--line-2);transform:translateY(-50%);
}
/* one punched detent per available column count */
.density__stop{
  position:absolute;top:50%;width:5px;height:5px;
  margin-top:-2.5px;transform:translateX(-50%);
  border:1px solid var(--line-2);border-radius:50%;background:var(--canvas);
}

/* The native input sits transparent on top: real keyboard support,
   real drag physics, none of the native chrome. */
.density input{
  -webkit-appearance:none;appearance:none;
  position:absolute;inset:0;width:100%;height:100%;
  margin:0;padding:0;background:none;cursor:ew-resize;
  z-index:1;                       /* the clip covers the detent it sits on */
}
.density input:focus{outline:none}
.density input::-webkit-slider-runnable-track{height:100%;background:none;border:0}
.density input::-moz-range-track{height:100%;background:none;border:0}
.density input::-webkit-slider-thumb{
  -webkit-appearance:none;
  box-sizing:border-box;width:var(--thumb);height:20px;
  border:1px solid var(--line-2);border-radius:2px;
  background:var(--surface) linear-gradient(var(--accent),var(--accent)) center/2px 9px no-repeat;
  box-shadow:var(--shadow-1);
  transition:border-color .12s,box-shadow .12s;
}
.density input::-moz-range-thumb{
  box-sizing:border-box;width:var(--thumb);height:20px;
  border:1px solid var(--line-2);border-radius:2px;
  background:var(--surface) linear-gradient(var(--accent),var(--accent)) center/2px 9px no-repeat;
  box-shadow:var(--shadow-1);
}
.density input:hover::-webkit-slider-thumb,
.density input:focus-visible::-webkit-slider-thumb{border-color:var(--accent);box-shadow:var(--shadow-2)}
.density input:hover::-moz-range-thumb,
.density input:focus-visible::-moz-range-thumb{border-color:var(--accent)}

.density__read{
  font-family:var(--font-mono);font-size:12.5px;font-weight:600;
  font-variant-numeric:tabular-nums;color:var(--ink);
  min-width:1ch;text-align:right;
}
.density__cap{font-size:11.5px;color:var(--muted);white-space:nowrap}

/* Between the rail drawer breakpoint and a roomy desktop the toolbar has
   no slack, so the control sheds its caption before anything truncates. */
@media (max-width:1180px){
  .search{width:224px}
  .density__cap{display:none}
  .density__rail{width:82px}
}

/* chips */
.chipbar{
  flex:none;display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  padding:8px 14px;background:var(--surface-2);border-bottom:1px solid var(--line);min-height:41px;
}
.chipbar.is-empty{padding:6px 14px;min-height:0}
.chipbar__hint{font-family:var(--font-mono);font-size:11px;color:var(--muted)}
.chip{
  display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 4px 0 8px;
  background:var(--surface);border:1px solid var(--line-2);border-radius:100px;
  font-size:12px;
}
.chip__k{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.chip__v{font-weight:500;color:var(--ink)}
.chip__x{width:16px;height:16px;display:grid;place-items:center;border-radius:50%;color:var(--muted)}
.chip__x:hover{background:rgba(20,23,26,.08);color:var(--ink)}
.chip--search{border-style:dashed}
.chipbar__clear{
  margin-left:2px;height:24px;padding:0 9px;border-radius:100px;
  font-size:12px;font-weight:500;color:var(--danger);
}
.chipbar__clear:hover{background:rgba(140,47,34,.08)}

/* selection action bar */
.selbar{
  display:none;flex:none;align-items:center;gap:10px;
  padding:8px 14px;background:var(--accent);color:#fff;
}
.selbar.is-on{display:flex}
.selbar__count{font-family:var(--font-mono);font-size:12px;font-weight:600}
.selbar__spacer{flex:1}
.selbar .btn{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.32);color:#fff}
.selbar .btn:hover{background:rgba(255,255,255,.22);border-color:rgba(255,255,255,.5);color:#fff}
.selbar .btn--solid{background:#fff;border-color:#fff;color:var(--accent-ink);font-weight:600}
.selbar .btn--solid:hover{background:#EAF2EF;color:var(--accent-ink)}

/* grid */
.gridwrap{flex:1;overflow-y:auto;min-height:0;padding:16px 14px 40px}
.grid{
  --cols:3;
  --grid-gap:14px;
  display:grid;
  gap:var(--grid-gap);
  /* The buyer sets the column count from the toolbar; app.js writes it to
     --cols. Fixed counts rather than auto-fill: the product card is the
     unit the buyer scans, and its size should be a decision, not a
     by-product of window width. minmax(0,1fr) lets a card shrink below its
     content's intrinsic width instead of overflowing the column. */
  grid-template-columns:repeat(var(--cols),minmax(0,1fr));
}

/* Density follows the column count. Rather than a separate "compact"
   switch, the card sheds detail as its column narrows — each tier drops
   whatever has stopped being readable at that width, in order of how
   little a buyer scanning a wall of stock needs it. */
.grid[data-cols="1"],.grid[data-cols="2"]{--grid-gap:16px}
.grid[data-cols="4"]{--grid-gap:12px}
.grid[data-cols="5"]{--grid-gap:11px}
.grid[data-cols="6"],.grid[data-cols="7"]{--grid-gap:9px}

/* --- product card : stock-tag construction --- */
.card{
  position:relative;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);
  overflow:hidden;text-align:left;width:100%;display:block;
  transition:border-color .12s,box-shadow .12s,transform .12s;
}
.card:hover{border-color:var(--line-2);box-shadow:var(--shadow-2);transform:translateY(-1px)}
.card__shot{position:relative;aspect-ratio:1/1;display:block;overflow:hidden}

/* A photo frame holds its SKU's tint until the photograph paints over
   it, so a card is never a white hole waiting for bytes.

   The placeholder sits BEHIND the image rather than the image being
   hidden until a script says otherwise. That ordering matters: the
   obvious construction is opacity:0 on the image plus an .is-loaded
   class added on load, but an image served from cache can already be
   complete before that handler is attached, so the load event never
   fires and the photograph stays invisible permanently. Failing that
   way turns a slow image into a missing one. Here the worst case is a
   shimmer that keeps animating underneath an opaque photograph, which
   nobody can see. */
.card__shot.is-photo{background:var(--tint,var(--surface-2))}
.card__shot.is-photo::before{
  content:'';position:absolute;inset:0;z-index:0;
  background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.38) 50%,transparent 70%);
  background-size:220% 100%;animation:shimmer 1.25s ease-in-out infinite;
}
.card__shot.is-photo.is-loaded::before,
.card__shot.is-photo.is-broken::before{display:none}
/* Above the placeholder, and opaque from the start. */
.card__shot.is-photo img{position:relative;z-index:1}
@media (prefers-reduced-motion:reduce){
  .card__shot.is-photo::before{animation:none}
}
.card__shot svg{width:100%;height:100%;display:block}
.card__sku-tab{
  position:absolute;top:0;right:0;
  padding:3px 7px 4px;border-bottom-left-radius:var(--r-sm);
  background:rgba(255,255,255,.92);
  font-family:var(--font-mono);font-size:9.5px;font-weight:500;letter-spacing:.04em;color:var(--ink-2);
}
.card__avail{
  position:absolute;left:7px;bottom:7px;
  font-family:var(--font-mono);font-size:9px;letter-spacing:.07em;text-transform:uppercase;
  padding:2px 6px;border-radius:100px;background:rgba(255,255,255,.92);color:var(--ink-2);
}
.card__avail.is-low{color:var(--signal)}
.card__avail.is-mto{color:var(--muted)}

.card__foot{position:relative;padding:11px 11px 10px;border-top:1px dashed var(--line-2)}
/* ticket notches */
.card__foot::before,.card__foot::after{
  content:"";position:absolute;top:-6px;width:10px;height:10px;border-radius:50%;
  background:var(--canvas);border:1px solid var(--line);
}
.card__foot::before{left:-6px}
.card__foot::after{right:-6px}
.card__name{
  font-size:12.5px;font-weight:500;line-height:1.3;color:var(--ink);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.6em;
}
.card__data{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-top:7px}
.card__price{font-family:var(--font-mono);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.02em}
.card__sku{font-family:var(--font-mono);font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
/* One row, always exactly one row.

   `height` rather than min-height, and nowrap rather than wrap, for two
   separate reasons.

   The look: three chips need ~208px of the card's inner width, which only
   2- and 3-up have. Wrapping was what turned the tighter densities into
   confetti. Chips are shed by count below instead, so what remains always
   fits on the single line.

   The arithmetic: the virtualised grid measures ONE probe card to get its
   row pitch, and the probe is built with no attributes. While this line
   grew with its content the probe under-measured every real card by its
   height — 17.8px a row, which compounds down a long scroll. A fixed
   height measures the same empty or full, so the probe is finally
   telling the truth. Keep it fixed. */
/* Every attribute, at every density — the narrow card scrolls instead of
   dropping any. `height` rather than min-height, and a hidden scrollbar,
   because the virtualised grid measures ONE probe card to get its row
   pitch and that probe carries no attributes: anything that lets this
   line change height makes the probe lie about every real card. It used
   to, by 17.8px a row, which compounds down a long scroll. Fixed height
   in, fixed height out. */
.card__attrs-wrap{position:relative;margin-top:7px}
.card__attrs{
  display:flex;gap:5px;flex-wrap:nowrap;height:18px;
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;-ms-overflow-style:none;   /* a bar would add height */
  scroll-behavior:smooth;overscroll-behavior-x:contain;
}
.card__attrs::-webkit-scrollbar{display:none}
/* 14 line + 2 padding + 2 border = the 18px above. Change one, change both. */
.attr{font-family:var(--font-mono);font-size:9.5px;line-height:14px;letter-spacing:.03em;color:var(--muted);background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:1px 5px;white-space:nowrap;flex:0 0 auto}

/* Shown only when there is actually something out of sight, and the one
   pointing at an end you have already reached hides itself. The ring of
   card-coloured shadow is what the chips slide under, so a half-chip
   never appears to collide with the control. */
.attrs__arrow{
  position:absolute;top:50%;transform:translateY(-50%);
  width:15px;height:15px;padding:0;z-index:2;
  display:none;place-items:center;cursor:pointer;
  border:1px solid var(--line);border-radius:50%;
  background:var(--surface);color:var(--ink);
  box-shadow:0 0 0 3px var(--surface);
}
.attrs__arrow svg{width:8px;height:8px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
.attrs__arrow--prev{left:0}
.attrs__arrow--next{right:0}
.attrs__arrow:hover{border-color:var(--ink);background:var(--surface-2)}
.card__attrs-wrap.is-scrollable .attrs__arrow{display:grid}
.card__attrs-wrap.is-start .attrs__arrow--prev{display:none}
.card__attrs-wrap.is-end   .attrs__arrow--next{display:none}
/* 5-up and tighter: one line of name, and the footer closes up. */
.grid:is([data-cols="5"],[data-cols="6"],[data-cols="7"]) .card__name{-webkit-line-clamp:1;min-height:0}
.grid:is([data-cols="5"],[data-cols="6"],[data-cols="7"]) .card__foot{padding:8px 8px 9px}
/* 6-up and tighter: the SKU is already stamped on the photograph, so the
   footer keeps the one number that is only there — the price. */
.grid:is([data-cols="6"],[data-cols="7"]) .card__sku{display:none}
.grid:is([data-cols="6"],[data-cols="7"]) .card__name{font-size:11.5px}
.grid:is([data-cols="6"],[data-cols="7"]) .card__price{font-size:12px}
.grid:is([data-cols="6"],[data-cols="7"]) .card__foot{padding:7px 7px 8px}

/* selection */
.card__check{
  position:absolute;top:7px;left:7px;width:19px;height:19px;border-radius:4px;
  border:1px solid rgba(20,23,26,.2);background:rgba(255,255,255,.94);
  display:none;place-items:center;box-shadow:var(--shadow-1);
}
.card__check svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round;opacity:0}
.is-selecting .card{cursor:pointer}
.is-selecting .card__check{display:grid}
.card.is-picked{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.card.is-picked .card__check{background:var(--accent);border-color:var(--accent)}
.card.is-picked .card__check svg{opacity:1}

/* ---- states ---- */
.state{
  display:none;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;padding:60px 24px;gap:6px;min-height:340px;
}
.state.is-on{display:flex}
.state__art{margin-bottom:12px}
.state h3{font-family:var(--font-display);font-variation-settings:"wdth" 112;font-size:17px;font-weight:700;margin:0}
.state p{margin:0;color:var(--muted);max-width:44ch;font-size:12.5px}
.state__actions{display:flex;gap:8px;margin-top:12px}

/* skeletons */
.skel-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);overflow:hidden}
.skel-shot{aspect-ratio:1/1;background:linear-gradient(100deg,#EDEEEC 30%,#F6F7F5 50%,#EDEEEC 70%);background-size:220% 100%;animation:shimmer 1.25s ease-in-out infinite}
.skel-foot{padding:11px;border-top:1px dashed var(--line)}
.skel-line{height:8px;border-radius:3px;background:linear-gradient(100deg,#EDEEEC 30%,#F6F7F5 50%,#EDEEEC 70%);background-size:220% 100%;animation:shimmer 1.25s ease-in-out infinite}
.skel-line + .skel-line{margin-top:7px}
@keyframes shimmer{0%{background-position:180% 0}100%{background-position:-40% 0}}
.skel-rail{padding:13px}
.skel-rail .skel-line{height:9px}
.skel-rail .skel-line.w60{width:60%}
.skel-rail .skel-line.w80{width:80%}
.skel-rail .skel-line.w40{width:40%}
.skel-gap{height:18px}

/* ============================================================
   3 · EXPORT
   ============================================================ */
#view-export{overflow-y:auto;display:none}
#view-export.is-active{display:block}
.export{max-width:1000px;margin:0 auto;padding:30px 28px 56px}
.export__cols{display:grid;grid-template-columns:1fr 336px;gap:20px;align-items:start;margin-top:20px}

.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden}
.panel__head{padding:12px 15px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px}
.panel__head h2{margin:0;font-size:13px;font-weight:600}
.panel__body{padding:15px}

.fmt{display:grid;gap:8px}
.fmt__opt{
  display:grid;grid-template-columns:16px 1fr auto;gap:11px;align-items:start;
  padding:11px 12px;border:1px solid var(--line);border-radius:var(--r-md);cursor:pointer;
  transition:border-color .12s,background .12s;
}
.fmt__opt:hover{background:var(--surface-2)}
.fmt__opt.is-on{border-color:var(--accent);background:var(--accent-soft)}
.fmt__radio{width:16px;height:16px;border-radius:50%;border:1px solid var(--line-2);background:var(--surface);margin-top:1px;display:grid;place-items:center}
.fmt__opt.is-on .fmt__radio{border-color:var(--accent)}
.fmt__opt.is-on .fmt__radio::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--accent)}
.fmt__name{font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px}
.fmt__ext{font-family:var(--font-mono);font-size:10px;color:var(--muted);border:1px solid var(--line);border-radius:var(--r-sm);padding:1px 4px}
.fmt__desc{font-size:12px;color:var(--muted);margin-top:2px}
.fmt__size{font-family:var(--font-mono);font-size:11px;color:var(--muted);white-space:nowrap;margin-top:2px}

.opts{display:grid;gap:2px;margin-top:4px}
.checkrow{display:flex;align-items:center;gap:9px;padding:7px 4px;cursor:pointer;border-radius:var(--r-sm)}
.checkrow:hover{background:var(--surface-2)}
.checkrow__box{width:14px;height:14px;border:1px solid var(--line-2);border-radius:3px;display:grid;place-items:center;background:var(--surface);flex:none}
.checkrow__box svg{width:9px;height:9px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;opacity:0}
.checkrow.is-on .checkrow__box{background:var(--accent);border-color:var(--accent)}
.checkrow.is-on .checkrow__box svg{opacity:1}
.checkrow span{font-size:12.5px;color:var(--ink-2)}

.sel-list{max-height:280px;overflow-y:auto}
.sel-row{display:grid;grid-template-columns:30px 1fr auto 20px;gap:10px;align-items:center;padding:7px 15px;border-bottom:1px solid var(--line)}
.sel-row:last-child{border-bottom:0}
.sel-row__thumb{width:30px;height:30px;border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--line)}
.sel-row__thumb svg{width:100%;height:100%;display:block}
.sel-row__name{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sel-row__sku{font-family:var(--font-mono);font-size:10.5px;color:var(--muted)}
.sel-row__price{font-family:var(--font-mono);font-size:12px;font-variant-numeric:tabular-nums}
.sel-row__x{width:20px;height:20px;display:grid;place-items:center;border-radius:var(--r-sm);color:var(--muted)}
.sel-row__x:hover{background:rgba(20,23,26,.07);color:var(--danger)}

.summary{display:grid;gap:0;border:1px solid var(--line);border-radius:var(--r-md);overflow:hidden;margin-bottom:14px}
.summary__row{display:flex;justify-content:space-between;gap:12px;padding:9px 12px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.summary__row:last-child{border-bottom:0}
.summary__row dt{color:var(--muted);font-size:12px;margin:0}
.summary__row dd{margin:0;font-family:var(--font-mono);font-size:12px;font-weight:500;text-align:right}

.confirm{display:none;text-align:center;padding:44px 24px}
.confirm.is-on{display:block}
.confirm__tick{
  width:46px;height:46px;border-radius:50%;background:var(--accent-soft);
  display:grid;place-items:center;margin:0 auto 16px;
}
.confirm__tick svg{width:22px;height:22px;stroke:var(--accent);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.confirm h2{font-family:var(--font-display);font-variation-settings:"wdth" 112;font-size:19px;margin:0 0 6px}
.confirm p{margin:0 auto;color:var(--muted);max-width:44ch}
.confirm__file{
  display:inline-flex;align-items:center;gap:9px;margin-top:16px;padding:9px 13px;
  border:1px solid var(--line);border-radius:var(--r-md);background:var(--surface-2);
  font-family:var(--font-mono);font-size:12px;
}
.confirm__actions{display:flex;gap:8px;justify-content:center;margin-top:20px}

/* The filter drawer's toggle exists only on narrow screens. Declared
   before the media query below so source order doesn't defeat it. */
.filters-toggle{display:none}

@media (max-width:1080px){
  .drops{grid-template-columns:1fr}
  .export__cols{grid-template-columns:1fr}
}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
}

/* ============================================================
   PRODUCTION ADDITIONS
   Everything above this line is the approved demo, untouched.
   Below: the UI the live app needs that the mockup faked.
   ============================================================ */

/* --- real product photography -------------------------------- */
/* The demo only ever drew SVG. Real photos need object-fit so a
   portrait shot doesn't distort inside the square card. */
/* No background on the image itself: the frame behind it carries the
   SKU's tint while the bytes are in flight, and an opaque grey here
   would cover it. Fade-in lives with .card__shot.is-photo above. */
.card__shot img{
  width:100%;height:100%;display:block;object-fit:cover;
}
.sel-row__thumb img{width:100%;height:100%;object-fit:cover;display:block;border-radius:var(--r-sm)}
.qrow__thumb{overflow:hidden;background:var(--surface-2)}
.qrow__thumb img{width:100%;height:100%;object-fit:cover;display:block}

/* --- sheet connect form (replaces the fake sheet drop zone) --- */
.drop__zone--form{cursor:default}
.sheet-form{display:flex;gap:6px;width:100%;max-width:420px;margin-top:4px}
.sheet-form input{
  flex:1;min-width:0;height:30px;padding:0 10px;
  border:1px solid var(--line-2);border-radius:var(--r-md);background:var(--surface);
  font-family:var(--font-mono);font-size:11.5px;
}
.sheet-form input:focus{border-color:var(--accent);outline:none;box-shadow:0 0 0 3px rgba(14,79,66,.1)}
.sheet-msg{
  font-family:var(--font-mono);font-size:11px;min-height:15px;color:var(--muted);
  /* Failures from Google can be a couple of sentences of guidance, so this
     has to wrap and stay readable rather than clip to one line. */
  max-width:420px;line-height:1.5;text-align:center;
  overflow-wrap:anywhere;
}
.sheet-msg.is-err{color:var(--danger)}
.sheet-msg.is-ok{color:var(--accent)}

/* --- upload queue progress ------------------------------------ */
.qrow__bar{grid-column:1 / -1;height:2px;background:#EDEEEC;border-radius:2px;overflow:hidden;margin-top:5px}
.qrow__bar i{display:block;height:100%;background:var(--accent);border-radius:2px;transition:width .2s ease}
.qrow.is-done .qrow__bar{display:none}

/* --- export progress ------------------------------------------ */
/* Real exports take seconds (fetching images from R2), unlike the
   demo's instant fake. Without feedback it reads as a hang. */
#exportProgress{display:none}
#exportProgress.is-on{display:block}
.confirm__spinner{
  width:34px;height:34px;margin:0 auto 14px;border-radius:50%;
  border:2.5px solid var(--accent-soft);border-top-color:var(--accent);
  animation:spin .8s linear infinite;
}
.progress{
  width:100%;max-width:320px;margin:18px auto 0;height:4px;
  background:var(--line);border-radius:4px;overflow:hidden;
}
.progress i{display:block;height:100%;background:var(--accent);border-radius:4px;transition:width .25s ease}

/* --- toasts ---------------------------------------------------- */
/* The card stays white whatever it is reporting. Only the disc on the
   left carries the colour — green for done, red for failed — which is
   enough to read at a glance and quiet enough to stack four deep
   without the corner of the screen turning into a traffic light. */
.toasts{
  position:fixed;right:14px;bottom:calc(var(--status-h) + 12px);
  display:flex;flex-direction:column;gap:8px;z-index:60;
  max-width:min(380px,calc(100vw - 28px));
  pointer-events:none;                 /* the gaps must not eat clicks */
}
.toast{
  pointer-events:auto;
  display:flex;gap:10px;align-items:flex-start;
  padding:11px 12px;border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--shadow-2);
  font-size:12.5px;line-height:1.45;color:var(--ink-2);
  animation:toast-in .2s cubic-bezier(.2,.8,.3,1);
  transition:opacity .18s ease, transform .18s ease;
}
.toast.is-leaving{opacity:0;transform:translateX(8px)}

/* The disc. 18px is the smallest a tick reads cleanly at. */
.toast__icon{
  flex:none;width:18px;height:18px;margin-top:.5px;border-radius:50%;
  display:grid;place-items:center;
}
.toast__icon svg{
  width:11px;height:11px;fill:none;stroke:#fff;
  stroke-width:3;stroke-linecap:round;stroke-linejoin:round;
}
.toast--ok  .toast__icon{background:var(--accent)}
.toast--err .toast__icon{background:var(--danger)}
.toast--info .toast__icon{background:var(--line-2)}

/* Pending is the same disc with its ring turning, so a toast that
   resolves does not change size or shift the stack. */
.toast--pending .toast__icon{
  background:none;
  border:2px solid var(--line);border-top-color:var(--ink-2);
  animation:spin .7s linear infinite;
}

.toast__body{min-width:0;flex:1}
.toast__title{display:block;color:var(--ink);font-weight:600;margin-bottom:1px}
.toast__msg{display:block;overflow-wrap:anywhere}
.toast__x{
  flex:none;margin:-2px -3px 0 auto;padding:3px;border-radius:var(--r-sm);
  color:var(--muted);line-height:0;
}
.toast__x svg{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round}
.toast__x:hover{color:var(--ink);background:var(--surface-2)}
@keyframes toast-in{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}

/* --- challan history skeleton ----------------------------------- */
/* The row's real geometry, so nothing shifts when the answer lands. */
.hist__row--skel{pointer-events:none}
.hist__row--skel .skel-line{
  display:block;height:9px;border-radius:3px;
  background:linear-gradient(100deg,#EDEEEC 30%,#F6F7F5 50%,#EDEEEC 70%);
  background-size:220% 100%;animation:shimmer 1.25s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){
  .hist__row--skel .skel-line{animation:none}
}

/* --- global activity bar --------------------------------------- */
/* Every request in flight lights this. The app talks to the network on
   almost every click, and without a single always-visible signal that
   something is happening the wait reads as the click not registering —
   which is what makes people click again and call it sluggish. */
.netbar{
  position:fixed;top:0;left:0;right:0;height:2px;z-index:90;
  pointer-events:none;opacity:0;transition:opacity .2s ease .1s;
}
.netbar.is-on{opacity:1;transition-delay:0s}
.netbar i{
  display:block;height:100%;width:100%;
  background:linear-gradient(90deg,transparent,var(--accent),transparent);
  transform:translateX(-100%);
}
.netbar.is-on i{animation:netbar 1.1s ease-in-out infinite}
@keyframes netbar{
  0%{transform:translateX(-100%)}
  100%{transform:translateX(100%)}
}
@media (prefers-reduced-motion:reduce){
  .netbar.is-on i{animation:none;transform:none;opacity:.5}
}

/* --- misc ------------------------------------------------------ */
[hidden]{display:none !important}
/* A dimmed button still looks like a button that did nothing. The
   spinner takes the icon's slot as the first flex item, so an
   icon-only button spins where its icon was and a labelled one spins
   beside its label — no absolute positioning, and the label never
   moves. */
.btn.is-busy{pointer-events:none;opacity:.75}
.btn.is-busy > svg,.btn.is-busy > .icon{display:none}
.btn.is-busy::before{
  content:'';flex:none;width:13px;height:13px;border-radius:50%;
  border:2px solid var(--line-2);border-top-color:var(--accent);
  animation:spin .65s linear infinite;
}
@media (prefers-reduced-motion:reduce){
  .btn.is-busy::before,.toast--pending .toast__icon{animation-duration:1.6s}
}
.rail__source b{color:var(--ink-2);font-weight:500}

/* --- virtualised grid ------------------------------------------ */
/* Only the rows near the viewport are built as cards. These two
   spacers hold open the scroll height for everything above and below,
   so the scrollbar stays honest. They span every column so the
   auto-fill layout is unaffected. */
.grid__spacer{grid-column:1 / -1;pointer-events:none}

/* Active-filter count on the drawer toggle. */
.filters-toggle .group__badge{background:var(--accent);color:#fff;margin-left:1px}

/* ============================================================
   CARD CAROUSEL
   Arrows sit on the photograph, not below it, so the card's footer
   keeps its ticket construction. They are quiet until the card is
   hovered or focused — a grid of 40 cards each shouting two arrows
   would bury the products. On touch, where there is no hover, they
   stay visible.
   ============================================================ */
.shot__arrow{
  position:absolute;top:50%;transform:translateY(-50%);
  width:30px;height:30px;display:grid;place-items:center;
  border-radius:50%;
  background:rgba(255,255,255,.92);
  border:1px solid rgba(20,23,26,.10);
  box-shadow:0 1px 3px rgba(20,23,26,.18);
  color:var(--ink);
  /* Resting at .55 rather than 0. Fully hidden meant a card with four
     photographs was indistinguishable from one with a single photo
     until the pointer happened to cross it, so the arrows read as
     missing rather than quiet — nobody hunts for a control they have
     no reason to believe exists. This is legible enough to notice on a
     card you are looking at and faint enough that forty of them do not
     compete with the products. */
  opacity:.55;transition:opacity .13s ease,background .13s ease;
  z-index:3;
}
.shot__arrow svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.1;stroke-linecap:round;stroke-linejoin:round}
.shot__arrow--prev{left:7px}
.shot__arrow--next{right:7px}
.card:hover .shot__arrow,
.card:focus-within .shot__arrow{opacity:1}
.shot__arrow:hover{background:#fff;opacity:1}
.shot__arrow:focus-visible{opacity:1}

/* One photograph, no paging: the arrows are not rendered at all in that
   case, but guard anyway so a stale card can never show a dead control. */
.card__shot[data-count="1"] .shot__arrow{display:none}

/* Position pill, in the mono face every other number on the card uses. */
.shot__counter{
  position:absolute;top:7px;left:7px;z-index:3;
  padding:2px 6px;border-radius:100px;
  background:rgba(20,23,26,.62);color:#fff;
  font-size:9.5px;font-weight:500;letter-spacing:.04em;
  backdrop-filter:blur(2px);
}
/* The SKU tab already owns the bottom-left corner. */
.card__shot[data-count="1"] .shot__counter{display:none}

/* Select mode used to hide the arrows entirely. That made them
   unreachable in ordinary use: the first click on any card turns select
   mode ON, so the moment someone touched a product they lost the
   ability to page its photographs, with no way back except leaving the
   mode. Paging and picking are not in conflict — the arrow handler
   stops propagation, so pressing one never toggles the tick — and
   choosing which products to send is exactly when a buyer most wants
   to look at the other angles. They stay. */
@media (hover:none){
  /* No hover to reveal them, so they stay put. */
  .shot__arrow{opacity:.95}
}

/* --- carousel image swap ---------------------------------------- */
/* Paging to the next photograph must not blank the card. The frame
   keeps showing the image it has while the next one decodes, and says
   it is working with a small spinner in the corner rather than by
   going empty — the swap is a replacement, not a fresh load. */
.card__shot.is-swapping::after{
  content:'';position:absolute;top:8px;right:8px;z-index:4;
  width:14px;height:14px;border-radius:50%;
  border:2px solid rgba(255,255,255,.55);border-top-color:#fff;
  box-shadow:0 1px 3px rgba(20,23,26,.28);
  animation:spin .65s linear infinite;
}

/* ============================================================
   02 · GALLERY

   The catalogue is product-first: a photograph there is an illustration
   of a thing for sale. The gallery is file-first — here the photograph
   IS the subject, so its name leads, set in the mono face every other
   piece of data in this app uses. Tiles are denser and squarer than
   product cards so a few hundred can be scanned at once, and they drop
   the ticket-notch construction, which belongs to products.
   ============================================================ */
#view-gallery{overflow-y:auto;display:none}
#view-gallery.is-active{display:block}
.gallery{max-width:1180px;margin:0 auto;padding:30px 28px 56px}

/* --- controls --- */
.gal-bar{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:10px 12px;margin-bottom:14px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
}

/* Segmented filter. One control, three states — clearer than three
   toggles that could contradict each other. */
.seg{display:flex;border:1px solid var(--line-2);border-radius:var(--r-md);overflow:hidden;background:var(--surface-2)}
.seg__btn{
  height:30px;padding:0 11px;font-size:12.5px;font-weight:500;color:var(--muted);
  border-right:1px solid var(--line);white-space:nowrap;
}
.seg__btn:last-child{border-right:0}
.seg__btn:hover{background:var(--surface);color:var(--ink)}
.seg__btn.is-on{background:var(--accent);color:#fff}
.seg__n{
  font-family:var(--font-mono);font-size:10px;margin-left:5px;
  padding:1px 5px;border-radius:100px;background:var(--signal-soft);color:var(--signal);
}
.seg__btn.is-on .seg__n{background:rgba(255,255,255,.22);color:#fff}

/* --- the sheet --- */
.gal-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(148px,1fr));
  gap:12px;
}
.gal-tile{
  position:relative;background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r-md);
  overflow:hidden;transition:border-color .12s,box-shadow .12s;
}
.gal-tile:hover{border-color:var(--line-2);box-shadow:var(--shadow-1)}
.gal-tile.is-picked{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.gal-tile.is-orphan{border-style:dashed;border-color:#E6D5B0}

/* Same construction as the catalogue card: the shimmer sits behind an
   always-opaque image, so a cached photograph can never be left hidden
   by a load event that already fired. */
.gal-tile__shot{position:relative;aspect-ratio:1/1;display:block;background:var(--surface-2);overflow:hidden}
.gal-tile__shot::before{
  content:'';position:absolute;inset:0;z-index:0;
  background:linear-gradient(100deg,#EDEEEC 30%,#F6F7F5 50%,#EDEEEC 70%);
  background-size:220% 100%;animation:shimmer 1.25s ease-in-out infinite;
}
.gal-tile__shot.is-loaded::before,
.gal-tile__shot.is-broken::before{display:none}
.gal-tile__shot img{width:100%;height:100%;object-fit:cover;display:block;position:relative;z-index:1}
@media (prefers-reduced-motion:reduce){
  .gal-tile__shot::before{animation:none}
}

.gal-tile__foot{padding:8px 9px 9px;border-top:1px solid var(--line)}
/* The name is the subject of this screen, so it is the loudest thing on
   the tile — and it is data, so it is mono. */
.gal-tile__name{
  font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--ink);
  display:block;width:100%;text-align:left;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  border-radius:2px;padding:1px 2px;margin:-1px -2px;
}
.gal-tile__name:hover{background:var(--accent-soft);color:var(--accent-ink)}
.gal-tile__sub{
  display:block;font-size:11px;color:var(--muted);margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.gal-tile__sub--warn{color:var(--signal);font-weight:500}

/* Inline rename. Editing happens where the name already is, so there is
   no dialog to lose your place in. */
.gal-tile__edit{
  width:100%;height:24px;padding:0 5px;
  font-family:var(--font-mono);font-size:12px;font-weight:600;
  border:1px solid var(--accent);border-radius:var(--r-sm);
  background:var(--surface);color:var(--ink);
}
.gal-tile__edit:focus{outline:none;box-shadow:0 0 0 3px rgba(14,79,66,.12)}
.gal-tile__hint{
  display:block;font-size:10px;line-height:1.35;margin-top:3px;color:var(--muted);
  font-family:var(--font-mono);
}
.gal-tile__hint.is-err{color:var(--danger)}

/* Actions appear on the photograph, quiet until wanted. */
.gal-tile__acts{
  position:absolute;top:6px;right:6px;display:flex;gap:4px;
  opacity:0;transition:opacity .12s ease;
}
.gal-tile:hover .gal-tile__acts,
.gal-tile:focus-within .gal-tile__acts{opacity:1}
.gal-act{
  width:26px;height:26px;display:grid;place-items:center;border-radius:var(--r-sm);
  background:rgba(255,255,255,.94);border:1px solid rgba(20,23,26,.10);
  box-shadow:0 1px 3px rgba(20,23,26,.16);color:var(--ink-2);
}
.gal-act:hover{background:#fff;color:var(--ink)}
.gal-act--danger:hover{background:#FDF6F4;color:var(--danger);border-color:#D9B3AC}
.gal-act svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}

/* Position marker, so the running order is legible at a glance. */
.gal-tile__pos{
  position:absolute;left:6px;top:6px;
  font-family:var(--font-mono);font-size:9.5px;font-weight:600;
  padding:2px 6px;border-radius:100px;
  background:rgba(20,23,26,.62);color:#fff;
}
.gal-tile__pos--cover{background:var(--accent)}

.gal-tile__check{
  position:absolute;left:6px;bottom:6px;width:20px;height:20px;border-radius:4px;
  background:var(--surface);border:1px solid var(--line-2);display:none;place-items:center;
}
body.is-gal-selecting .gal-tile__check{display:grid}
body.is-gal-selecting .gal-tile__acts{display:none}
.gal-tile__check svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:2.6;stroke-linecap:round;opacity:0}
.gal-tile.is-picked .gal-tile__check{background:var(--accent);border-color:var(--accent)}
.gal-tile.is-picked .gal-tile__check svg{opacity:1}

.btn--danger{background:var(--danger);border-color:var(--danger);color:#fff}
.btn--danger:hover{background:#73261B;border-color:#73261B;color:#fff}

.gal-empty{display:none}
.gal-empty.is-on{display:flex}
.gal-more{display:flex;justify-content:center;padding:22px 0 4px}
.gal-skel{aspect-ratio:1/1;border-radius:var(--r-md);background:var(--surface-2);border:1px solid var(--line)}

.convention__note{margin:9px 0 0;font-size:11.5px;color:var(--muted);line-height:1.5}

/* ============================================================
   RESPONSIVE LAYER — deliberately the last thing in this file.

   Several rules here match at the SAME specificity as the base rules
   they override (.sheet-form input, .toolbar .btn). CSS settles ties by
   source order, so this block must sit after every base rule, including
   any added later. Keep it at the bottom.

   The desktop layout is a three-panel ledger: filters, grid, status.
   On a phone it folds into one column. Nothing is restyled for its own
   sake — same hairline rules, same mono face on every number, same
   ledger green reserved for the one action that matters per screen.
   What changes is what earns a row of its own, and what gets a target
   big enough for a thumb.
   ============================================================ */
@media (max-width:900px){

  /* Mobile browser chrome collapses on scroll; 100vh would hide the
     status bar behind it. dvh tracks the visible viewport. */
  .app{height:100dvh}

  /* --- app bar: identity on row one, the three steps on row two --- */
  .appbar{
    height:auto;flex-wrap:wrap;gap:0 10px;padding:0 12px;
    padding-top:env(safe-area-inset-top,0);
  }
  .brand{height:48px;flex:1;min-width:0}
  .brand__sub{display:none}          /* the sheet name is in the status bar */
  .appbar__right{height:48px}
  .sync{border:0;background:none;padding:0;gap:7px}
  .sync__text{display:none}

  .nav{order:3;width:100%;margin:0;gap:0;border-top:1px solid var(--line)}
  .nav__item{flex:1;justify-content:center;height:42px;padding:0 6px;gap:5px;font-size:12.5px}
  .nav__item::after{left:0;right:0}

  /* --- filter rail becomes a drawer --- */
  .rail{
    position:absolute;z-index:40;top:0;bottom:0;left:0;
    width:min(320px,88vw);
    transform:translateX(-100%);
    transition:transform .2s cubic-bezier(.2,.8,.3,1);
    box-shadow:var(--shadow-2);
  }
  body.is-rail-open .rail{transform:none}
  .rail-scrim{
    position:absolute;inset:0;z-index:30;background:rgba(20,23,26,.4);
    opacity:0;pointer-events:none;transition:opacity .2s ease;
  }
  body.is-rail-open .rail-scrim{opacity:1;pointer-events:auto}
  .opt{padding:9px 8px}
  .rail__head{padding:13px}

  /* --- toolbar: search, then the three controls, then the rail ---
     The result count lives in the status bar, so it comes out. Per-row
     gets a full-width row of its own, last, sitting directly above the
     grid it changes and wide enough to drag with a thumb. */
  .toolbar{flex-wrap:wrap;row-gap:8px;padding:9px 12px}
  .search{order:-1;flex:1 1 100%;width:100%}
  .toolbar__spacer,.vsep,.count{display:none}
  .filters-toggle{display:inline-flex}
  .toolbar .btn,.toolbar .select-wrap select{flex:1;justify-content:center}

  .density{--thumb:18px;order:1;flex:1 1 100%;height:36px;padding:0 12px 0 11px;gap:11px}
  .density__rail{width:auto;height:34px}
  .density__cap{display:none}      /* the readout plus the glyph is enough at this width */
  .density input::-webkit-slider-thumb{height:24px}
  .density input::-moz-range-thumb{height:24px}

  /* --- grid --- */
  .gridwrap{padding:12px 12px 40px}
  /* The chips stay here too, and none are dropped: what made them confetti
     was wrapping, and they no longer wrap. A handset swipes the row
     directly, so the arrows are only a pointer affordance — but they are
     given a wider tap target here, where the card is narrowest. */
  .attrs__arrow{width:17px;height:17px}
  .card__foot{padding:9px 9px 10px}

  /* The tiers shift down three counts here: a 4-up column on a 390px
     handset is ~83px, narrower than a 7-up column at a desk. Same
     shedding order, reached sooner. */
  .grid:is([data-cols="3"],[data-cols="4"]) .card__name{-webkit-line-clamp:1;min-height:0}
  .grid:is([data-cols="3"],[data-cols="4"]) .card__sku{display:none}
  .grid:is([data-cols="3"],[data-cols="4"]) .card__foot{padding:7px 7px 8px}
  .grid[data-cols="4"] .card__name{font-size:11.5px}
  .grid[data-cols="4"] .card__price{font-size:12px}
  .grid[data-cols="4"] .card__sku-tab{padding:2px 5px 3px;font-size:9px}
  /* Bigger arrows for thumbs, tucked closer to the edge on a 188px card. */
  .shot__arrow{width:34px;height:34px}
  .shot__arrow--prev{left:4px}
  .shot__arrow--next{right:4px}
  .card__check{width:24px;height:24px}

  .chipbar{padding:8px 12px}
  .selbar{padding:9px 12px;gap:8px;flex-wrap:wrap}

  /* --- status bar: one honest line of numbers --- */
  .statusbar{
    height:auto;min-height:var(--status-h);gap:10px;padding:6px 12px;
    padding-bottom:calc(6px + env(safe-area-inset-bottom,0px));
    font-size:10.5px;
  }
  .statusbar__sep{display:none}
  .statusbar span:last-of-type{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:45%}

  /* --- gallery --- */
  .gallery{padding:20px 12px 44px}
  .gal-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
  .gal-bar{flex-wrap:wrap;row-gap:8px;padding:9px}
  .gal-bar .search{order:-1;width:100%}
  .gal-bar .toolbar__spacer,.gal-bar .count{display:none}
  .seg{flex:1}
  .seg__btn{flex:1;height:36px;padding:0 8px;font-size:12px}
  /* No hover on a phone, so the actions stay put. */
  .gal-tile__acts{opacity:1}
  .gal-act{width:30px;height:30px}

  /* --- upload --- */
  .upload{padding:20px 12px 44px}
  .page-head h1{font-size:21px}
  .drop{padding:15px}
  .drop__zone{min-height:150px;padding:16px}
  /* A five-column queue row cannot survive 390px: filename on its own
     line, the facts beneath it. */
  .qrow{
    grid-template-columns:28px 1fr auto;
    grid-template-areas:"thumb name name" ". sku status" ". size size";
    gap:3px 10px;padding:10px 12px;
  }
  .qrow__thumb{grid-area:thumb;width:28px;height:28px}
  .qrow__name{grid-area:name;white-space:normal;overflow-wrap:anywhere}
  .qrow > span:nth-of-type(2){grid-area:sku;text-align:left}
  .qrow > span:nth-of-type(3){grid-area:status;justify-self:end}
  .qrow > span:nth-of-type(4){grid-area:size;text-align:left}
  .queue__head{flex-wrap:wrap;row-gap:6px;padding:11px 12px}
  .convention__row{flex-wrap:wrap}
  .token--sep{display:none}
  .sheet-form{flex-wrap:wrap}
  .sync-panel{padding:14px;gap:12px}
  .sync-panel__actions{margin-left:0;width:100%;flex-wrap:wrap}
  .sync-panel__actions .btn{flex:1;justify-content:center}

  /* --- export --- */
  .export{padding:20px 12px 44px}
  .export__cols > div{position:static !important}   /* nothing sticky on a phone */
  .confirm{padding:30px 16px}
  .confirm__actions{flex-direction:column}
  .confirm__actions .btn{width:100%;justify-content:center}
  .fmt__opt,.checkrow{padding:12px 11px}

  /* --- thumb-sized targets, declared last so nothing outranks them --- */
  .btn{height:36px;padding:0 13px}
  .btn--lg{height:40px}
  .select-wrap select{height:36px}
  .search input,.sheet-form input{height:38px}
  .sheet-form input{flex:1 1 100%;min-width:0}
  .sync__btn{height:34px}
  .rail__head .btn{height:32px;padding:0 9px}
}

@media (max-width:359px){
  .nav__item{font-size:11.5px;padding:0 3px}
}

/* Resume control for bulk uploads. Sits under the drop zone because it
   changes what a drop will do, and people read it there. */
.skip-toggle{
  display:flex;align-items:center;gap:8px;margin-top:12px;
  font-size:12.5px;color:var(--ink-2);cursor:pointer;
}
.skip-toggle input{width:15px;height:15px;accent-color:var(--accent);cursor:pointer}

/* ============================================================
   05 · VENDORS  +  THE CHALLAN WIZARD

   The app has had no modal until now, on purpose: every other step
   is a place you navigate to and can leave your work in. Issuing a
   challan is different — it is one transaction, begun from a
   selection that only exists in the catalogue's memory, and walking
   away halfway through should discard it rather than leave a
   half-written document lying around. That is what a modal says.
   ============================================================ */

/* --- shared form atoms ------------------------------------------ */
.field{display:grid;gap:4px;min-width:0}
.field__k{
  font-family:var(--font-mono);font-size:9.5px;font-weight:600;
  letter-spacing:.12em;text-transform:uppercase;color:var(--muted);
}
.field input,.field textarea,.field select{
  height:30px;padding:0 9px;
  border:1px solid var(--line-2);border-radius:var(--r-md);
  background:var(--surface);font-size:12.5px;width:100%;
}
.field textarea{height:auto;min-height:56px;padding:7px 9px;line-height:1.45;resize:vertical}
.field input:focus,.field textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(14,79,66,.1);
}
.field input::placeholder{color:var(--line-2)}
.field--wide{grid-column:1 / -1}
.field__hint{font-size:11px;color:var(--muted)}
.field.is-bad input{border-color:var(--danger);box-shadow:0 0 0 3px rgba(140,47,34,.1)}

.ch-fields{display:grid;gap:11px}
.ch-fields--2{grid-template-columns:1fr 1fr}

/* --- modal shell ------------------------------------------------- */
.modal{position:fixed;inset:0;z-index:60;display:grid;place-items:center;padding:26px}
.modal[hidden]{display:none}
.modal__scrim{position:absolute;inset:0;background:rgba(20,23,26,.42);backdrop-filter:blur(2px)}
.modal__box{
  position:relative;
  width:min(1180px,100%);max-height:100%;
  display:flex;flex-direction:column;
  background:var(--canvas);
  border:1px solid var(--line-2);border-radius:var(--r-lg);
  box-shadow:0 24px 60px -12px rgba(20,23,26,.4),0 2px 6px rgba(20,23,26,.12);
  overflow:hidden;
  animation:modal-in .16s ease-out;
}
@keyframes modal-in{from{opacity:0;transform:translateY(6px) scale(.995)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.modal__box{animation:none}}

.modal__head{
  flex:none;display:flex;align-items:center;gap:20px;
  padding:12px 15px;background:var(--surface);border-bottom:1px solid var(--line);
}
.modal__title h2{margin:1px 0 0;font-size:14px;font-weight:600;letter-spacing:-.005em}
.modal__x{
  margin-left:auto;width:26px;height:26px;display:grid;place-items:center;
  border-radius:var(--r-sm);color:var(--muted);
}
.modal__x:hover{background:rgba(20,23,26,.06);color:var(--ink)}

/* Step rail. Numbered like the app bar, so "01 / 02" means the same
   thing here as it does up there. */
.steps{display:flex;align-items:center;gap:0;margin:0;padding:0;list-style:none}
.steps li{
  display:flex;align-items:center;gap:7px;
  padding:5px 12px;font-size:12.5px;color:var(--muted);
  border:1px solid var(--line);border-right:0;background:var(--surface-2);
}
.steps li:first-child{border-radius:var(--r-md) 0 0 var(--r-md)}
.steps li:last-child{border-right:1px solid var(--line);border-radius:0 var(--r-md) var(--r-md) 0}
.steps li span{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;color:var(--line-2)}
.steps li.is-on{background:var(--accent-soft);border-color:#A8C6BD;color:var(--accent-ink);font-weight:600}
.steps li.is-on span{color:var(--accent)}
.steps li.is-done{color:var(--ink-2);background:var(--surface)}

.modal__body{flex:1;min-height:0;overflow-y:auto;padding:16px}
.modal__foot{
  flex:none;display:flex;align-items:center;gap:8px;
  padding:11px 15px;background:var(--surface);border-top:1px solid var(--line);
}
.ch-foot__note{flex:1;font-family:var(--font-mono);font-size:11.5px;color:var(--muted)}
.ch-foot__note b{color:var(--ink);font-weight:600}

.ch-step{display:none}
.ch-step.is-on{display:block}
.ch-busy.is-on{display:grid;place-items:center;gap:10px;text-align:center;padding:60px 20px}
.ch-busy h3{margin:0;font-size:14px;font-weight:600}
.ch-busy p{margin:0;font-size:12.5px;color:var(--muted)}
.ch-busy .progress{width:min(360px,100%)}

/* --- step 01 · the line-item table ------------------------------- */
.ch-bulk{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding:9px 12px;margin-bottom:12px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);
}
.ch-bulk__f{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2)}
.ch-bulk__f input{
  width:66px;height:26px;padding:0 7px;text-align:right;
  border:1px solid var(--line-2);border-radius:var(--r-sm);
  background:var(--surface);font-family:var(--font-mono);font-size:12px;
}
.ch-bulk__note{font-size:11.5px;color:var(--muted)}

.ch-tablewrap{
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  overflow:auto;max-height:min(58vh,560px);
}
.ch-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px}
.ch-table thead th{
  position:sticky;top:0;z-index:2;
  background:var(--surface-2);border-bottom:1px solid var(--line);
  padding:8px 10px;text-align:left;white-space:nowrap;
  font-family:var(--font-mono);font-size:9.5px;font-weight:600;
  letter-spacing:.12em;text-transform:uppercase;color:var(--muted);
}
.ch-table tbody td{padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:middle}
.ch-table tbody tr:last-child td{border-bottom:0}
.ch-table tbody tr:hover{background:var(--surface-2)}
.ch-c-n{width:38px;text-align:right !important;font-family:var(--font-mono);color:var(--muted)}
.ch-c-img{width:56px}
.ch-c-num{width:96px;text-align:right !important}
.ch-c-x{width:34px}
.ch-thumb{
  /* display:block matters: a bare <span> is inline, so width and height
     are ignored and the photo falls back to its natural aspect ratio —
     a tall shot became a thin strip. */
  display:block;width:38px;height:38px;
  border-radius:var(--r-sm);overflow:hidden;
  border:1px solid var(--line);background:var(--surface-2);
}
.ch-thumb img,.ch-thumb svg{width:100%;height:100%;display:block;object-fit:cover}
.ch-sku{font-family:var(--font-mono);font-size:12px;font-weight:500}
.ch-name{font-size:11.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:26ch}
.ch-type{font-size:12px;color:var(--ink-2)}

/* Number inputs sit flush in the cell: a boxed control per cell would
   turn 20 rows into a wall of borders. The border appears on hover and
   focus, which is when it actually tells you something. */
.ch-in{
  width:100%;height:26px;padding:0 7px;text-align:right;
  border:1px solid transparent;border-radius:var(--r-sm);
  background:transparent;
  font-family:var(--font-mono);font-size:12px;font-variant-numeric:tabular-nums;
}
.ch-in:hover{border-color:var(--line);background:var(--surface)}
.ch-in:focus{outline:none;border-color:var(--accent);background:var(--surface);box-shadow:0 0 0 3px rgba(14,79,66,.1)}
.ch-in::-webkit-outer-spin-button,.ch-in::-webkit-inner-spin-button{opacity:.35}
.ch-val{font-family:var(--font-mono);font-size:12.5px;font-weight:500;font-variant-numeric:tabular-nums}
.ch-row__x{width:22px;height:22px;display:grid;place-items:center;border-radius:var(--r-sm);color:var(--line-2)}
.ch-row__x:hover{background:rgba(140,47,34,.08);color:var(--danger)}
.ch-table tfoot td{
  position:sticky;bottom:0;z-index:2;
  background:var(--accent-soft);border-top:1px solid #A8C6BD;
  padding:9px 10px;font-weight:600;color:var(--accent-ink);
}

/* --- step 02 · vendor and document ------------------------------- */
.ch-cols{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
.ch-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden}
.ch-card--wide{grid-column:1 / -1}
.ch-card__head{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--line)}
.ch-card__head h3{margin:0;font-size:13px;font-weight:600}
.ch-card__body{padding:14px;display:grid;gap:12px}

.ch-vcard{
  padding:11px 12px;border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--surface-2);font-size:12.5px;line-height:1.55;color:var(--ink-2);
}
.ch-vcard b{color:var(--ink);font-weight:600}
.ch-vcard .num{color:var(--ink-2)}
/* pre-wrap here and nowhere else: a vendor's address is stored with the
   line breaks they typed, and those are what gets printed on the challan,
   so the card has to show them. Everything else in these cards uses real
   markup for its line breaks and would otherwise inherit the source
   file's own indentation as blank lines. */
.ch-vcard__grid{display:grid;grid-template-columns:auto 1fr;gap:2px 14px;white-space:pre-wrap}
.ch-vcard__grid .ch-vcard__k{white-space:nowrap}
.is-view{white-space:normal}
.ch-vcard__k{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}

/* The standing-discount notice. Amber rather than green: it is telling
   you a number changed underneath you, which is worth a second's
   attention even when it is exactly what you wanted. */
.ch-disc{
  display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  padding:8px 11px;border-radius:var(--r-md);
  background:var(--signal-soft);border:1px solid #E8D9BC;
  font-size:12px;color:#6B4907;
}
.ch-disc[hidden]{display:none}
.ch-disc b{font-weight:600}

/* ============================================================
   THE VENDORS VIEW
   ============================================================ */
#view-vendors{overflow-y:auto;display:none}
#view-vendors.is-active{display:block}
.vendors{max-width:1180px;margin:0 auto;padding:30px 28px 56px}
.vendors__cols{display:grid;grid-template-columns:1.25fr 1fr;gap:16px;align-items:start}
.vendors__side{display:grid;gap:16px}

.vd-list{max-height:none}
.vd-row{
  display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;
  padding:11px 15px;border-bottom:1px solid var(--line);
}
.vd-row:last-child{border-bottom:0}
.vd-row__name{font-size:13px;font-weight:600}
.vd-row__meta{font-family:var(--font-mono);font-size:11px;color:var(--muted);margin-top:2px}
.vd-row__meta b{color:var(--ink-2);font-weight:500}
.vd-row__actions{display:flex;gap:5px;opacity:0;transition:opacity .12s}
.vd-row:hover .vd-row__actions,.vd-row:focus-within .vd-row__actions{opacity:1}
.vd-form{padding:14px 15px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.vd-form .ch-fields{grid-template-columns:1fr 1fr}
.vd-form__actions{display:flex;gap:7px;margin-top:12px}
.vd-empty{padding:24px 15px;text-align:center;color:var(--muted);font-size:12.5px}
.vd-empty p{margin:0}
.vd-pill{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.06em;
  padding:2px 6px;border-radius:var(--r-sm);background:var(--signal-soft);color:var(--signal);
}

.hist__row{
  display:grid;grid-template-columns:auto 1fr auto;gap:12px;align-items:center;
  padding:10px 15px;border-bottom:1px solid var(--line);
}
.hist__row:last-child{border-bottom:0}
.hist__no{
  font-family:var(--font-mono);font-size:11.5px;font-weight:600;color:var(--accent-ink);
  background:var(--accent-soft);border-radius:var(--r-sm);padding:3px 7px;
}
.hist__who{font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hist__meta{font-family:var(--font-mono);font-size:11px;color:var(--muted);margin-top:1px}

.is-view{font-size:12.5px;line-height:1.6;color:var(--ink-2)}
.is-view b{display:block;font-size:13px;color:var(--ink);font-weight:600;margin-bottom:2px}
.is-view__bank{
  margin-top:11px;padding-top:11px;border-top:1px dashed var(--line);
  display:grid;grid-template-columns:auto 1fr;gap:2px 14px;
}

/* --- narrow screens ---------------------------------------------- */
@media (max-width:1000px){
  .ch-cols{grid-template-columns:1fr}
  .vendors__cols{grid-template-columns:1fr}
  .modal{padding:0}
  .modal__box{width:100%;height:100%;max-height:100%;border-radius:0;border:0}
  .modal__head{flex-wrap:wrap;gap:10px}
  .steps{order:3;width:100%}
  .steps li{flex:1;justify-content:center}
}
@media (max-width:700px){
  .ch-fields--2,.vd-form .ch-fields{grid-template-columns:1fr}
  .ch-c-img{display:none}
  .ch-table thead th.ch-c-img{display:none}
  .vendors{padding:22px 16px 44px}
}

/* ============================================================
   CHALLAN — format chooser
   A layer over the challan modal, not a second dialog stacked on it.
   The three options are the same shape as the export format cards in
   step 03, so the control is already familiar.
   ============================================================ */
.ch-format{
  position:absolute;inset:0;z-index:20;
  display:grid;place-items:center;
  background:rgba(20,23,26,.42);
  padding:20px;
}
.ch-format[hidden]{display:none}
.ch-format__card{
  width:min(430px,100%);
  background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-lg);
  box-shadow:var(--shadow-2);padding:20px;
}
.ch-format__card h3{
  font-family:var(--font-display);font-variation-settings:"wdth" 112;
  font-size:16px;font-weight:700;margin:0 0 4px;
}
.ch-format__card > p{margin:0 0 14px;font-size:12.5px;color:var(--muted);line-height:1.5}
.ch-format__opts{display:grid;gap:8px;margin-bottom:12px}
.ch-format__opt{
  display:block;width:100%;text-align:left;
  padding:11px 12px;border:1px solid var(--line-2);border-radius:var(--r-md);
  background:var(--surface-2);transition:border-color .12s,background .12s;
}
.ch-format__opt:hover{border-color:var(--accent);background:var(--accent-soft)}
.ch-format__opt:focus-visible{border-color:var(--accent)}
.ch-format__name{display:block;font-size:13px;font-weight:600;color:var(--ink)}
.ch-format__ext{
  font-family:var(--font-mono);font-size:10.5px;font-weight:500;
  color:var(--muted);margin-left:7px;
}
.ch-format__desc{display:block;font-size:11.5px;color:var(--muted);margin-top:2px;line-height:1.45}
#chFormatCancel{width:100%;justify-content:center}

/* Two formats per history row — a list being scanned should not stop to
   ask a question. */
.ch-hist__dl{display:inline-flex;gap:4px}
.ch-hist__dl .btn{height:26px;padding:0 8px;font-size:11px;font-family:var(--font-mono)}

@media (max-width:900px){
  .ch-format{padding:12px}
  .ch-format__opt{padding:13px 12px}
}
