/* ACOCOS /pos/ + /login/ — component styles. Every colour, spacing, and
   radius here is a var() from tokens.css; no literal hex/px sizing outside
   that file. See /POS-DESIGN.md for the reasoning behind each rule. */

*, *::before, *::after { box-sizing: border-box; }

/* The HTML `hidden` attribute must always win, even over a component that sets
   its own display. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  background: var(--page);
  color: var(--text);
}

body {
  font-family: var(--font);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.5;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
}

a { color: inherit; }

/* Money everywhere: fixed-width digits stop totals jittering as they update. */
.num, .tabular {
  font-variant-numeric: tabular-nums;
}

/* --- icons: inline sprite glyphs, colour via currentColor (shared across /pos/
   and the dashboard; the sprite is included once per page from _icons.svg) --- */
.ic { width: 1em; height: 1em; fill: currentColor; flex: none; }
.top__link { gap: 6px; }
.top__link .ic { width: 15px; height: 15px; }
.theme-toggle .ic { width: 18px; height: 18px; }
/* On narrow screens the header nav collapses to icon-only so it never overflows;
   the download bar keeps its labels (it wraps). aria-label carries the meaning. */
@media (max-width: 600px) {
  .top__actions .top__link .lbl,
  .dash-header__actions .top__link .lbl { display: none; }
  .top__actions .top__link,
  .dash-header__actions .top__link { padding: 0 var(--space-2); }
}

/* --- focus + motion + a11y floor --- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* /pos/ motion — subtle and fast. A client is waiting, so nothing here draws
   in, staggers, or counts up: just a press affordance and a quick fade as HTMX
   fragments (basket lines, autocomplete, the money bar) settle. Only transform
   and opacity animate — both are compositor-only, so they never trigger layout.
   The dashboard keeps its richer motion in dashboard.css. */
@media (prefers-reduced-motion: no-preference) {
  .btn, .tile, .top__link, .theme-toggle {
    transition: transform var(--dur-pos) var(--ease), background-color var(--dur-pos) var(--ease),
      border-color var(--dur-pos) var(--ease);
  }
  .btn:active, .tile:not(:disabled):active, .top__link:active, .theme-toggle:active { transform: scale(0.96); }
  .htmx-added { animation: pos-fade var(--dur-pos) var(--ease); }
}
@keyframes pos-fade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* 0.01ms, not `none`, so any animationend listener still fires exactly once. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- layout --- */
.wrap {
  max-width: 480px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-4) calc(var(--nav-height) + var(--safe-bottom) + var(--space-4));
}
/* Sale screen (mobile): extra bottom room so the last card isn't hidden behind
   the fixed money bar — but only while the bar is actually shown (cart has
   items). Empty cart keeps the normal padding, no dead gap. Desktop resets
   this below — the money bar is static in the rail there. */
.wrap:has(.money-bar:not(.money-bar--empty)) {
  padding-bottom: calc(var(--nav-height) + var(--safe-bottom) + 210px);
}

.top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border-bottom: 2px solid var(--rule);
  position: sticky;
  top: 0;
  z-index: 6;
}
/* The wordmark doubles as the way home (pos:index) — the convention every
   web app already trained people on. Inherits the header's colour rather
   than looking like a link: it should read as the brand first and reveal
   itself as tappable on hover/focus. */
.top__brand {
  font-weight: 800;
  font-size: 24px;
  letter-spacing: -0.03em;
  color: inherit;
  text-decoration: none;
}
@media (hover: hover) {
  .top__brand:hover { color: var(--accent); }
}
@media (min-width: 560px) {
  .top__brand { font-size: 30px; }
}

/* --- currency rate strip: today's NBKR/manual rates, in its own card so it
   reads with the same weight as the Клиент/Товары sections above/below it --- */
.rates-strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.rates-strip__item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}
.rates-strip__age {
  font-size: 11px;
  font-weight: 700;
  text-transform: none;
}
/* Курс card: the rates on the left, a big blue «pull fresh NBKR rate» button
   on the right. The button's icon spins only while its HTMX request is in
   flight (htmx adds .htmx-request), giving staff clear feedback. */
.rates-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}
.rates-refresh {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border: 0;
  border-radius: var(--radius-control);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
}
.rates-refresh .ic { width: 26px; height: 26px; }
@media (hover: hover) {
  .rates-refresh:hover { filter: brightness(1.08); }
}
.rates-refresh:active { transform: scale(0.96); }
@media (prefers-reduced-motion: no-preference) {
  .rates-refresh.htmx-request .ic { animation: rates-spin 0.7s linear infinite; }
}
/* Motion-free fallback: with «Reduce motion» on, the spin above never runs, so
   the button needs a state that isn't an animation — otherwise a slow refresh
   looks identical to a dead button. */
.rates-refresh.htmx-request { opacity: 0.6; cursor: progress; }
@keyframes rates-spin { to { transform: rotate(360deg); } }

/* Busy state for any .btn that fires an HTMX request (htmx sets .htmx-request
   on the triggering element for the life of the request). The label SWAPS
   rather than only spinning: an animation alone disappears under «Reduce
   motion» and tells a screen reader nothing, whereas «Обновляем курс…» reads
   correctly in both cases. Pair it with hx-disabled-elt="this" so the tap
   can't be repeated mid-flight. */
/* Two selectors, because htmx marks whichever element ISSUED the request: the
   button itself for hx-post on a <button>, but the <form> for a submit. */
.btn__busy { display: none; }
.btn.htmx-request .btn__idle,
.htmx-request .btn .btn__idle { display: none; }
.btn.htmx-request .btn__busy,
.htmx-request .btn .btn__busy { display: inline-flex; align-items: center; gap: var(--space-2); }
.btn.htmx-request { cursor: progress; }
/* No double-submit while a request is in flight — the form case can't use
   hx-disabled-elt without an extended selector, so it's enforced here. */
form.htmx-request .btn { pointer-events: none; cursor: progress; }
.btn:disabled { opacity: 0.75; pointer-events: none; }
.btn__spinner {
  flex: none;
  width: 15px;
  height: 15px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  opacity: 0.9;
}
@media (prefers-reduced-motion: no-preference) {
  .btn__spinner { animation: rates-spin 0.7s linear infinite; }
}
.top__actions { display: flex; align-items: center; gap: var(--space-2); }
.top__link {
  font-size: 13px;
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  display: inline-flex;
  align-items: center;
  /* A 1px --border-strong edge defines the button on BOTH the lighter --surface
     header bar (/pos/) and the darker page background (dashboard/storage), where
     the fill contrast alone would otherwise read as plain text on /pos/. */
  background: var(--surface-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-control);
}
@media (hover: hover) {
  .top__link:hover { border-color: var(--accent); background: var(--accent-bg); }
}

.theme-toggle {
  display: inline-flex;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  align-items: center;
  justify-content: center;
  background: var(--surface-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-control);
  color: var(--text-2);
  font-size: 16px;
  cursor: pointer;
}
@media (hover: hover) {
  .theme-toggle:hover { border-color: var(--accent); background: var(--accent-bg); }
}
/* Reflects the state actually rendered (explicit cookie OR the OS-driven
   fallback), not just the cookie — a pure-CSS match for tokens.css's
   prefers-color-scheme rule, so it's correct with zero JS and no flash. */
.theme-toggle__icon--sun { display: none; }
.theme-toggle__icon--moon { display: inline; }
html[data-theme="dark"] .theme-toggle__icon--sun { display: inline; }
html[data-theme="dark"] .theme-toggle__icon--moon { display: none; }
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .theme-toggle__icon--sun { display: inline; }
  html:not([data-theme]) .theme-toggle__icon--moon { display: none; }
}

.bottom-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* Content stays --nav-height tall; the safe-area gutter is extra padding
     below it (iOS home-indicator), so icons/labels never sit in that zone. */
  height: calc(var(--nav-height) + var(--safe-bottom));
  padding-bottom: var(--safe-bottom);
  display: flex;
  background: var(--surface);
  border-top: 0.5px solid var(--border);
  z-index: 5;
}
.bottom-nav a, .bottom-nav__logout {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: var(--text-3);
  text-decoration: none;
  font-size: 11px;
  min-height: var(--tap-min);
}
/* Logout is a POST form (Django's LogoutView is POST-only), styled to match
   the plain nav links exactly — no visible border/background of its own. */
.bottom-nav__logout-form { flex: 1; display: flex; margin: 0; }
.bottom-nav__logout {
  flex: 1;
  width: 100%;
  border: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
}
.bottom-nav a[aria-current="page"] { color: var(--accent); font-weight: 500; }
.bottom-nav .ic { width: 21px; height: 21px; }

/* --- notes / tasks board --- */
.note {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-3);
  margin-bottom: var(--space-2);
  box-shadow: var(--shadow);
}
.note--pinned { border-color: var(--accent); }
.note--done { opacity: 0.62; }
.note__toggle { flex: none; margin: 0; }
.note__check {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  border: 1.5px solid var(--border-strong);
  background: transparent;
  color: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.note__check .ic { width: 15px; height: 15px; }
.note--done .note__check { background: var(--paid); border-color: var(--paid); color: var(--on-accent); }
.note__body { flex: 1; min-width: 0; }
.note__title { font-weight: 500; font-size: 15px; }
.note--done .note__title { text-decoration: line-through; }
.note__text { font-size: 14px; color: var(--text-2); margin-top: 2px; white-space: pre-wrap; overflow-wrap: anywhere; }
.note__meta { font-size: 11px; color: var(--text-3); margin-top: var(--space-2); }
.note__actions { display: flex; flex-direction: column; gap: var(--space-1); flex: none; }
.note__act {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-control);
  border: 0;
  background: transparent;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  text-decoration: none;
}
.note__act .ic { width: 17px; height: 17px; }
.note__act.is-on { color: var(--accent); }
@media (hover: hover) {
  .note__act:hover { background: var(--surface-sunken); color: var(--text); }
}

/* shared muted list-count line + composed empty state (icon + friendly line) */
.list-count { font-size: 12px; color: var(--text-2); margin: var(--space-4) 0 var(--space-3); }
.empty {
  color: var(--text-3);
  font-size: 13px;
  text-align: center;
  padding: var(--space-6) var(--space-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
.empty .ic { width: 30px; height: 30px; opacity: 0.5; }
.empty p { margin: 0; }

/* --- clients: filter/sort pills + list --- */
/* Stack the two pill groups so they never force horizontal overflow on a phone;
   each group sizes to its content and wraps internally if truly needed. */
.filters { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); margin-top: var(--space-3); }
.filters__set { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0; }
.filters__label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-3); }
.filters__group { flex-wrap: wrap; max-width: 100%; }
@media (min-width: 560px) { .filters { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: var(--space-4); } }
.filters__group {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface-sunken);
  border-radius: 20px;
}
.fpill {
  border: 0;
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: 12px;
  padding: 8px 12px;
  min-height: var(--tap-min);
  border-radius: 16px;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}
.fpill.is-on { background: var(--accent); color: var(--on-accent); font-weight: 500; }
/* Only the variant-picker's size chips are real <button>s (every other
   .fpill is a filter <a>, which can't be disabled) — a size with no
   available stock in any color, same visual rule as .tile:disabled. */
.fpill:disabled { opacity: 0.45; cursor: not-allowed; }
.client-list { padding: 0; overflow: hidden; }
.client-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  color: var(--text);
  border-bottom: 0.5px solid var(--border);
}
.client-row:last-child { border-bottom: 0; }
/* When the row is a CSRF-protected submit button (orders client picker) rather
   than a link: strip the native button chrome so it reads identically. */
.client-row--btn {
  width: 100%;
  border: 0;
  border-bottom: 0.5px solid var(--border);
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
form:last-child .client-row--btn { border-bottom: 0; }
.client-row__main { min-width: 0; flex: 1; }
.client-row__debt { flex: none; }
.client-row__name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.client-row__phone { font-size: 13px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.client-row__debt {
  font-size: 13px;
  color: var(--debt);
  white-space: nowrap;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
@media (hover: hover) { .client-row:hover { background: var(--surface-sunken); } }

/* --- cards + sections --- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
  box-shadow: var(--shadow);
}
.card h2 {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin: 0 0 var(--space-3);
}
.card h2 .ic { width: 15px; height: 15px; color: var(--accent); }

.muted { color: var(--text-2); font-size: 13px; }
.hint { color: var(--text-3); font-size: 12px; }
.hint--ok { color: var(--paid); }
.hint--error { color: var(--debt); }
/* Payment conversion math — calm, neutral, always shown for a foreign
   payment: no alarm styling for the common case (risk-proportional friction,
   not a blanket warning). --risk (amber) appears only when this specific
   payment actually needs a second confirmation; --warn (red) only when
   there's no rate to convert with at all. */
.pay-note {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-2);
  background: var(--surface-sunken);
  border-radius: var(--radius-control);
  padding: var(--space-2) var(--space-3);
}
.pay-note--risk {
  color: var(--partial);
  background: var(--partial-bg);
  margin-top: var(--space-2);
}
.pay-note--risk ul { margin: var(--space-2) 0 0; padding-left: 1.2em; }
.pay-note--risk label {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.pay-note--risk input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: none;
  margin-top: 1px;
}
.pay-note--warn { color: var(--debt); background: var(--debt-bg); }

/* --- change (сдача) double-check panel: the full picture before handing
   money back, always shown together (never just the final figure alone) --- */
.change-panel {
  margin-top: var(--space-3);
  padding: var(--space-3);
  background: var(--surface-sunken);
  border-radius: var(--radius-control);
}
.change-panel__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 2px 0;
  font-size: 14px;
  gap: var(--space-2);
}
.change-panel__row--total {
  border-top: 2px solid var(--rule);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  font-size: 18px;
  font-weight: 800;
}

/* Owner-only manual rate entry — tucked in a <details> so it never adds a tap
   for Editor/Manager (who don't even get the toggle rendered, see
   can_override_rate) and stays visually secondary for the Owner too. */
.rate-override { margin-top: var(--space-3); }
.rate-override summary {
  font-size: 12px;
  font-weight: 700;
  color: var(--text-2);
  cursor: pointer;
}
.rate-override input { margin-top: var(--space-2); }

.error {
  color: var(--debt);
  background: var(--debt-bg);
  border-radius: var(--radius-control);
  padding: var(--space-3);
  font-size: 13px;
  margin: 0 0 var(--space-3);
}

/* Flash messages carry their level's colour — a success banner rendered in
   the error palette reads as a failure, which is worse than no banner. */
.flash {
  border-radius: var(--radius-control);
  padding: var(--space-3);
  font-size: 13px;
  margin: 0 0 var(--space-3);
}
.flash--ok { color: var(--paid); background: var(--paid-bg); }
.flash--warn { color: var(--partial); background: var(--partial-bg); }
.flash--error { color: var(--debt); background: var(--debt-bg); }

/* Creation step rail — three short words, current one filled. Not a
   decorative eyebrow: the order genuinely is a 3-step run here. */
.steps { display: flex; gap: var(--space-2); list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.steps__item {
  flex: 1;
  min-width: 0;
  padding: 6px var(--space-2);
  border-radius: var(--radius-control);
  background: var(--surface-sunken);
  color: var(--text-3);
  font-size: 12px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.steps__item.is-on { background: var(--accent); color: var(--on-accent); font-weight: 500; }
.step-eyebrow {
  margin: 0 0 var(--space-1);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-3);
}

/* --- forms --- */
label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  margin: var(--space-3) 0 var(--space-1);
}
label:first-child { margin-top: 0; }

input, select, textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: 10px var(--space-3);
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-weight: 600;
  font-size: 16px; /* prevents iOS auto-zoom on focus */
}
/* Search fields carry the heavier --rule border (matches the client-chip/
   header treatment) — they're the primary way in on the sale + clients
   screens, so they read as the loudest field on the card. */
input[type="search"] { border-color: var(--rule); font-weight: 500; }
input::placeholder { color: var(--text-3); font-weight: 400; }
input:focus, select:focus, textarea:focus {
  border-color: var(--accent);
}

.row { display: flex; gap: var(--space-2); }
.row > * { flex: 1; min-width: 0; }

/* --- buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: 10px var(--space-4);
  border-radius: var(--radius-control);
  border: 0.5px solid transparent;
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  width: 100%;
}
.btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
@media (hover: hover) {
  .btn-primary:not(:disabled):hover { transform: translateY(-2px); }
}
.btn-ghost {
  background: transparent;
  color: var(--text);
  border: 2px solid var(--rule);
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
@media (hover: hover) {
  .btn-ghost:not(:disabled):hover { background: var(--rule); color: var(--surface); }
}
.btn-ghost--debt { color: var(--debt); border-color: var(--debt); }
@media (hover: hover) {
  .btn-ghost--debt:not(:disabled):hover { background: var(--debt); color: var(--on-accent); }
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-sm {
  width: auto;
  min-height: 34px;
  padding: 4px 12px;
  font-size: 12px;
}
.btn-icon {
  width: var(--tap-min);
  min-width: var(--tap-min);
  padding: 0;
  flex: none;
}

/* --- product grid + tile --- */
.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}
.tile {
  position: relative;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-2);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  overflow: hidden;
}
@media (hover: hover) {
  .tile:not(:disabled):hover { border-color: var(--accent); transform: translateY(-2px); }
}
.tile:disabled { opacity: 0.45; cursor: not-allowed; }
.tile__photo {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: calc(var(--radius-card) - 4px);
  background: var(--surface-sunken);
  display: block;
  margin-bottom: var(--space-2);
}
.tile__photo--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-3);
  background: linear-gradient(160deg, var(--accent-2), var(--surface-sunken));
}
.tile__cat { font-size: 11px; color: var(--text-3); margin-bottom: 1px; }
.tile__name { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 2px; }
/* A product tile's .tile__photo pushes the badge (top:0, right:0 of the
   WHOLE tile) safely above the text below it. The variant picker's color
   tiles have no photo, so without this the badge sits directly on top of
   the color name — reserve the same clearance a photo would have given. */
.tile--swatch { padding-top: 26px; }
.tile__price { font-size: 13px; color: var(--text); font-weight: 600; }
.tile__reserved { font-size: 11px; color: var(--text-3); margin-top: 2px; font-variant-numeric: tabular-nums; }
.tile__badge {
  position: absolute;
  top: 0;
  right: 0;
  border-radius: 0;
  padding: 3px 9px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* --- variant picker: a centered modal OVER the products, not a card below the
   grid. Tapping a tile opens the size/qty dialog right where the eye is — no
   scrolling to find it. Shown purely by CSS when #variant-picker has content
   (:not(:empty)); ОТМЕНА / Escape / adding an item clear it to empty, which
   hides it again. No inline script needed (CSP-safe). --- */
/* Same CSS-only overlay mechanism is shared by the variant picker, the
   Owner's manual-rate dialog (#rate-modal) and the order handover summary
   (#order-modal) — each shows when its div has content and hides when it's
   emptied (data-clear-target / oob swap). */
#variant-picker:not(:empty),
#rate-modal:not(:empty),
#order-modal:not(:empty) {
  position: fixed;
  inset: 0;
  z-index: 20; /* above header (6), nav (5), money bar (4) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: rgba(0, 0, 0, 0.5); /* backdrop scrim */
}
#variant-picker > .card,
#rate-modal > .card,
#order-modal > .card {
  width: 100%;
  max-width: 420px;
  margin: 0;
  max-height: calc(100dvh - 2 * var(--space-6));
  overflow-y: auto;
  animation: picker-in var(--dur-pos) var(--ease);
}
@keyframes picker-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
/* Freeze the page behind the open dialog so only the dialog scrolls. */
body:has(#variant-picker:not(:empty)),
body:has(#rate-modal:not(:empty)),
body:has(#order-modal:not(:empty)) { overflow: hidden; }

/* --- status badges: colour + word, never colour alone. Solid fills (not soft
   tints) at weight 800 — declarative, high-contrast; --on-accent flips per
   theme so the fill always clears AA (see tokens.css). --accent (plum) never
   appears here: paid/partial/debt/neutral are the only fills a badge takes. */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 800;
  white-space: nowrap;
}
.badge--paid { background: var(--paid); color: var(--on-accent); }
.badge--partial { background: var(--partial); color: var(--on-accent); }
.badge--debt { background: var(--debt); color: var(--on-accent); }
.badge--neutral { background: var(--text-3); color: var(--on-accent); }

/* --- client running balance (client_detail.html) — leads the page, large
   and status-coloured, so "how much does she owe right now" is the first
   thing read, before any per-sale detail. Reuses the SAME --debt/--paid
   tokens the badge/oline classes already use, just at statement scale. */
.balance-figure {
  font-size: 32px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  margin: 0;
}
.balance-figure--debt { color: var(--debt); }
.balance-figure--paid { color: var(--paid); }
.balance-figure--credit { color: var(--paid); }
.balance-label { color: var(--text-3); font-size: 13px; margin: 0 0 2px; }

/* --- client statement date-range filter (client_detail.html) --- */
.statement-filter { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
.statement-filter label { display: flex; flex-direction: column; gap: 2px; font-size: 12px; }

/* --- client chip --- */
.client-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  background: var(--surface);
  border: 2px solid var(--rule);
  border-radius: var(--radius-card);
  padding: var(--space-3);
}
.client-chip__name { font-weight: 500; }
.client-chip__phone { color: var(--text-2); font-size: 13px; }
.client-chip__debt { text-align: right; font-size: 13px; white-space: nowrap; }

/* --- compact item table (e.g. the repayment window's "what was bought"
   list) — a real <table> because it genuinely has independent columns
   (товар/размер/цвет/кол-во/сумма), unlike .line-row's label+total shape. */
/* Wraps a wide .table (5+ columns of real content — a description plus
   several money columns) so IT scrolls sideways in its own bounded strip
   on a narrow phone, never the page body — the rest of the card (a balance
   line above, an action button below) stays fixed in place either way. */
.table-scroll { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th, .table td { padding: var(--space-1) var(--space-2); text-align: left; }
.table th:first-child, .table td:first-child { padding-left: 0; }
.table th:last-child, .table td:last-child { padding-right: 0; }
.table th { color: var(--text-3); font-size: 11px; font-weight: 700; text-transform: uppercase; }
.table tbody tr { border-top: 0.5px solid var(--border); }
.table td.num, .table th.num { text-align: right; white-space: nowrap; }

/* Batch production grid (manufacturing/partials/production_grid.html): one
   size x color cell holds an accepted + a defect qty input side by side,
   narrow enough that a whole matrix of colors still fits without the table
   itself needing to scroll on top of the page's own overflow-x wrapper. */
.qty-cell { display: flex; align-items: center; gap: 3px; }
.qty-cell input {
  width: 44px;
  padding: var(--space-1);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.qty-cell input[data-role="defect"] { color: var(--debt); }
.qty-cell__empty { color: var(--text-3); }
/* A product with many colours makes the grid wider than a phone (9 colours =
   ~1000px), so .table-scroll gives it its own horizontal scroll. Pin the size
   column while that happens — otherwise scrolling right to reach «бежевый»
   scrolls the size away too, and there's nothing on screen saying which row
   the numbers being typed belong to. Opaque background so the scrolling cells
   pass UNDER it rather than showing through. */
#production-grid-card .table th:first-child,
#production-grid-card .table td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  padding-right: var(--space-2);
}

/* --- line rows --- */
.line-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 0.5px solid var(--border);
}
.line-row:last-child { border-bottom: none; }
.line-row__label { flex: 1; min-width: 0; }
.line-row__label strong { font-weight: 500; }
.line-row__total { text-align: right; white-space: nowrap; }

/* Two lines per cart item: the full variant name (never truncated — a
   clipped "Red Dr..." vs "Red Dres..." is a real mis-sale risk), then
   qty×price + line total right-aligned below it. */
.line-row--compact {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
}
.line-row__cat {
  font-size: 11px;
  color: var(--text-3);
}
.line-row__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
}
.line-row__name {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  font-weight: 500;
  font-size: 14px;
  white-space: normal;
  overflow-wrap: break-word;
  /* A safety cap, not a truncation — full names read fine at 2 lines; an
     exceptional third line clips rather than pushing the row open forever. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The money column. qty × price sits hard left, the figure hard right, so
   every line's total lands on the same edge and the cart reads as a column of
   numbers rather than text scattered mid-row. Tabular figures keep the digits
   in vertical register between rows. */
.line-row__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-1); /* separates the money from the name/attrs above */
}
.line-row__attrs {
  font-size: 12px;
  color: var(--text-3);
  margin-top: 1px;
}
.line-row__qty {
  flex: none;
  white-space: nowrap;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.line-row--compact .line-row__total {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
/* Discounted line: the sticker price is struck through on the row above and
   the charged figure carries the weight below it, so «what it cost» and «what
   came off» stay two readable facts instead of one already-netted number. */
.line-row__total--struck {
  text-decoration: line-through;
  color: var(--text-3);
  font-weight: 400;
}
.line-row__meta--net { margin-top: 1px; }
.line-row__disc {
  flex: none;
  font-size: 12px;
  font-weight: 600;
  color: var(--paid);
  white-space: nowrap;
}
/* Per-line discount toggle — same quiet-<details> treatment as .rate-override,
   so an undiscounted line (the common case) stays one compact row. */
.line-row__discount { margin-top: var(--space-2); }
.line-row__discount summary {
  display: inline-block; /* the marker + label hug the left edge, never centred */
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}
.line-row__discount form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.line-row__discount select { flex: none; width: auto; }
.line-row__discount input { flex: 1; min-width: 0; }
/* Loud bordered "btn" styling is wrong for a per-row delete — keep the 44px
   touch target, but read as a quiet icon, not the row's dominant element.
   Negative margin keeps the visual row compact while the hit area stays full. */
.line-row__remove {
  flex: none;
  width: var(--tap-min);
  height: var(--tap-min);
  margin: calc(-1 * (var(--tap-min) - 20px) / 2) calc(-1 * (var(--tap-min) - 20px) / 2) calc(-1 * (var(--tap-min) - 20px) / 2) 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-3);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (hover: hover) {
  .line-row__remove:hover { color: var(--debt); background: var(--surface-sunken); }
}

/* Item count in the card header — "ПОЗИЦИИ · 4". The cart rail is the densest
   thing on the screen and the header is a 12px uppercase label, not a title —
   the card's default 16px top padding left it floating in a band of empty
   space. Tightened at the top only; the sides and bottom keep the normal card
   padding so the rows themselves don't feel cramped against the border. */
.items-card { padding-top: var(--space-3); }
.items-card h2 { margin-bottom: var(--space-2); }
.items-scroll { position: relative; }

table { width: 100%; border-collapse: collapse; font-size: 14px; }
td, th { padding: var(--space-2) var(--space-1); border-bottom: 0.5px solid var(--border); text-align: left; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* --- money bar: the signature element, always visible above the keyboard --- */
/* Mobile: the money bar is FIXED above the bottom nav, so the running total +
   confirm are visible from anywhere — no scrolling past the product grid to
   reach the cart. (Desktop makes it static inside the right rail, below.) */
.money-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--nav-height) + var(--safe-bottom));
  max-width: 480px; /* match .wrap's content width */
  margin-inline: auto;
  background: var(--surface);
  border-top: 2px solid var(--rule);
  border-radius: var(--radius-card) var(--radius-card) 0 0;
  box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.14);
  padding: var(--space-3) var(--space-4);
  z-index: 4;
}
/* Nothing in the cart yet — don't reserve the fixed bar; give products the room. */
.money-bar--empty { display: none; }
.money-bar__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 2px 0;
  font-size: 14px;
}
.money-bar__row--total {
  border-top: 2px solid var(--rule);
  margin-top: var(--space-1);
  padding-top: var(--space-2);
}
.money-bar__row--total .money-bar__label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.money-bar__row--total .money-bar__amount {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.02em;
}
.money-bar__row--paid .money-bar__label,
.money-bar__row--paid .money-bar__amount { color: var(--paid); }
.money-bar__row--partial .money-bar__label,
.money-bar__row--partial .money-bar__amount { color: var(--partial); }
.money-bar__row--debt .money-bar__label,
.money-bar__row--debt .money-bar__amount { color: var(--debt); }
.money-bar__amount { font-variant-numeric: tabular-nums; }
.money-bar__conv { font-size: 12px; color: var(--text-3); display: block; }
.money-bar .btn { margin-top: var(--space-3); }

/* Заказы order totals — the SAME row styling as the sale screen's money bar,
   but a normal in-flow card, not the fixed floating bar: an order-detail
   page has no "stay visible while scrolling a long product grid" need, so
   position:fixed here only ever floated the totals over the Аванс form
   below it. */
.money-summary .btn { margin-top: var(--space-3); }

/* --- Заказы: production queue + order flow ---
   The queue answers three questions per row: what to make, how many, by when,
   and for whom — so a row is a header (name + the one number that matters),
   a meta line (the arithmetic behind it), and a breakdown list. Grouping by
   product adds a second breakdown (the per-size split) above the client one. */
/* The way back out of a detail page. Sits above the first card so it's the
   first thing under the header — an order screen ends in «Выдать заказ»,
   which is final, so leaving must never require going near that button. */
.back-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-bottom: var(--space-2);
  font-size: 14px;
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
}
@media (hover: hover) {
  .back-link:hover { text-decoration: underline; }
}

/* Order builder: a fixed identity header, then everything mutable below it in
   #order-body. The header is the only part that never swaps, so the client
   and order number stay put while lines/totals update underneath. */
.order-head__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.order-head__row h2 { margin: 0; }
.order-head__client {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: var(--space-2);
  text-decoration: none;
  color: inherit;
}
.order-head__client .muted { font-size: 13px; }
@media (hover: hover) {
  .order-head__client:hover strong { text-decoration: underline; }
}

/* One order line = name + qty×price on the left, line total right, remove at
   the end; the «произвести» control sits on its own row beneath so the line
   itself stays readable at a glance while scanning a long order. */
.oline {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 0.5px solid var(--border);
}
.oline__main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.oline__name { font-weight: 500; }
.oline__meta { font-size: 12px; }
.oline__total { flex: none; white-space: nowrap; }
.oline__remove { flex: none; }
/* .btn defaults to width:100% (right for a stacked full-width action), which
   flex-basis:auto would otherwise resolve AS that 100% inside a flex row —
   reset it explicitly so a per-row button (e.g. «Погасить») stays compact. */
.oline__action { flex: none; width: auto; }
.oline__produce {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 0 var(--space-3);
  border-bottom: 0.5px solid var(--border);
}
.oline__produce input { width: 88px; margin: 0; }
.oline__produce .btn { flex: none; }

/* Должники: a row's «Выписка»/«Напомнить»/«Не напоминать» sit on their own
   line beneath the name/debt (same reasoning as .oline__produce above) —
   up to 3 buttons never fit next to the debt figure on a narrow phone, so
   they wrap onto their own flow instead of overflowing the row. */
.debtor-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: 0 0 var(--space-3);
  border-bottom: 0.5px solid var(--border);
}
.debtor-row__actions form { margin: 0; }
.debtor-row__actions .btn { flex: none; width: auto; margin: 0; }

/* Actions stack in their own card so «Выдать заказ» is never adjacent to an
   edit control that could be mis-tapped for it. */
.order-actions,
.order-finish { display: flex; flex-direction: column; gap: var(--space-2); }
.order-actions form,
.order-finish form { margin: 0; }
.order-actions .btn,
.order-finish .btn { margin: 0; }
.order-actions .hint,
.order-finish .hint { margin: 0; }
/* The end of the order, visually separated from every edit control above it. */
.order-finish { border-color: var(--border-strong); }
.order-finish h2 { margin-bottom: var(--space-1); }

/* Production progress: how much of a line has actually reached the warehouse.
   Bar + number together — the bar is scannable down a long order, the number
   is the one that's true. Never colour alone (matches the badge rule). */
.oprogress { display: flex; align-items: center; gap: var(--space-2); margin-top: 2px; }
.oprogress__bar {
  flex: 1;
  min-width: 0;
  height: 6px;
  border-radius: 3px;
  background: var(--surface-sunken);
  overflow: hidden;
}
.oprogress__bar > span {
  display: block;
  height: 100%;
  background: var(--paid);
  border-radius: 3px;
}
.oprogress__label { flex: none; font-size: 12px; }
.oprogress--order { margin: 0 0 var(--space-3); }

.oline__stock { padding: 0 0 var(--space-2); }
.oline__stockline { margin: 4px 0 0; font-size: 12px; }
.oline__ok { color: var(--paid); }

.order-flow {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-control);
  background: var(--surface-sunken);
  font-size: 13px;
  color: var(--text-2);
}
.order-flow a { margin-left: var(--space-2); white-space: nowrap; }

.queue-summary {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-control);
  background: var(--surface-sunken);
}
.queue-summary__cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.queue-summary__num { font-size: 20px; font-weight: 600; line-height: 1.1; }
.queue-summary__num.is-overdue { color: var(--debt); }
.queue-summary__label {
  font-size: 11px;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* A covered row still shows (she must not sell reserved stock to a walk-in),
   just de-emphasised; an overdue one gets a left rule so it reads at a glance
   while scanning a long queue. */
.q-row--covered { opacity: 0.62; }
.q-row--overdue { border-color: var(--debt); }
.q-row__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}
.q-row__title { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.q-row__title .muted { font-size: 12px; }
.q-row__badge { flex: none; }
.q-row__meta {
  margin-top: var(--space-2);
  font-size: 12px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.q-due--overdue { color: var(--debt); font-weight: 500; }

.q-sub { margin-top: var(--space-3); }
.q-sub__label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
  margin-bottom: 4px;
}
.q-sub__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 7px 0;
  border-bottom: 0.5px solid var(--border);
  font-size: 13px;
}
.q-sub__row:last-child { border-bottom: 0; }
.q-sub__row > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.q-sub__row .muted { font-size: 12px; }
.q-sub__row--link { color: inherit; text-decoration: none; }
@media (hover: hover) {
  .q-sub__row--link:hover { background: var(--surface-sunken); }
}

/* --- login: split layout, the one place with room for character ---
   The whole page renders with the database down: the photo is a static file,
   nothing here queries. On mobile the photo collapses to a short header band. */
.login-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 140px 1fr;
}
.login-photo {
  background: var(--accent-bg) url("login.543ef5b7271d.jpg") center / cover no-repeat;
}
.login-form-col {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-4);
}
.login-card {
  width: 100%;
  max-width: 360px;
}
.login-wordmark {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.03em;
  margin: 0 0 var(--space-1);
}
.login-sub { color: var(--text-2); font-size: 13px; margin: 0 0 var(--space-5); }
.login-field { margin-bottom: var(--space-3); }
.login-field label {
  display: block;
  font-size: 12px;
  color: var(--text-2);
  margin-bottom: 4px;
}
.login-card input[type="text"],
.login-card input[type="password"],
.login-card input[type="number"] {
  width: 100%;
}
.login-otp input { letter-spacing: 0.35em; font-variant-numeric: tabular-nums; }
.login-error,
.login-notice {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-control);
  font-size: 13px;
}
.login-error { background: var(--debt-bg); color: var(--debt); }
.login-notice { background: var(--paid-bg); color: var(--paid); }

@media (min-width: 768px) {
  .login-page {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr;
  }
  .login-photo { grid-row: auto; }
}

/* --- error pages (404/403/400/csrf/lockout): a single centred card --- */
.error-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}
.error-card {
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-6) var(--space-5);
  text-align: center;
}
.error-icon { color: var(--text-3); margin: 0 auto var(--space-4); display: block; }
.error-message { font-size: 19px; font-weight: 500; color: var(--text); margin: 0 0 var(--space-2); }
.error-card .btn { margin-top: var(--space-5); }

/* ===================================================================
   Desktop (>=768px). No separate codebase, no device detection: the same
   markup, re-placed with CSS. The cart (client + items + payment + money
   bar, i.e. #sale-body) becomes a sticky right rail; products fill the
   left. Scoped to the sale screen with :has(#sale-body) so the read-only
   pages (Сегодня/Клиенты) stay a single comfortable column.
   =================================================================== */
@media (min-width: 768px) {
  /* Read-only pages: a comfortable centred reading column. */
  .wrap {
    max-width: 720px;
    padding-bottom: calc(var(--nav-height) + var(--space-6));
  }

  /* Product grid gets the width it deserves: 4 columns, hover affordance. */
  .grid {
    grid-template-columns: repeat(4, 1fr);
  }
  @media (hover: hover) {
    .tile {
      transition: transform var(--dur-pos) var(--ease), background-color var(--dur-pos) var(--ease);
    }
    .tile:not(:disabled):hover {
      background: var(--surface-sunken);
    }
  }

  /* Sale screen: two columns — products left, cart rail right. */
  .wrap:has(#sale-body) {
    max-width: 1200px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--space-4);
    align-items: start;
  }
  .wrap:has(#sale-body) > * {
    grid-column: 1;
    min-width: 0;
  }
  /* The rail itself: a flex column capped to the viewport height so it can
     NEVER grow taller than the screen. It does NOT scroll itself (overflow:
     hidden) — its single scrolling child is .cart-scroll below. Sticky top
     is the requested 16px of breathing room PAST the sticky header (76px);
     max-height clears the header above and the fixed bottom-nav (+ iOS safe
     area) below, each with the same 16px gap, so the rail can never render
     under either fixed bar. */
  .wrap:has(#sale-body) > #sale-body {
    grid-column: 2;
    grid-row: 1 / 100; /* span the whole left stack */
    position: sticky;
    top: calc(76px + 16px);
    display: flex;
    flex-direction: column;
    max-height: calc(
      100dvh - 76px - 16px - var(--nav-height) - var(--safe-bottom) - 16px
    );
    overflow: hidden;
  }
  /* ONE scroll region for the whole column — ПОЗИЦИИ and ОПЛАТА scroll
     together, no nested scrollbar inside the items list. Итого/Остаток/
     Подтвердить (.money-bar) live OUTSIDE this wrapper, pinned below it. */
  .wrap:has(#sale-body) .cart-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-gutter: stable;
    /* Bottom fade ONLY. The matching top fade was removed: at rest — which is
       how the rail is 95% of the time, with a cart of one or two lines — it
       had nothing to fade except the ПОЗИЦИИ card's own top edge, ghosting the
       border and header into the background and reading as dead space above
       the list. "There is more below" is the affordance worth having; "there
       is more above" is not, when there usually isn't. */
    -webkit-mask-image: linear-gradient(
      to bottom,
      black calc(100% - 12px),
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      black calc(100% - 12px),
      transparent 100%
    );
  }
  .wrap:has(#sale-body) .items-card {
    margin-bottom: var(--space-3);
  }
  .wrap:has(#sale-body) .money-bar {
    flex: 0 0 auto;
  }
  /* Desktop: the sale screen has room to reset the wider mobile bottom padding. */
  .wrap:has(#sale-body) {
    padding-bottom: calc(var(--nav-height) + var(--safe-bottom) + var(--space-6));
  }
  /* Inside the rail the money bar is no longer a floating bottom bar — reset
     every fixed-position property back to a normal in-flow block. */
  .wrap:has(#sale-body) .money-bar {
    position: static;
    max-width: none;
    margin: var(--space-3) 0 0;
    border-radius: var(--radius-card);
    border-top: 0.5px solid var(--border);
    box-shadow: var(--shadow);
  }
}

@media (min-width: 1280px) {
  .grid {
    grid-template-columns: repeat(5, 1fr);
  }
}

/* --- Confirmation dialog (static/pos/js/safety.js) ---------------------- */
/* Native <dialog>: focus trap, ESC-to-dismiss and the backdrop come free, and
   ::backdrop can't be styled into existence any other way. Destructive verb on
   the right in --debt, the safe way out on the left and focused by default. */
.confirm-dialog {
  max-width: 340px;
  width: calc(100vw - 2 * var(--space-4));
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 18px 48px rgb(0 0 0 / 0.35);
}
.confirm-dialog::backdrop { background: rgb(0 0 0 / 0.55); }
.confirm-dialog__text {
  margin: 0 0 var(--space-4);
  font-size: 15px;
  line-height: 1.45;
}
.confirm-dialog__actions { display: flex; gap: var(--space-2); }
.confirm-dialog__actions .btn { flex: 1 1 0; }

/* --- cart undo/redo toolbar ------------------------------------------- */
.items-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.items-card__head h2 { margin-bottom: 0; }

/* Two separate square controls, each with its own outline — the same shape
   language as every other small control in /pos/ (--radius-control, a 1px
   --border, accent on hover). The chevrons come from the shared Phosphor
   sprite so they match the rest of the icon set. */
.undo-bar { display: flex; gap: var(--space-1); flex: none; }
.undo-bar__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-2);
  line-height: 1;
  cursor: pointer;
}
.undo-bar__btn .ic { width: 16px; height: 16px; }
@media (hover: hover) {
  .undo-bar__btn:not(:disabled):hover { color: var(--accent); border-color: var(--accent); }
}
/* Dimmed, not hidden: "there is nothing to undo yet" is information, and a
   control that vanishes and reappears makes the card header jump. */
.undo-bar__btn:disabled { opacity: 0.35; cursor: default; }
