/* ==========================================================================
   Ravon Express — design system
   --------------------------------------------------------------------------
   A single hand-written stylesheet: no framework, no build step, no CDN.
   Organised as: tokens → reset → primitives → layout → components → utilities.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Brand — dark blue.
     `--brand` is the resting colour, 600 the hover and 700 the pressed state;
     they are the same hue walked down in lightness, so a button darkens rather
     than shifting colour under the cursor. `--brand-soft` / `--brand-border`
     are the pale pair behind a selected row or an active nav item.

     Deliberately darker than `--info` (#175cd3): the two blues sit next to each
     other on an info banner inside a branded panel, and "ours" has to read as
     the heavier of the pair. Semantic red is untouched — `--danger` still means
     something went wrong. */
  --brand:            #14418a;
  --brand-600:        #0f3470;
  --brand-700:        #0b2856;
  --brand-soft:       #eff4fb;
  --brand-border:     #c2d5ef;

  /* Neutrals */
  --bg:               #f6f7f9;
  --surface:          #ffffff;
  --surface-2:        #fbfcfd;
  /* Table headings. Their own token rather than --surface-2, which at #fbfcfd
     is all but white and left a heading row indistinguishable from the data
     under it. Dark enough to read as a rule across the top of the table,
     light enough that the uppercase grey label on it stays comfortable. */
  --thead-bg:         #eceff4;
  /* The disclosed filter panel. Deliberately a step darker than the table
     heading above it: it is a block that appears and disappears, and it has to
     read as a distinct surface rather than as another header row. Its labels
     are forced to full-strength ink — on this ground the muted grey they carry
     elsewhere goes soft, and a label nobody can read is a filter nobody uses. */
  --filters-bg:       #e4e9f0;
  --filters-ink:      #0b111f;
  --surface-3:        #f2f4f7;
  --overlay:          rgba(16, 20, 28, .45);

  /* The text ramp, darkened one step across the board. Body copy was already
     near-black; what made the interface read pale was the three greys under it,
     which carry most of what is actually on screen — field labels, hints, table
     meta, card subtitles, the nav. Every one of them now clears 7:1 on
     --surface (WCAG AAA for body text) rather than the 4.5-ish the lighter two
     used to sit at, which is a real difference on a branch's monitor in
     daylight. --text-4 is the lightest of the four and is no longer
     near-invisible. Two things that used to share these greys now have steps of
     their own below them, because neither is content: `--text-hint` for prose
     that explains something, and `--placeholder` for a prompt in an empty
     field. */
  --text:             #0b111f;
  --text-2:           #2f3b4d;
  --text-3:           #495567;
  --text-4:           #6b7688;
  /* The placeholder has its own step, one lighter than the ramp's last.
     `--text-4` used to do both jobs, and darkening the ramp for legibility —
     the note above — took the placeholder with it: `+998 90 123 45 67` sitting
     in an empty box came out the same weight as a number somebody had actually
     typed, so a form of empty fields read as a form already filled in. A
     placeholder is a prompt, not a value, and has to look like one. It is only
     ever a repeat of the label or an example, so nothing is lost by its being
     faint — everything it says is said again by something that is not. */
  --placeholder:      #aab2bf;
  /* Explanatory prose — a card's subtitle, the line under a heading that says
     what the panel is for, «Коробки и пакеты, проданные клиенту отдельно от
     мест». It is read once and then becomes furniture, which is the argument
     for it being lighter than the content it explains and heavier than a
     placeholder, which is not text at all. Its own step rather than reusing
     `--text-3`: that grey also carries field labels and the nav, which have to
     stay readable at a glance on a branch monitor. */
  --text-hint:        #767f8d;
  /* One colour per operational area, on the top bar — see `.topbar--*`.
     Not one shared tint: an operator moves between four of these all day and
     the bar has to say *which* of them they are in, not merely that they are
     in one of them. Each is its own hue mixed ~13% into the surface — a real
     step off white, because a marker nobody notices marks nothing, and no
     stronger, because the title, the search box and the avatar sit on it. */
  --topbar-shipments: #e0e6f0;   /* blue   — накладные */
  --topbar-pickups:   #e0eee7;   /* green  — вызовы курьера */
  --topbar-returns:   #f5e7df;   /* amber  — возвраты */
  --topbar-scan:      #ece6f3;   /* violet — сканирование */
  --text-invert:      #ffffff;

  --border:           #e4e7ec;
  --border-2:         #d0d5dd;
  --border-strong:    #98a2b3;

  /* Sender / consignee. Every screen that shows the two parties tints them the
     same way, so a waybill reads the same on the booking form, in a list and
     on the detail page: who it came from on the left, who it goes to on the
     right. Kept pale enough that ordinary body text stays readable on it. */
  /* The requester panel, tinted by customer type. Pale enough that black
     labels and grey hints stay legible on it — this marks a panel, it does not
     highlight it. «Частное лицо» is deliberately untinted: it is the ordinary
     case and colouring the majority would leave nothing standing out. */
  --ctype-eshop:          #fdf3c6;
  --ctype-eshop-border:   #e8d182;
  --ctype-contract:       #eadbc8;
  --ctype-contract-border:#c4a582;
  /* Sender blue and consignee green, taken 5% darker: at their old values
     they were a hair off white, and on a printed or projected screen the two
     ends of a waybill were telling apart by nothing at all. Darkened in HSL
     so the hue and saturation are untouched and only the light comes out. */
  --party-from:        #d7edff;
  --party-from-border: #b6d9fa;
  --party-to:          #dafae9;
  --party-to-border:   #b3e7ca;

  /* Semantic */
  --success:          #067647;
  --success-soft:     #ecfdf3;
  --success-border:   #abefc6;
  --warning:          #b54708;
  --warning-soft:     #fffaeb;
  --warning-border:   #fedf89;
  --danger:           #b42318;
  --danger-soft:      #fef3f2;
  --danger-border:    #fecdca;
  --info:             #175cd3;
  --info-soft:        #eff8ff;
  --info-border:      #b2ddff;
  --neutral-soft:     #f2f4f7;

  /* Charts. The first series is the house colour, so a single-series chart is
     branded without being told to be. --c2 moves to teal: it used to be the
     blue that now sits at --c1, and two adjacent blues in one legend are two
     series nobody can tell apart. */
  --c1: #14418a; --c2: #0e9384; --c3: #067647; --c4: #b54708;
  --c5: #6941c6; --c6: #175cd3; --c7: #c11574; --c8: #3538cd;

  /* Elevation */
  --shadow-xs: 0 1px 2px rgba(16,24,40,.05);
  --shadow-sm: 0 1px 3px rgba(16,24,40,.10), 0 1px 2px rgba(16,24,40,.06);
  --shadow-md: 0 4px 8px -2px rgba(16,24,40,.10), 0 2px 4px -2px rgba(16,24,40,.06);
  --shadow-lg: 0 12px 16px -4px rgba(16,24,40,.08), 0 4px 6px -2px rgba(16,24,40,.03);
  --shadow-xl: 0 20px 24px -4px rgba(16,24,40,.08), 0 8px 8px -4px rgba(16,24,40,.03);
  --ring: 0 0 0 3px rgba(20, 65, 138, .16);

  /* A tooltip is drawn *over* the page, so it inverts rather than tinting:
     dark on light here, light on dark below. Not `--surface` in either theme —
     a bubble the same colour as the panel under it has to be separated by a
     shadow alone, and at 12px that is not enough. */
  --tooltip-bg:  #1f2937;
  --tooltip-ink: #f3f4f6;

  /* Shape */
  --r-xs: 4px; --r-sm: 6px; --r: 8px; --r-md: 10px; --r-lg: 12px; --r-xl: 16px; --r-full: 999px;

  /* Type */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Layout */
  --sidebar-w: 252px;
  --sidebar-w-collapsed: 68px;
  --topbar-h: 60px;

  --t-fast: 120ms cubic-bezier(.4,0,.2,1);
  --t: 200ms cubic-bezier(.4,0,.2,1);
}

[data-theme="dark"] {
  --bg:            #0d1117;
  --surface:       #161b22;
  --surface-2:     #1c222b;
  /* Deeper than the light theme's move is *lighter* here: the rows are
     #161b22, so a heading that went darker would disappear into them. What
     the eye is after in both themes is the separation, not the direction. */
  --thead-bg:      #212934;
  /* Dark has the same job to do in the other direction: a panel that separates
     itself from the page. The ink stays light here — black on this would be
     unreadable, which is the point of tokenising it rather than hard-coding. */
  --filters-bg:    #232b36;
  --filters-ink:   #f0f6fc;
  --surface-3:     #232a35;
  --overlay:       rgba(0, 0, 0, .65);

  /* The same move, mirrored: "darker" on a light background means "further
     from the page", so here the ramp goes lighter by the same one step. Both
     themes end up at the same contrast against their own surface, which is the
     point — the interface should not read weaker in one than the other. */
  --text:          #f0f6fc;
  --text-2:        #c9d1d9;
  --text-3:        #a6b0bb;
  --text-4:        #838d99;
  /* Mirrored: away from the page here means darker, not lighter. */
  --placeholder:   #5b6572;
  --text-hint:     #7e8894;
  /* The same four, mixed into the dark surface instead — each keeps its hue so
     the mapping an operator learns in one theme holds in the other. */
  --topbar-shipments: #1b2d46;
  --topbar-pickups:   #204235;
  --topbar-returns:   #443c22;
  --topbar-scan:      #33314d;

  --border:        #2a313c;
  --border-2:      #3a424e;
  --border-strong: #4d5866;

  /* The same two tints, dialled down until they only just lift off the dark
     surface — anything stronger fights the text. */
  /* Dark theme: the same two hues taken down to a wash, because at full
     strength a yellow panel in a dark interface is a warning light. */
  --ctype-eshop:          #33290f;
  --ctype-contract:       #2c211a;
  --ctype-eshop-border:   #5c4a1a;
  --ctype-contract-border:#54402c;
  /* Dark has no room to go darker — another 10% down and the tint is the page.
     The same 10% is spent on making it *read* instead: a stronger, slightly
     lighter wash. Same intent as the light theme, opposite direction, which is
     the rule this stylesheet follows wherever a tint has to separate itself
     from its ground. */
  --party-from:        #121f32;
  --party-from-border: #1a3c6d;
  --party-to:          #10271c;
  --party-to-border:   #1d5337;

  /* The brand has to be lifted here, not just its tints. `--brand` does two
     jobs — the fill behind a primary button's white label, and the text colour
     of an active nav item sitting on `--brand-soft` — and #14418a is legible
     in neither against a dark surface. #2f74d8 clears AA against white for the
     first job and reads clearly on the navy tint for the second. The hover and
     pressed steps go *lighter* rather than darker: on a dark surface, darkening
     a control makes it recede when it should be coming forward. */
  --brand:         #2f74d8;
  --brand-600:     #4a89e4;
  --brand-700:     #6099ec;
  --brand-soft:    #101c30;
  --brand-border:  #24406e;
  --success-soft:  #0e2a1c; --success-border: #1e4c34; --success: #4ade80;
  --warning-soft:  #2b1f0d; --warning-border: #55401a; --warning: #fbbf24;
  --danger-soft:   #2c1416; --danger-border:  #572026; --danger:  #f87171;
  --info-soft:     #0f1f38; --info-border:    #1d3a63; --info:    #60a5fa;
  --neutral-soft:  #232a35;

  --shadow-xs: 0 1px 2px rgba(0,0,0,.4);
  --shadow-sm: 0 1px 3px rgba(0,0,0,.5);
  --shadow-md: 0 4px 10px rgba(0,0,0,.5);
  --shadow-lg: 0 12px 24px rgba(0,0,0,.55);
  --shadow-xl: 0 24px 48px rgba(0,0,0,.6);
  --ring: 0 0 0 3px rgba(56, 116, 205, .32);

  --tooltip-bg:  #e8edf5;
  --tooltip-ink: #0d1117;
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100dvh;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
h1,h2,h3,h4,h5,h6 { font-weight: 600; line-height: 1.3; letter-spacing: -.011em; }
h1 { font-size: 22px; } h2 { font-size: 18px; } h3 { font-size: 15px; }
table { border-collapse: collapse; width: 100%; }
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
::placeholder { color: var(--placeholder); opacity: 1; }
/* A select showing its empty option is showing a placeholder. The options
   themselves stay full-strength: the closed control is the prompt, the open
   list is a list of real choices. */
.select.is-placeholder { color: var(--placeholder); }
.select.is-placeholder option { color: var(--text); }
.select.is-placeholder option[value=""] { color: var(--placeholder); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: var(--r-full); border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }

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

/* --------------------------------------------------------------------------
   3. Boot screen
   -------------------------------------------------------------------------- */
.boot {
  min-height: 100dvh; display: grid; place-content: center; gap: 16px; justify-items: center;
}
.boot__mark {
  width: 40px; height: 40px; border-radius: var(--r-md);
  background: var(--brand); animation: pulse 1.2s ease-in-out infinite;
}
.boot__text { color: var(--text-3); font-size: 13px; }
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .5; transform: scale(.9); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes slideIn { from { transform: translateX(100%); } to { transform: none; } }
@keyframes shimmer { 100% { transform: translateX(100%); } }

/* --------------------------------------------------------------------------
   4. App shell
   -------------------------------------------------------------------------- */
.shell { display: flex; min-height: 100dvh; }

.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100dvh;
  transition: width var(--t), flex-basis var(--t);
  z-index: 40;
}
.sidebar--collapsed { width: var(--sidebar-w-collapsed); flex-basis: var(--sidebar-w-collapsed); }
.sidebar--collapsed .nav__label,
.sidebar--collapsed .nav__group-title,
.sidebar--collapsed .brand__text,
.sidebar--collapsed .nav__badge { display: none; }
.sidebar--collapsed .nav__link { justify-content: center; padding-inline: 0; }

.brand {
  display: flex; align-items: center; gap: 10px;
  padding: 0 16px; height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  border-bottom: 1px solid var(--border);
}
.brand__mark {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: var(--r-sm);
  background: var(--brand); color: #fff;
  display: grid; place-content: center; font-weight: 700; font-size: 13px; letter-spacing: -.02em;
}
.brand__text { font-weight: 650; font-size: 15px; letter-spacing: -.015em; white-space: nowrap; }
.brand__sub { display: block; font-size: 10.5px; font-weight: 500; color: var(--text-4); letter-spacing: .02em; text-transform: uppercase; }

.nav { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 10px 10px 16px; }
.nav__group + .nav__group { margin-top: 14px; }
.nav__group-title {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-4); padding: 6px 10px 4px;
}
.nav__link {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--r-sm);
  color: var(--text-2); font-weight: 500; font-size: 13.5px;
  transition: background var(--t-fast), color var(--t-fast);
  white-space: nowrap; position: relative;
}
.nav__link:hover { background: var(--surface-3); color: var(--text); text-decoration: none; }
.nav__link.is-active { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
[data-theme="dark"] .nav__link.is-active { color: #ff6b6b; }
.nav__link.is-active::before {
  content: ''; position: absolute; left: -10px; top: 50%; transform: translateY(-50%);
  width: 3px; height: 18px; background: var(--brand); border-radius: 0 3px 3px 0;
}
/* The two create buttons above the first destination. Tinted so they read as
   actions rather than places, and never `is-active` — you are not "on" them,
   you press them. Separated by a rule so the list below still starts cleanly. */
.nav__group--actions { padding-bottom: 12px; margin-bottom: 4px; border-bottom: 1px solid var(--border); }
.nav__link--action {
  color: var(--brand); font-weight: 600;
  background: color-mix(in srgb, var(--brand) 7%, transparent);
}
.nav__link--action + .nav__link--action { margin-top: 4px; }
.nav__link--action:hover { background: color-mix(in srgb, var(--brand) 14%, transparent); color: var(--brand); }
[data-theme="dark"] .nav__link--action { color: #ff6b6b; }
/* A destination that is also a section — «Отчёты» and its ten reports. The
   row is the link and the chevron is a separate button beside it, so opening
   the list and going to the page stay two different gestures. */
.nav__row { display: flex; align-items: center; gap: 2px; }
.nav__row > .nav__link { flex: 1; min-width: 0; }
.nav__toggle {
  flex: 0 0 24px; height: 28px; display: grid; place-content: center;
  border-radius: var(--r-xs); color: var(--text-4); cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.nav__toggle:hover { background: var(--surface-3); color: var(--text-2); }
/* Indented under the parent and hung off a rule, so a long list still reads
   as belonging to the heading above it rather than as ten more destinations. */
.nav__sub {
  margin: 2px 0 4px 19px; padding-left: 9px;
  border-left: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 1px;
}
.nav__link--sub { padding: 6px 9px; font-size: 12.5px; }
.nav__link--sub.is-active::before { display: none; }
/* Collapsed to icons there is no room for a tree, and the parent link still
   reaches the list page. */
.sidebar--collapsed .nav__sub,
.sidebar--collapsed .nav__toggle { display: none; }

.nav__icon { flex: 0 0 18px; width: 18px; height: 18px; stroke-width: 1.8; }
.nav__label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.nav__badge {
  font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: var(--r-full);
  background: var(--brand); color: #fff; min-width: 18px; text-align: center;
}

.sidebar__footer { border-top: 1px solid var(--border); padding: 8px; }

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

.topbar {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 12px; padding: 0 20px;
  position: sticky; top: 0; z-index: 30;
}
/* The title and, for a page that carries no heading of its own, its one-line
   description. Stacked tight: two lines have to sit inside the bar's fixed
   height, and `min-width: 0` lets a long description ellipsize rather than
   shove the quick search off the end. */
.topbar__heading { min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.topbar__title { font-size: 15px; font-weight: 620; letter-spacing: -.01em; line-height: 1.25; }
/* The way out of a sub-page. It is the one control people reach for without
   looking, so the arrow is bigger than the bar's other ghost buttons, which
   are for things you go looking for deliberately. The explicit svg size is
   needed because the icon would otherwise take its width from the attribute
   and `flex: none` because the title beside it truncates — without it the
   arrow would give up its own width as the title grew. */
.topbar__back { flex: none; width: 34px; margin-right: -2px; }
.topbar__back svg { width: 20px; height: 20px; flex-basis: 20px; }
/* Which record — an anchor a page teleports its number into. It holds no space
   when empty, so an ordinary screen's title sits exactly where it did. */
.topbar__context { display: flex; align-items: center; gap: 7px; flex: none; }
.topbar__context:empty { display: none; }
.topbar__spacer { flex: 1; }

.page { padding: 20px; flex: 1; animation: fadeUp .22s ease both; }
.page--wide { max-width: none; }
.page__header { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.page__heading { flex: 1; min-width: 200px; }
.page__title { font-size: 20px; font-weight: 650; letter-spacing: -.018em; }
.page__subtitle { color: var(--text-3); font-size: 13px; margin-top: 3px; }
.page__actions { display: flex; gap: 8px; flex-wrap: wrap; }

@media (max-width: 900px) {
  .sidebar {
    position: fixed; inset: 0 auto 0 0; transform: translateX(-100%);
    box-shadow: var(--shadow-xl);
  }
  .sidebar--open { transform: none; }
  .page { padding: 14px; }
  .topbar { padding: 0 12px; }
}

/* The operational areas, each marked on the bar in its own colour.
   `/shipments`, `/pickups`, `/returns` and the scan station are where a branch
   spends the day, and an operator moves between them constantly — so the bar
   says *which one* they are on, before any word is read. Everything else keeps
   the plain bar: a report or a masterdata screen is somewhere they visit, and
   colouring those too would leave the colour meaning nothing.

   Kept translucent like the plain bar, so what scrolls underneath still shows
   through the blur. The border goes a step firmer than `--border`, because a
   tinted band that ends in the same hairline as a white one reads as a shadow
   rather than an edge. */
.topbar--shipments,
.topbar--pickups,
.topbar--returns,
.topbar--scan { border-bottom-color: var(--border-2); }

.topbar--shipments { background: color-mix(in srgb, var(--topbar-shipments) 88%, transparent); }
.topbar--pickups   { background: color-mix(in srgb, var(--topbar-pickups)   88%, transparent); }
.topbar--returns   { background: color-mix(in srgb, var(--topbar-returns)   88%, transparent); }
.topbar--scan      { background: color-mix(in srgb, var(--topbar-scan)      88%, transparent); }

.scrim {
  position: fixed; inset: 0; background: var(--overlay); z-index: 35;
  animation: fadeUp .15s ease;
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 36px; padding: 0 14px; border-radius: var(--r-sm);
  font-size: 13.5px; font-weight: 550; white-space: nowrap;
  border: 1px solid transparent; background: var(--surface-3); color: var(--text);
  transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast), opacity var(--t-fast);
  text-decoration: none;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(.5px); }
.btn:disabled, .btn.is-disabled { opacity: .5; pointer-events: none; }
.btn svg { width: 16px; height: 16px; flex: 0 0 16px; stroke-width: 1.9; }

.btn--primary { background: var(--brand); color: #fff; box-shadow: var(--shadow-xs); }
.btn--primary:hover { background: var(--brand-600); }
.btn--primary:focus-visible { box-shadow: var(--ring); }

.btn--secondary { background: var(--surface); border-color: var(--border-2); color: var(--text); box-shadow: var(--shadow-xs); }
.btn--secondary:hover { background: var(--surface-3); border-color: var(--border-strong); }

.btn--ghost { background: transparent; color: var(--text-2); }
.btn--ghost:hover { background: var(--surface-3); color: var(--text); }

.btn--danger { background: var(--danger); color: #fff; }
.btn--danger:hover { filter: brightness(.93); }

.btn--success { background: var(--success); color: #fff; }
.btn--success:hover { filter: brightness(.93); }

.btn--sm { height: 30px; padding: 0 10px; font-size: 12.5px; border-radius: var(--r-xs); }
.btn--sm svg { width: 14px; height: 14px; flex-basis: 14px; }
.btn--lg { height: 42px; padding: 0 20px; font-size: 15px; }
.btn--icon { width: 36px; padding: 0; }
.btn--icon.btn--sm { width: 30px; }
.btn--block { width: 100%; }

.btn-group { display: inline-flex; }
.btn-group .btn { border-radius: 0; margin-left: -1px; }
.btn-group .btn:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); margin-left: 0; }
.btn-group .btn:last-child { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.btn-group .btn.is-active { background: var(--brand-soft); color: var(--brand); border-color: var(--brand-border); z-index: 1; }

/* --------------------------------------------------------------------------
   6. Cards & panels
   -------------------------------------------------------------------------- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-xs);
}
.card__header {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
  /* One height whether or not the header carries a button. 14px of padding
     either side of a title is 49px and either side of a `--sm` button is 58,
     so «Детали» and «Возврат отправителю» sat side by side with their tinted
     bars visibly different depths — which reads as one of them being wrong
     rather than as one of them having a button. 59 is the taller of the two —
     14 + a 30px `--sm` button + 14 + the 1px bottom border, which counts
     because everything here is `border-box` — and it is the taller that wins,
     since a button cannot be made shorter. */
  min-height: 59px;
}
/* Except where the header *is* a strip of tabs: they bring their own 38px and
   their own bottom border, and a minimum meant for a title and a button would
   hang 20px of empty space under them. Was an inline `padding: 0 16px` on two
   pages; it is a class now so the exception is visible and named. */
.card__header--tabs { min-height: 0; padding: 0 16px; }
.card__title { font-size: 14.5px; font-weight: 620; letter-spacing: -.008em; }
/* A header that is an instruction rather than a label — «Смена пароля» on the
   profile page somebody has just been sent to because their password must be
   changed before they can work. A solid bar, not the usual tinted alert: the
   card *is* the message on that screen, and a whole page of pale headers with
   one pale-tinted one among them is not a stop sign.

   The red is written out rather than taken from `--danger`, which flips to a
   pale #f87171 in dark mode — right for text on a dark ground, unreadable
   under white. Both values here are chosen to carry white at AA. */
.card__header--urgent {
  background: #b42318; border-bottom-color: #912018;
  border-radius: var(--r-md) var(--r-md) 0 0;
}
.card__header--urgent .card__title,
.card__header--urgent .card__subtitle { color: #fff; }
[data-theme="dark"] .card__header--urgent { background: #a11f14; border-bottom-color: #7a1710; }
/* A pale header, for a page that is a column of panels rather than one card.
   On the waybill's detail page five panels of the same white as their own
   bodies ran together into one long sheet: the eye had a rule and a slightly
   bolder line to find each section by. A tinted bar gives every panel a
   visible top edge, which is what makes «где кончается один блок и начинается
   другой» answerable at a glance rather than by reading.

   `--brand-soft` and not a colour of its own: it is the same pale blue the
   active nav item and the selected day in a date picker sit on, and it is
   defined in both themes, so a header does not have to be re-picked for dark
   mode. Unlike `--urgent` this is not a signal — the text keeps its ordinary
   colour and contrast, because a header that shouts on five panels at once
   says nothing. */
.card__header--tint {
  background: var(--brand-soft); border-bottom-color: var(--brand-border);
  border-radius: var(--r-md) var(--r-md) 0 0;
}
.card__subtitle { font-size: 12.5px; color: var(--text-hint); margin-top: 1px; }
.card__body { padding: 16px; }
.card__body--flush { padding: 0; }
.card__footer { padding: 12px 16px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--r-md) var(--r-md); }

/* A card that follows the page as it scrolls — the live price beside the
   booking form, which is one long page. Only sticks where there is a column
   to stick within; below the sidebar breakpoint the grid stacks and a sticky
   card would cover the fields underneath it. */
@media (min-width: 1100px) {
  /* Below the top bar, for the reason spelled out on `.form-rail`: the bar is
     sticky at `top: 0` and 60px tall, so anything parked at 16 slides under
     it. */
  .card--sticky { position: sticky; top: calc(var(--topbar-h) + 16px); align-self: start; }
}

/* Legend under a catalogue table: what each enum value in the table means.
   Sits inside the card below the pagination, on the secondary surface so it
   reads as a footnote to the table rather than another section. Two columns
   where there is room, one on a narrow screen. */
/* A manual entry read once: the «?» on a masterdata row. Wider line spacing
   than body text and a capped measure, because it is a paragraph of prose in a
   dialog sized for forms, and prose at 100 characters a line is not read. */
.help-text {
  font-size: 14px; line-height: 1.65; color: var(--text);
  max-width: 62ch; white-space: pre-line;
}

.legend {
  padding: 14px 16px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 0 0 var(--r-md) var(--r-md);
}
.legend__title {
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; color: var(--text-3); margin: 0 0 10px;
}
.legend__list {
  display: grid; gap: 7px 28px; margin: 0;
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
}
/* Term and description on one line, the term fixed-width so the sentences
   line up into a readable column. */
.legend__item { display: flex; gap: 8px; align-items: baseline; }
.legend__term {
  flex: 0 0 148px; font-size: 12.5px; font-weight: 600; color: var(--text);
}
/* The sentence explaining an enum value under a catalogue table — the same
   kind of prose as a card subtitle, so the same step. */
.legend__desc { margin: 0; font-size: 12.5px; color: var(--text-hint); line-height: 1.5; }

@media (max-width: 640px) {
  .legend__item { flex-direction: column; gap: 1px; }
  .legend__term { flex: none; }
}

/* ── Sender / consignee ─────────────────────────────────────────────────────
   The two parties are the one thing every operator looks for first, so they
   carry a tint instead of the plain white surface: `--from` for the sender,
   `--to` for the consignee. Three shapes, one palette:

     .party        a whole card (the booking form's two panels)
     .party-block  a block inside a card (the detail page's two addresses)
     td.party-cell a table cell (the consignee column in every list)

   The tint replaces the card's own background, so put it on the `.card`
   element itself rather than on `.card__body`. */
.party--from       { background: var(--party-from); border-color: var(--party-from-border); }
.party--to         { background: var(--party-to);   border-color: var(--party-to-border); }
.party--from .card__header,
.party--to   .card__header { border-bottom-color: inherit; }

.party-block {
  padding: 12px 14px; border-radius: var(--r); border: 1px solid transparent;
}
.party-block--from { background: var(--party-from); border-color: var(--party-from-border); }
.party-block--to   { background: var(--party-to);   border-color: var(--party-to-border); }
/* The handling branch, given the same weight as the contact name: those two
   are what an operator looks for — who to ring, and where the parcel is. */
.party-block__branch {
  display: flex; align-items: center; gap: 5px;
  font-size: 13px; font-weight: 650; margin-top: 2px;
}
.party-block__branch svg { flex: none; opacity: .65; }

/* Table cells keep the tint edge-to-edge. A cell background paints over the
   row's, so the hover highlight has to be restated here or the tinted column
   would be the one column that stops responding to the pointer. */
.table td.party-cell--from { background: var(--party-from); }
.table td.party-cell--to   { background: var(--party-to); }
.table tbody tr:hover td.party-cell--from { background: color-mix(in srgb, var(--text) 5%, var(--party-from)); }
.table tbody tr:hover td.party-cell--to   { background: color-mix(in srgb, var(--text) 5%, var(--party-to)); }

/* Inputs sitting on a tinted panel keep the plain surface so the writable
   areas still read as writable. */
.party--from .input, .party--to .input,
.party--from .select, .party--to .select { background: var(--surface); }

.grid { display: grid; gap: 16px; }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
/* Exactly two columns, so a pair of panels stays a pair.
   `--2` above is auto-fit: on a wide monitor 1200px of room becomes four
   columns of 300, which is right for cards that are a list and wrong where the
   pairing is the point — «Детали» beside «Возврат отправителю», «Фотоотчёт»
   beside «Документы». Those two rows are how the page is meant to be read, and
   a fourth panel sliding up into one of them breaks it. One column below 900,
   where two of anything is too narrow to read. */
.grid--pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 900px) { .grid--pairs { grid-template-columns: 1fr; } }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
/* Five KPI cards rather than four — an account that earns cashback (093) has
   one more figure than one that does not. A narrower track, because five of
   `--4`'s 190px need 1014px and the content area beside the 252px sidebar is
   about 990 on a 1280 monitor: the fifth card dropped to a row of its own,
   which reads as a card that does not belong with the others. Five of these
   need 914 and fit. Not `--4` with a wider viewport assumed — the row has to
   survive the laptop the counter actually runs. */
.grid--5 { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.grid--sidebar { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }
/* For a rail carrying more than key/value pairs — an address block, a list of
   status transitions. Only 60px more, but it is the difference between those
   wrapping to three lines and fitting on two. */
.grid--sidebar-wide { grid-template-columns: minmax(0, 1fr) 400px; }
@media (max-width: 1100px) {
  .grid--sidebar, .grid--sidebar-wide { grid-template-columns: 1fr; }
}

/* KPI tile */
.kpi { padding: 16px; display: flex; flex-direction: column; gap: 6px; }
.kpi__label { font-size: 12px; font-weight: 550; color: var(--text-3); display: flex; align-items: center; gap: 6px; }
.kpi__value { font-size: 26px; font-weight: 650; letter-spacing: -.03em; line-height: 1.15; font-variant-numeric: tabular-nums; }
.kpi__meta { font-size: 12px; color: var(--text-3); display: flex; align-items: center; gap: 5px; }
.kpi__delta { font-weight: 600; }
.kpi__delta--up { color: var(--success); }
.kpi__delta--down { color: var(--danger); }

/* --------------------------------------------------------------------------
   7. Forms
   -------------------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
/* A field whose value is short and fixed-length — a табельный номер, a code.
   It keeps its cell in the grid (so the field beside it still starts where the
   column does) and only the control is capped, rather than being given a
   narrower grid column, which would drag every row below it out of line. */
.field--narrow .input, .field--narrow .select { max-width: 190px; }
@media (max-width: 760px) { .field--narrow .input, .field--narrow .select { max-width: none; } }
.field__label { font-size: 12.5px; font-weight: 550; color: var(--text-2); display: flex; align-items: center; gap: 4px; }
.field__label .req { color: var(--brand); }
.field__hint { font-size: 11.5px; color: var(--text-4); }
.field__error { font-size: 11.5px; color: var(--danger); font-weight: 500; }

/* ── A field's hint, behind an icon ─────────────────────────────────────────
   Hints used to be a line of grey prose under the field. Read once, then
   furniture — and a row of vertical space on every form for ever, on forms
   that are already long. Behind an icon it is there for whoever wonders.

   Hover *and* focus, because the icon is in the tab order: a hint only a mouse
   can reach is a hint half the operators never see. `cursor: help` is what
   tells a mouse it is worth stopping on.

   The bubble is positioned from the icon rather than the field so it points at
   what it explains, and it is `left: 50%` translated back by half its own
   width only when there is room — a hint on the first column of a grid would
   otherwise hang off the left edge of the dialog. That is why it anchors left
   instead: predictable beats centred. */
.field__info {
  position: relative; display: inline-flex; align-items: center;
  color: var(--text-4); cursor: help;
}
.field__info > svg { width: 13px; height: 13px; flex: 0 0 13px; }
.field__info:hover, .field__info:focus-visible { color: var(--brand); }
.field__info:focus { outline: none; }
.field__info:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 50%; }
/* The bubble itself lives on <body>, teleported and positioned from the icon
   (see `useAnchoredTip`): as a child of the field it was painted under a
   dialog's header and clipped by the dialog's scrolling body, and neither is a
   z-index question. Above the modal layer (100) and the full-screen map (120),
   because a hint is asked for from inside both. */
.tip {
  position: fixed; z-index: 140;
  padding: 7px 9px; border-radius: var(--r-sm);
  background: var(--tooltip-bg); color: var(--tooltip-ink);
  font-size: 12px; font-weight: 450; line-height: 1.4; letter-spacing: 0;
  text-transform: none; white-space: normal; text-align: left;
  box-shadow: var(--shadow-lg); pointer-events: none;
}

.input, .select, .textarea {
  width: 100%; min-width: 0; height: 36px; padding: 0 11px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-2); border-radius: var(--r-sm);
  font-size: 13.5px; transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.textarea { height: auto; min-height: 76px; padding: 9px 11px; resize: vertical; line-height: 1.55; }
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.input:focus, .select:focus, .textarea:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.input:disabled, .select:disabled, .textarea:disabled { background: var(--surface-3); color: var(--text-4); cursor: not-allowed; }
.input.is-invalid, .select.is-invalid { border-color: var(--danger); }
.input--mono { font-family: var(--mono); letter-spacing: .01em; }

/* No spinner on a number field, anywhere.
   Defensive rather than corrective: `TextInput` renders a grouped numeric field
   as `type="text"` (see `grouped` there), so today nothing in the application
   draws a spinner at all — only a field passing `raw`, and nothing does. The
   rule is here so the escape hatch is safe by default, and because nobody sets
   a price, a weight or a box's height by clicking a 9px arrow.
   Both spellings are needed — `appearance` for Firefox, the two pseudo-elements
   for Chrome and Safari, which ignore it on this control. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.select {
  appearance: none; padding-right: 30px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23667085' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}

/* ── Date range ─────────────────────────────────────────────────────────────
   One field for a period, with the month on a popover. The field is a button
   wearing `.input` so it lines up with the real inputs beside it in a filter
   row without inheriting their text-entry behaviour. */
.daterange { position: relative; }
.daterange__field {
  display: flex; align-items: center; gap: 7px; cursor: pointer; text-align: left;
  color: var(--text-4);
}
.daterange__field.is-set { color: var(--text); }
.daterange__field > svg { flex: 0 0 auto; color: var(--text-4); }
.daterange__clear { flex: 0 0 auto; padding: 0 2px; color: var(--text-4); font-size: 15px; line-height: 1; }
.daterange__clear:hover { color: var(--danger); }
.daterange__pop {
  position: absolute; z-index: 60; top: calc(100% + 5px); left: 0; min-width: 258px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--shadow-lg); padding: 8px; animation: fadeUp .12s ease both;
}
.daterange__head { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
.daterange__month { flex: 1; text-align: center; font-size: 13px; font-weight: 600; text-transform: capitalize; }
.daterange__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.daterange__grid--head span {
  text-align: center; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  color: var(--text-4); padding-bottom: 3px;
}
.daterange__day {
  height: 30px; border: 0; background: none; color: var(--text); cursor: pointer;
  font-size: 12.5px; border-radius: var(--r-xs); font-variant-numeric: tabular-nums;
}
.daterange__day:hover { background: var(--surface-3); }
.daterange__day.is-outside { color: var(--text-4); }
/* The span between the two ends is tinted, the ends themselves are solid — so
   half a selection still reads as "from here to wherever you go next". */
.daterange__day.is-in { background: var(--brand-soft); border-radius: 0; }
.daterange__day.is-edge { background: var(--brand); color: #fff; font-weight: 600; }
.daterange__day.is-today:not(.is-edge) { box-shadow: inset 0 0 0 1px var(--border-strong); }
.daterange__foot { display: flex; align-items: center; gap: 4px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }

.input-group { display: flex; align-items: stretch; }
.input-group .input { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.input-group__addon {
  display: inline-flex; align-items: center; padding: 0 11px;
  background: var(--surface-3); border: 1px solid var(--border-2); border-left: none;
  border-radius: 0 var(--r-sm) var(--r-sm) 0; font-size: 12.5px; color: var(--text-3); white-space: nowrap;
}

.checkbox, .radio { display: inline-flex; align-items: flex-start; gap: 8px; cursor: pointer; font-size: 13.5px; }
.checkbox input, .radio input { width: 16px; height: 16px; margin-top: 2px; accent-color: var(--brand); flex: 0 0 16px; cursor: pointer; }
.checkbox span, .radio span { user-select: none; }

.switch { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  width: 36px; height: 20px; border-radius: var(--r-full); background: var(--border-2);
  position: relative; transition: background var(--t-fast); flex: 0 0 36px;
}
.switch__track::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: transform var(--t-fast);
}
.switch input:checked + .switch__track { background: var(--brand); }
.switch input:checked + .switch__track::after { transform: translateX(16px); }
.switch input:focus-visible + .switch__track { box-shadow: var(--ring); }

.form-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.form-grid--2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* A fixed column count, for fields that belong together and have to stay on
   one line. `auto-fit` decides for itself and will break the row the moment the
   panel narrows — which is right for a long form and wrong for a set of three
   fields that are read as one sentence. Collapses to a single column on a
   phone, where one line is not achievable and not desirable. */
/* One column, explicitly — for a `.form-grid` that would otherwise auto-fit
   a lone field across the full width and leave it stretched. */
.cols-1 { grid-template-columns: minmax(0, 1fr); }
.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
/* Weighted variants, where one field genuinely needs more room than its
   neighbours — a street address beside a flat number. */
.cols-2--wide-first { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
/* The mirror, for a fixed-width thing beside a variable one — a four-digit
   branch code and the street it is on. */
.cols-2--wide-last  { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
.cols-3--wide-mid   { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr); }
/* The pickup window row — date, the two ends of the window, the two counts.
   Five equal columns starve the first: a native date control needs ~140px to
   show dd.mm.yyyy beside its calendar button and a time ~95px, so an even split
   cuts the year off on a laptop. Weighted with floors instead.

   Those five want ~507px including gaps, and the booking column is narrower
   than that until roughly 1260px of viewport — so below that the row is let go
   rather than forced, auto-fit packing as many as genuinely fit at the date's
   own width. Forcing it there does not shrink the row, it overflows the card. */
.form-grid--window { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
@media (min-width: 1260px) {
  /* Six on one line: date, the window's two ends, the two counts and the
     channel the call came in on. The channel sits beside the weight rather
     than wrapping to a row of its own under the date, where it lined up with
     nothing. It gets the widest minimum of the six because it holds words
     («Мобильное приложение») where the others hold numbers. */
  /* The date is a fixed-width control — the browser draws `type="date"` at the
     size of `дд.мм.гггг` and a calendar button, and it does not grow to fill a
     wider cell. It used to take the widest share of the row, which bought a
     gap to the right of the picker and squeezed the two counts beside it. It
     is now the narrowest of the six that holds a real value, and the room went
     to the fields whose contents actually vary in length. */
  .form-grid--window {
    grid-template-columns:
      minmax(126px, .95fr) repeat(2, minmax(88px, .9fr))
      repeat(2, minmax(0, .62fr)) minmax(120px, 1.35fr);
  }
  /* Seven, when the dispatcher may also name the courier.
     The courier is last and takes the largest share by a distance: it is the
     only cell on the row holding a person's name, and a name elided to
     «Абдулла…» defeats the point of choosing from a list. What it takes, the
     two estimates give up — «Мест» and «Вес (кг)» hold three characters each
     and had a full share apiece, so a number sat in the left third of a cell
     with two thirds of white space beside it. They are the narrowest of the
     seven now, which is the shape of what they contain. */
  .form-grid--window--courier {
    grid-template-columns:
      minmax(122px, .85fr) repeat(2, minmax(84px, .78fr))
      repeat(2, minmax(0, .52fr)) minmax(112px, 1.1fr)
      minmax(190px, 2fr);
  }
}
/* Between the tablet break and the one-line width the row wraps and the columns
   go back to being equal. Deliberately left alone: `max-width` on a grid item
   narrows the control and not the track, so capping the two estimates there
   would buy the gap this block's opening comment exists to warn about. Below
   1260px the row is not one line and the widths are not what is being read. */
@media (max-width: 760px) {
  .cols-2, .cols-3, .cols-4, .cols-5, .cols-6,
  .cols-2--wide-first, .cols-2--wide-last, .cols-3--wide-mid, .form-grid--window { grid-template-columns: 1fr; }
}
/* `.pay-row__top` is deliberately **not** in the list above. Its second column
   is a fixed 124px rather than a share, so it does not starve on a narrow
   screen the way a fraction does — and the pair is «касса and how much», which
   is one thought and worth keeping on one line where it fits at all. */
.form-row { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }
@media (max-width: 640px) { .form-row { grid-template-columns: 1fr; } }
.span-2 { grid-column: span 2; }
/* Wider spans, for a grid whose columns are a unit rather than a field each —
   `.cols-4` with a 3+1 row over two 2+2 rows, which is how a form gets a 75/25
   split on one line and an even one on the next without a second grid. */
.span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; }
/* Where the grid itself collapses to one column, a span has nothing to span. */
@media (max-width: 760px) { .span-3, .span-4 { grid-column: auto; } }
@media (max-width: 640px) { .span-2 { grid-column: auto; } }
/* The whole row, however many columns `auto-fit` decided on. `span 2` only
   guesses at that, and a wrong guess leaves the next field stranded at the end
   of the row instead of at the start of the next one. */
.span-full { grid-column: 1 / -1; }
/* Break the row before this field without knowing how many columns `auto-fit`
   settled on: pinning it to column 1 makes auto-placement drop to the next row
   whenever column 1 of the current one is taken. Use it to keep two groups of
   fields apart in one grid — the account block and the person at the door —
   where a wide screen already separates them and a narrow one runs them
   together. A no-op when the field would have started the row anyway. */
.row-start { grid-column: 1; }

.fieldset { border: 1px solid var(--border); border-radius: var(--r); padding: 14px; }
.fieldset__legend { font-size: 12px; font-weight: 620; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); padding: 0 6px; }
/* A tinted fieldset — the two ends of a route on the quote screen. Written
   with both classes because `.fieldset` sets the `border` shorthand further
   down the file and would otherwise reset the tinted border-color. Same
   `--party-*` tokens the register and the detail cards use, so blue is always
   where it came from and green always where it is going, in both themes. */
.fieldset.party-block--from {
  background: var(--party-from); border-color: var(--party-from-border);
}
.fieldset.party-block--to {
  background: var(--party-to); border-color: var(--party-to-border);
}
.fieldset.party-block--from .fieldset__legend,
.fieldset.party-block--to .fieldset__legend { color: var(--text-2); }

/* --------------------------------------------------------------------------
   8. Tables
   -------------------------------------------------------------------------- */

/* The strip above a table that carries the column picker. Right-aligned and
   sitting on the table's own border, so it reads as part of the table rather
   than as another row of page actions. */
.table-tools {
  display: flex; justify-content: flex-end; align-items: center; gap: 8px;
  padding: 7px 12px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}

/* When the strip also carries the page's own controls it stops being a utility
   strip and becomes the page's single toolbar row, so it takes `.toolbar`'s
   metrics and its plain background — the filters are the row's main content
   now, not an accessory to the table. */
.table-tools--bar {
  justify-content: flex-start; flex-wrap: wrap; gap: 9px;
  padding: 12px 16px; background: transparent;
}
/* An auto margin, not a flex-grow: `.search` is `flex: 1` with a 380px cap, and
   a growing spacer would split the free space with it and leave the search box
   short of its own maximum. This lets the search fill first and swallows only
   what is left over. */
.table-tools__spacer { flex: 0 0 0; margin-inline-start: auto; }

/* Wider than an ordinary dropdown: these are column names, and truncating
   them defeats the point of choosing between them. Capped in height because a
   register has thirty-five, and scrolled inside itself so the page does not
   move under the menu. */
.col-picker { width: 260px; }
.col-picker__list { max-height: 320px; overflow-y: auto; padding: 4px; }
.col-picker__item {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 8px; border-radius: var(--r-sm); cursor: pointer; font-size: 13px;
}
.col-picker__item:hover { background: var(--surface-3); }
.col-picker__item input { width: 15px; height: 15px; accent-color: var(--brand); flex: 0 0 15px; cursor: pointer; }
.col-picker__foot {
  display: flex; gap: 4px; padding: 5px; border-top: 1px solid var(--border);
}
.col-picker__foot .btn { flex: 1; }

/* A boxed explanation inside a form — what is about to happen, stated before
   the button is pressed. Tinted by tone, but the icon and the wording carry
   the meaning; the colour only reinforces it. */
.callout {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 11px 13px; border-radius: var(--r); font-size: 13px;
  border: 1px solid var(--border); background: var(--surface-2);
}
.callout > svg { flex: 0 0 auto; margin-top: 1px; }
.callout--info    { background: var(--info-soft);    border-color: var(--info-border);    color: var(--text); }
.callout--info    > svg { color: var(--info); }
.callout--warning { background: var(--warning-soft); border-color: var(--warning-border); color: var(--text); }
.callout--warning > svg { color: var(--warning); }

/* The band linking a waybill to the return leg carrying it back, shown at the
   top of either one. Brand-tinted rather than semantic: a return is a normal
   part of the business, not a warning. */
.rto-link {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 10px 14px; border-radius: var(--r);
  background: var(--brand-soft); border: 1px solid var(--brand-border);
  color: var(--text); font-size: 13.5px;
}
.rto-link > svg { color: var(--brand); flex: 0 0 auto; }

/* One suggestion from the phone-number lookup: a whole row is the target, not
   a link inside it, because the operator is choosing a person rather than
   following a reference. */
.party-suggest {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--r);
  background: var(--surface); color: var(--text); cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.party-suggest:hover { background: var(--brand-soft); border-color: var(--brand-border); }
.party-suggest > svg { color: var(--text-4); flex: 0 0 auto; }

/* The report index: what «Отчёты» itself opens. Cards rather than a list,
   because choosing a report is mostly reading the descriptions — the name
   alone does not tell anyone whether «Уровень сервиса» answers their question. */
.section-title {
  font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-3); margin: 0 0 10px;
}
.report-card {
  display: flex; align-items: center; gap: 12px; text-align: left; width: 100%;
  padding: 14px; cursor: pointer;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.report-card:hover { border-color: var(--brand-border); box-shadow: var(--shadow-sm); }
.report-card__icon {
  flex: 0 0 34px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-content: center;
  background: var(--brand-soft); color: var(--brand);
}
.report-card__name { display: block; font-weight: 600; font-size: 13.5px; color: var(--text); }
.report-card__desc { display: block; font-size: 12px; color: var(--text-3); margin-top: 2px; line-height: 1.4; }
.report-card > svg:last-child { color: var(--text-4); flex: 0 0 auto; }

/* Document change log. One collapsed row per change; opening it shows the
   fields that moved with the old value beside the new one. The before/after
   columns are tinted rather than labelled by colour alone — the heading row
   says which is which, and the tint only reinforces it. */
.chg { border-bottom: 1px solid var(--border); }
.chg:last-child { border-bottom: 0; }
.chg__head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 4px; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--text); text-align: left;
}
.chg__head:hover { background: var(--surface-2); }
/* A comment's header is not a button — nothing opens — so it must not light up
   under the cursor the way a change entry does. */
.chg__head--static { cursor: default; }
.chg__head--static:hover { background: none; }
/* The comment itself (migration 057). `pre-wrap` because a note is typed with
   its own line breaks and a courier's three-line instruction collapsed into
   one paragraph is materially harder to read at a door. */
.chg__body {
  margin: 0 0 10px; padding: 0 4px; font-size: 13px; line-height: 1.5;
  color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere;
}
.chg__when { color: var(--text-3); font-size: 12.5px; white-space: nowrap; }
.chg__who  { color: var(--text-2); font-weight: 550; }
.chg__move { font-size: 12px; color: var(--text-2); }
.chg__diff { width: 100%; font-size: 12.5px; margin: 0 0 10px; border-collapse: collapse; }
.chg__diff th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .03em;
  color: var(--text-3); font-weight: 600; padding: 5px 8px; border-bottom: 1px solid var(--border);
}
.chg__diff td { padding: 5px 8px; border-bottom: 1px solid var(--border); vertical-align: top;
  word-break: break-word; max-width: 260px; }
.chg__old { background: var(--danger-soft);  color: var(--text-2); text-decoration: line-through; }
.chg__new { background: var(--success-soft); color: var(--text); font-weight: 550; }

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table { font-size: 13px; min-width: 100%; }
.table th {
  text-align: left; font-weight: 600; font-size: 11.5px; letter-spacing: .03em;
  text-transform: uppercase; color: var(--text-3);
  padding: 5px 14px; background: var(--thead-bg);
  border-bottom: 1px solid var(--border); white-space: nowrap; position: sticky; top: 0; z-index: 2;
}
.table th.is-sortable { cursor: pointer; user-select: none; }
.table th.is-sortable:hover { color: var(--text); }
.table th .sort-ind { opacity: .45; margin-left: 3px; }
.table th.is-sorted .sort-ind { opacity: 1; color: var(--brand); }
/* ── Row density ───────────────────────────────────────────────────────────
   Vertical padding only; the horizontal stays, because columns need their
   gutters however tight the rows are.

   4px rather than the old 10px. A document row is two lines of content
   (a name over a phone, 19.5px each), so the padding was carrying a quarter of
   the row height for nothing — an operator scanning a hundred waybills wants
   more of them on screen, not more air between them.

   The three tiers move together so the ladder stays ordered: a table marked
   `--compact` must not end up taller than a plain one. Below the default they
   are separated by line-height and font-size as much as by padding, which is
   why 3px and 2px still read as different densities.
   ------------------------------------------------------------------------ */
.table td { padding: 4px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tbody tr { transition: background var(--t-fast); }
/* One row height across the ordinary lists, whatever a particular cell holds.
   Padding alone does not give you that: a cell with two lines in it — a name
   over a phone, a total over its COD — is 46px tall, and a list of single-line
   cells came out at 28. Moving between /shipments and /returns, /manifests or
   /runs, the rows visibly changed size for no reason a reader could name. A
   minimum on the row settles it in the direction of the taller table, since a
   two-line cell cannot be made shorter and a one-line row can be given room.
   It is a *minimum*: a taller cell still sets its own row's height.
   Compact and dense opt out by construction — they exist to be denser than
   this, and a minimum applied to them would erase the distinction. */
.table:not(.table--compact):not(.table--dense) tbody tr { height: 46px; }
.table tbody tr:hover { background: var(--surface-2); }
.table tbody tr.is-clickable { cursor: pointer; }
/* A row that opens on a *double* click. A pointer would promise that one click
   does something and it does not — `cell` is the cursor a spreadsheet uses for
   exactly this, and it is the one people already read as "click to select". */
.table tbody tr.is-selectable { cursor: cell; }
.table tbody tr:last-child td { border-bottom: none; }
.table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.table .mono { font-family: var(--mono); font-size: 12.5px; }
.table .nowrap { white-space: nowrap; }
.table__actions { display: flex; gap: 4px; justify-content: flex-end; }
/* One payment on the booking form's rail (085). A rule between the rows rather
   than a box around each: they are a list of one kind of thing, and a card per
   payment in a 340px rail is three borders to say what one line does. */
.pay-row + .pay-row { border-top: 1px solid var(--border); margin-top: 10px; padding-top: 10px; }
/* Касса and сумма on one line, with the sum given a fixed column rather than a
   share of one. A share of a 340px rail is ~90px, and a grouped sum is wider
   than its digits — «100 000» carries a separator — so it came out as
   «100 00», the one kind of truncation this form cannot have, because the
   figure is read back to a customer. 124px holds seven digits and their
   groups. The касса takes what is left and truncates instead: a select keeps
   its full value one click away, a number does not. */
.pay-row__top { grid-template-columns: minmax(0, 1fr) 124px; }
/* The row's own mark (094), above its controls rather than beside them: the
   rail is 340px and the line below is already a select and a sum that need
   every pixel they have. It reads as a heading for the row, which is what it
   is — «эта строка Payme» — and it is the one place a mark sits over a control
   that repeats its name, because the select is what sets the till and this is
   what an operator checks three rows by without reading three dropdowns. */
.pay-row__mark { margin-bottom: 6px; }
/* An icon button beside a field never gives up its 36px — the field next to it
   is what absorbs a narrow rail. Without this the browser shrinks both in
   proportion and the glyph ends up off-centre in a squashed box. */
.row > .btn--icon { flex: 0 0 auto; }

/* A totals row under a table's own column, where the eye already is — the
   pieces table sums its «Факт., кг» there rather than in a sentence below the
   table. `:last-child td { border-bottom: none }` above has already taken the
   rule off the last body row, so the footer supplies its own top border; it is
   the strong one, because the line is what separates a total from the rows it
   totals rather than one row from the next. */
.table tfoot td { border-top: 1px solid var(--border-strong); border-bottom: none; }

.table--compact td { padding: 3px 12px; }
.table--compact th { padding: 3px 12px; }
/* A step below compact, for a register read as a block of data. The line-height
   comes down with the padding — leaving it at the body's 1.5 keeps the row tall
   however little padding it has. */
.table--dense td { padding: 2px 9px; font-size: 12.5px; line-height: 1.35; }
.table--dense th { padding: 2px 9px; }
/* A tenth taller than the padding and line-height alone produce (22px → 24px).
   The register is read for minutes at a time down a column of thirty-odd, and
   at 22 the rows ran together; the point of `dense` is to fit more on screen,
   not to make a line hard to keep your place in. Set on the row rather than as
   more padding so the numbers stay vertically centred in the taller cell. */
.table--dense tbody tr { height: 24px; }
.table--dense .badge { padding: 0 5px; font-size: 10.5px; }

/* ── A table whose cells are fields ─────────────────────────────────────────
   The pieces on the booking form: six short values a row, every one of them
   typed. With the ordinary `.input` chrome each cell became a box inside a
   box — a ten-piece booking drew sixty borders, and the table's own grid
   disappeared underneath them.

   So the field is flat until it is pointed at. The column headings and the
   row rules carry the structure, which is what a table is for; the border
   comes back on hover and on focus, where it is not decoration but the
   answer to "which cell am I in?". Transparent rather than `border: 0`,
   so nothing moves by a pixel when it appears.

   The horizontal padding is split between the cell and the input — 8 + 4 —
   so the text starts at the same 12px as the heading above it. Change one
   and change the other, or the columns stop lining up with their names. */
.table--fields td { padding: 2px 8px; }
.table--fields td .input,
.table--fields td .select {
  height: 30px; padding: 0 4px;
  background-color: transparent; border-color: transparent;
}
.table--fields td .select { padding-right: 22px; background-position: right 3px center; }
.table--fields td .input:hover,
.table--fields td .select:hover { background-color: var(--surface); border-color: var(--border-2); }
.table--fields td .input:focus,
.table--fields td .select:focus { background-color: var(--surface); border-color: var(--brand); }
/* The row highlight would otherwise show through the fields and make a hovered
   row look like six half-lit boxes. */
.table--fields tbody tr:hover { background: none; }

/* Line number: a gutter, not data. Narrow, dim, and never the thing the eye
   lands on first. */
.table th.table__num, .table td.table__num {
  width: 1%; white-space: nowrap; text-align: right;
  color: var(--text-4); font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--border);
}

/* Keyboard cursor. The row tints and the cell outlines: the row says where you
   are down the register, the cell says which column you are reading across it,
   which is the pair you lose when a table is wider than the screen. `inset` so
   the outline cannot be clipped by the scroll container. */
.table tbody tr.is-cursor-row { background: var(--surface-2); }
.table td.is-cursor { box-shadow: inset 0 0 0 2px var(--brand); border-radius: 2px; }
.table-wrap:focus { outline: none; }
.table-wrap:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* ── Filter row ─────────────────────────────────────────────────────────────
   A second <thead> row of inputs, one under each heading. Both rows stick, so
   the heading row's height has to be a known quantity rather than whatever the
   text wrapped to — hence the fixed height on the headings and the matching
   offset on the filters. Change one, change the other. */
/* `fixed` so the stated column widths are honoured rather than negotiated: a
   register is read by scanning down a column, and a column that changes width
   with its contents cannot be scanned. It also makes the ellipsis below work —
   a cell only clips when its width was decided before its text. */
.table--filtered { --thead-h: 34px; table-layout: fixed; }
.table--filtered thead th { height: var(--thead-h); padding-block: 0; z-index: 3; }
.table--filtered tbody td,
.table--filtered thead th {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A heading longer than its column clips rather than wraps — the two header
   rows are both sticky, so their height has to stay the fixed quantity the
   filter row's `top` is measured against. The full text is in the tooltip. */
.table--filtered thead th .sort-ind { float: right; }
.table__filters td {
  position: sticky; top: var(--thead-h); z-index: 2;
  padding: 5px 8px; background: var(--thead-bg);
  border-bottom: 1px solid var(--border-2); vertical-align: middle;
}
/* The controls inside are deliberately small — thirty of them across one row,
   and every pixel of height is a row of data pushed off the screen. */
.table__filters .input,
.table__filters .select {
  height: 28px; padding: 0 8px; font-size: 12px; min-width: 0; width: 100%;
  background: var(--surface);
}
.table__filters .select { padding-right: 22px; background-position: right 5px center; }
/* Two boxes side by side (a from/to pair) still have to fit the column. */
.table__filters .filter-pair { display: flex; gap: 4px; }

/* A tinted column keeps its tint in the filter row too, or the stripe breaks
   exactly where the eye is following it down the table. */
.table__filters td.party-cell--from { background: var(--party-from); }
.table__filters td.party-cell--to   { background: var(--party-to); }

.pagination {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 11px 16px; border-top: 1px solid var(--border); font-size: 13px; color: var(--text-3);
}
.pagination__spacer { flex: 1; }

/* --------------------------------------------------------------------------
   9. Badges, chips, pills
   -------------------------------------------------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; border-radius: var(--r-full);
  font-size: 11.5px; font-weight: 550; line-height: 1.5;
  background: var(--neutral-soft); color: var(--text-2);
  border: 1px solid transparent; white-space: nowrap;
}
.badge--success { background: var(--success-soft); color: var(--success); border-color: var(--success-border); }
.badge--warning { background: var(--warning-soft); color: var(--warning); border-color: var(--warning-border); }
.badge--danger  { background: var(--danger-soft);  color: var(--danger);  border-color: var(--danger-border); }
.badge--info    { background: var(--info-soft);    color: var(--info);    border-color: var(--info-border); }
.badge--brand   { background: var(--brand-soft);   color: var(--brand);   border-color: var(--brand-border); }
.badge__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 6px; }

/* Yes/no cells in a table.
   A column of "Да"/"Нет" in one weight reads as a wall of text — you have to
   go word by word to find the rows that are on. Weight and colour carry "yes"
   so the eye can scan the column; "no" recedes to the muted text colour
   rather than turning red, because it is the ordinary case, not a fault.
   Both tokens flip with the dark theme, so the contrast holds either way. */
.bool--yes { font-weight: 700; color: var(--success); }
.bool--no  { color: var(--text-3); }
/* For a flag where "yes" is the bad news — a refusal, a breach. The eye should
   land on the exception, not on the forty rows that are fine. */
.bool--alert { font-weight: 700; color: var(--danger); }

.chips { display: flex; gap: 7px; flex-wrap: wrap; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 11px; border-radius: var(--r-full);
  background: var(--surface); border: 1px solid var(--border-2);
  font-size: 12.5px; font-weight: 500; color: var(--text-2);
  transition: all var(--t-fast); cursor: pointer;
}
.chip:hover { border-color: var(--border-strong); color: var(--text); }
.chip.is-active { background: var(--brand-soft); border-color: var(--brand-border); color: var(--brand); font-weight: 600; }
.chip__count { font-size: 11px; font-weight: 650; opacity: .7; font-variant-numeric: tabular-nums; }

/* Prominent pill tabs — larger, clearer than chips; used to switch catalogues. */
.tab-pills { display: flex; gap: 8px; flex-wrap: wrap; }
.tab-pill {
  display: inline-flex; align-items: center;
  height: 38px; padding: 0 17px; border-radius: var(--r-full);
  background: var(--surface); border: 1px solid var(--border-2);
  font-size: 14px; font-weight: 550; color: var(--text-2);
  transition: all var(--t-fast); cursor: pointer;
}
.tab-pill:hover { border-color: var(--border-strong); color: var(--text); }
.tab-pill.is-active {
  background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 640;
}

/* ── Returns panel, document attachments, signature capture ─────────────────── */
.retrow { padding: 11px 0; border-top: 1px solid var(--border); }
.retrow:first-child { border-top: 0; padding-top: 2px; }
.rr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.rr-actions { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 9px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
/* Also used on a <button> — the proof of a return opens in the viewer rather
   than at a URL, and it should still read as the link it always was. */
.link {
  color: var(--brand); font-weight: 550; cursor: pointer;
  background: none; border: 0; padding: 0; font-size: inherit; font-family: inherit;
}
.link:hover { text-decoration: underline; }

.doc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 10px; }
.doc-item { border: 1px solid var(--border-2); border-radius: var(--r-md); overflow: hidden; background: var(--surface); }
/* Also a <button> now, not only an <a>: a photo opens in the lightbox rather
   than in a browser tab, and the thing that opens it is not a link to
   anywhere. The reset is what keeps the two looking identical. */
.doc-thumb {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 76px; padding: 0; border: 0;
  background: var(--surface-2); color: var(--text-3); cursor: zoom-in;
}
.doc-thumb img { width: 100%; height: 100%; object-fit: cover; }
.doc-meta { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px; gap: 4px; }
.thumb-sm { width: 54px; height: 54px; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--border-2); }
/* A captured signature shown as evidence rather than as decoration (088): a
   drawn PNG is mostly transparent, so it needs a light ground of its own or it
   disappears entirely in the dark theme. Bounded in height so a wide canvas
   does not push a details panel apart, and `object-fit: contain` because a
   cropped signature is not the signature anybody gave. */
.sig-proof {
  display: block; width: 100%; max-width: 260px; height: 64px;
  object-fit: contain; object-position: left center;
  background: #fff; border: 1px solid var(--border-2); border-radius: var(--r-sm);
  padding: 2px 4px; margin-top: 4px;
}

/* ── Lightbox ───────────────────────────────────────────────────────────────
   An attachment, the size of the window. Above the modal layer (100) and the
   full-screen map (120), because it can be opened from inside either. Dark
   ground in both themes on purpose: a photograph is being judged, and a white
   surround pulls the eye's white balance with it. */
.lightbox {
  position: fixed; inset: 0; z-index: 130;
  display: flex; flex-direction: column;
  background: rgba(9, 12, 18, .93);
}
.lightbox__bar {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px; color: #f3f5f8;
  border-bottom: 1px solid rgba(255, 255, 255, .12);
}
.lightbox__name { font-size: 14px; font-weight: 560; max-width: 40%; }
.lightbox__meta, .lightbox__count { font-size: 12.5px; color: rgba(243, 245, 248, .62); }
/* The two buttons in the bar sit on a dark ground, so they take their colours
   from it rather than from the page's tokens. */
.lightbox__bar .btn--ghost { color: rgba(243, 245, 248, .82); }
.lightbox__bar .btn--ghost:hover { background: rgba(255, 255, 255, .1); color: #fff; }
.lightbox__bar .btn--secondary {
  background: rgba(255, 255, 255, .12); border-color: rgba(255, 255, 255, .2); color: #fff;
}
.lightbox__bar .btn--secondary:hover { background: rgba(255, 255, 255, .2); }
.lightbox__stage {
  flex: 1; min-height: 0; position: relative;
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.lightbox__img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--r-sm); }
/* A drawing on a transparent ground — a signature — on white, as `.sig-proof`
   shows it in the page. Without it the ink is dark on the dark stage above and
   the viewer looks empty. White in both themes, because that is the colour of
   the paper the signature is being read as; the margin keeps the strokes off
   its edge. Photographs never get it (see `onPaper` in media.js). */
.lightbox__img--sheet { background: #fff; padding: 16px 20px; }
.lightbox__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; display: grid; place-content: center;
  color: #fff; background: rgba(255, 255, 255, .12); border: 0;
  border-radius: 50%; cursor: pointer;
}
.lightbox__nav:hover { background: rgba(255, 255, 255, .24); }
.lightbox__nav--prev { left: 14px; }
.lightbox__nav--next { right: 14px; }
@media (max-width: 640px) {
  .lightbox__name { max-width: 100%; }
  .lightbox__nav { width: 38px; height: 38px; }
}

.sigpad { position: relative; }
.sigpad__canvas {
  display: block; width: 100%;
  border: 1px dashed var(--border-strong); border-radius: var(--r-md);
  background: #fff; touch-action: none; cursor: crosshair;
}
.sigpad__clear { position: absolute; top: 6px; right: 6px; }
.filebtn { display: inline-flex; }

/* --------------------------------------------------------------------------
   Map — tile canvas, pins and the picker around them
   -------------------------------------------------------------------------- */
.map {
  position: relative; overflow: hidden; width: 100%;
  border: 1px solid var(--border-2); border-radius: var(--r-md);
  background: var(--surface-3);
  touch-action: none; user-select: none; -webkit-user-select: none;
}
.map--interactive { cursor: crosshair; }
.map--interactive.is-dragging { cursor: grabbing; }
.map:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--brand); }

.map__tiles { position: absolute; inset: 0; transform-origin: 50% 50%; }
.map__tile {
  position: absolute; width: 256px; height: 256px;
  pointer-events: none; -webkit-user-drag: none;
}

/* OSM tiles are drawn for light interfaces; invert them rather than punch a
   white hole in a dark screen. Everything layered on top stays untouched. */
[data-theme="dark"] .map__tiles { filter: invert(1) hue-rotate(180deg) brightness(.92) contrast(.9); }

.map__pin {
  position: absolute; color: var(--brand); pointer-events: none;
  transform: translate(-50%, -100%); filter: drop-shadow(0 2px 3px rgba(16, 24, 40, .35));
}
.map__pin--info { color: var(--info); }
.map__pin--success { color: var(--success); }
/* A numbered stop reads better as a disc than as a teardrop. */
.map__pin--num { transform: translate(-50%, -50%); }
.map__pin-num {
  display: grid; place-content: center; width: 26px; height: 26px;
  background: currentColor; border: 2px solid #fff; border-radius: 50%;
}
.map__pin-num i { font-style: normal; font-size: 12px; font-weight: 700; color: #fff; }
.map__pin-label {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: 2px; white-space: nowrap; font-size: 11px; font-weight: 600;
  color: var(--text); background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-xs); padding: 1px 5px;
}

.map__zoom { position: absolute; top: 8px; right: 8px; display: grid; gap: 4px; }
.map__zoom-btn {
  width: 28px; height: 28px; display: grid; place-content: center;
  font-size: 16px; line-height: 1; font-weight: 600;
  color: var(--text); background: var(--surface); cursor: pointer;
  border: 1px solid var(--border-2); border-radius: var(--r-sm); box-shadow: var(--shadow-xs);
}
.map__zoom-btn:hover { background: var(--surface-3); }

/* The map, the size of the window. Its own overlay rather than a `Modal`,
   for the Escape-key reason spelled out where it is built: a picker is often
   already inside a dialog, and two stacked modals both listening on `document`
   turn one key press into two closings. The z-index clears the modal layer
   (100) so the map opens over a dialog it was launched from. */
.map-full {
  position: fixed; inset: 0; z-index: 120;
  display: flex; flex-direction: column;
  background: var(--surface);
}
.map-full__bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.map-full__title { font-size: 14.5px; font-weight: 620; }
/* `min-height: 0` is what lets the map shrink inside the flex column instead
   of pushing the bar off the top: a flex item's default minimum is its content,
   and the map's content is a grid of tiles as tall as it likes. */
.map-full__canvas { flex: 1; min-height: 0; padding: 12px; }
/* No rounded corners or border on a map that fills its box. */
.map-full__canvas .map { height: 100%; border-radius: var(--r-md); }

.map__hint {
  position: absolute; inset: 0; display: grid; place-content: center; text-align: center;
  padding: 12px; font-size: 13px; font-weight: 600; color: #fff;
  background: rgba(16, 20, 28, .55);
}
.map__attr {
  position: absolute; right: 0; bottom: 0;
  font-size: 10px; color: var(--text-2); background: rgba(255, 255, 255, .78);
  padding: 1px 5px; border-top-left-radius: var(--r-xs);
}
[data-theme="dark"] .map__attr { color: var(--text-2); background: rgba(13, 17, 23, .78); }

.mappick { display: flex; flex-direction: column; gap: 8px; }
.mappick__bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mappick__coords { font-size: 12px; color: var(--text-2); }
.mappick__body { display: flex; flex-direction: column; gap: 8px; }
.mappick__search { display: flex; align-items: center; gap: 6px; }
.mappick__search .search { flex: 1; }
.mappick__results {
  list-style: none; margin: 0; padding: 4px; max-height: 176px; overflow-y: auto;
  border: 1px solid var(--border-2); border-radius: var(--r-md); background: var(--surface);
}
.mappick__results button {
  display: flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left;
  padding: 6px 8px; font-size: 13px; color: var(--text); line-height: 1.35;
  background: none; border: 0; border-radius: var(--r-sm); cursor: pointer;
}
.mappick__results button:hover { background: var(--surface-3); }
.mappick__note { font-size: 12px; color: var(--text-3); }
.mappick__fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

.avatar {
  width: 32px; height: 32px; flex: 0 0 32px; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-content: center; font-size: 12px; font-weight: 650;
  border: 1px solid var(--brand-border); overflow: hidden;
}
.avatar--sm { width: 24px; height: 24px; flex-basis: 24px; font-size: 10px; }
.avatar--lg { width: 48px; height: 48px; flex-basis: 48px; font-size: 17px; }

/* --------------------------------------------------------------------------
   10. Modal & drawer
   -------------------------------------------------------------------------- */
.modal-root { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 16px; }
.modal-root__scrim { position: absolute; inset: 0; background: var(--overlay); backdrop-filter: blur(2px); }
.modal {
  position: relative; background: var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--shadow-xl); width: 100%; max-width: 520px;
  max-height: calc(100dvh - 32px); display: flex; flex-direction: column;
  animation: fadeUp .18s ease both; border: 1px solid var(--border);
}
.modal--lg { max-width: 780px; }
.modal--xl { max-width: 1080px; }
.modal__header { display: flex; align-items: center; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--border); }
.modal__title { font-size: 15.5px; font-weight: 620; flex: 1; }
.modal__body { padding: 18px; overflow-y: auto; flex: 1; }
.modal__footer { display: flex; gap: 8px; justify-content: flex-end; padding: 14px 18px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--r-lg) var(--r-lg); }

.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(560px, 100vw);
  background: var(--surface); box-shadow: var(--shadow-xl); z-index: 100;
  display: flex; flex-direction: column; animation: slideIn .22s cubic-bezier(.32,.72,0,1) both;
  border-left: 1px solid var(--border);
}
.drawer--wide { width: min(880px, 100vw); }

/* --------------------------------------------------------------------------
   11. Toasts
   -------------------------------------------------------------------------- */
/* A toast is the only place an operator is told a thing failed, and it appears
   in the corner of a screen they are not looking at. It is sized to be read
   from a normal working distance without leaning in — the old 12.5px body text
   in a 400px box was being missed, and a refused delivery that nobody notices
   is a refused delivery that gets tried again.

   An error toast does not expire at all; see `toast()` in store.js. It is
   closed by its ✕, and touching it stops the others expiring too, so a message
   somebody has reached for stays put. */
.toasts {
  position: fixed; bottom: 22px; right: 22px; z-index: 200;
  display: flex; flex-direction: column-reverse; gap: 12px; max-width: min(520px, calc(100vw - 44px));
}
.toast {
  display: flex; align-items: flex-start; gap: 13px;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 5px solid var(--text-3);
  border-radius: var(--r); padding: 16px 18px; box-shadow: var(--shadow-lg);
  animation: fadeUp .2s ease both;
}
.toast--success { border-left-color: var(--success); }
.toast--error   { border-left-color: var(--danger); }
.toast--warning { border-left-color: var(--warning); }
.toast--info    { border-left-color: var(--info); }
.toast__icon { flex: 0 0 22px; margin-top: 1px; }
.toast__icon svg { width: 22px; height: 22px; }
.toast--success .toast__icon { color: var(--success); }
.toast--error .toast__icon { color: var(--danger); }
.toast--warning .toast__icon { color: var(--warning); }
.toast--info .toast__icon { color: var(--info); }
.toast__body { flex: 1; min-width: 0; }
/* The only way to close one. It has to be reachable without being in the way of
   the text, so it sits in the corner at full tap size and takes its colour from
   the toast only on hover. */
.toast__close {
  flex: none; align-self: flex-start; margin: -4px -6px 0 0;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: var(--r-full);
  padding: 0; background: none; border: 0; color: var(--text-4); cursor: pointer;
}
.toast__close > svg { flex: none; }
.toast__close:hover { background: var(--surface-3); color: var(--text); }
.toast__title { font-size: 16px; font-weight: 750; letter-spacing: -.01em; }
/* The message itself is the part that has to be read, so it is not the muted
   secondary text it used to be — full contrast, and bold enough to carry. */
.toast__text {
  font-size: 15px; font-weight: 550; line-height: 1.4; color: var(--text);
  margin-top: 3px; word-break: break-word;
}

/* --------------------------------------------------------------------------
   12. States: empty, loading, skeleton
   -------------------------------------------------------------------------- */
.empty { text-align: center; padding: 44px 20px; }
.empty__icon {
  width: 44px; height: 44px; margin: 0 auto 12px; color: var(--text-4);
  background: var(--surface-3); border-radius: var(--r-md); display: grid; place-content: center;
}
.empty__icon svg { width: 22px; height: 22px; }
.empty__title { font-size: 14.5px; font-weight: 600; }
.empty__text { font-size: 13px; color: var(--text-3); margin-top: 4px; max-width: 380px; margin-inline: auto; }
.empty__action { margin-top: 14px; }

.spinner {
  width: 16px; height: 16px; border: 2px solid var(--border-2);
  border-top-color: var(--brand); border-radius: 50%; animation: spin .6s linear infinite;
  flex: 0 0 auto;
}
.spinner--lg { width: 26px; height: 26px; border-width: 2.5px; }
.loading-block { display: grid; place-items: center; padding: 44px; gap: 12px; color: var(--text-3); font-size: 13px; }

.skeleton {
  background: var(--surface-3); border-radius: var(--r-xs);
  position: relative; overflow: hidden; height: 14px;
}
.skeleton::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--surface) 70%, transparent), transparent);
  animation: shimmer 1.3s infinite;
}

/* --------------------------------------------------------------------------
   13. Tracking timeline
   -------------------------------------------------------------------------- */
.timeline { position: relative; padding-left: 26px; }
.timeline::before {
  content: ''; position: absolute; left: 7px; top: 8px; bottom: 8px;
  width: 2px; background: var(--border); border-radius: 2px;
}
.tl-item { position: relative; padding-bottom: 20px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item__dot {
  position: absolute; left: -26px; top: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--border-2); z-index: 1;
}
/* Same rule on the timeline's own dot, for the same reason: the event the
   document is sitting on should not be the faintest mark in the column. */
.tl-item--current .tl-item__dot {
  border-color: var(--brand); background: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-soft);
}
.tl-item--done .tl-item__dot { border-color: var(--success); background: var(--success); }
.tl-item--exception .tl-item__dot { border-color: var(--danger); background: var(--danger); }
.tl-item__title { font-size: 13.5px; font-weight: 580; }
.tl-item__meta { font-size: 12.5px; color: var(--text-3); margin-top: 2px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.tl-item__note { font-size: 12.5px; color: var(--text-2); margin-top: 5px; padding: 7px 10px; background: var(--surface-3); border-radius: var(--r-sm); }

/* Progress rail on the tracking page */
.rail { display: flex; align-items: center; gap: 0; margin: 6px 0 4px; }
.rail__step { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 7px; position: relative; }
.rail__step + .rail__step::before {
  content: ''; position: absolute; left: -50%; top: 13px; width: 100%; height: 2px;
  background: var(--border-2);
}
.rail__step.is-done + .rail__step::before,
.rail__step.is-done::before { background: var(--success); }
.rail__mark {
  width: 28px; height: 28px; border-radius: 50%; display: grid; place-content: center;
  background: var(--surface); border: 2px solid var(--border-2); color: var(--text-4);
  font-size: 12px; font-weight: 650; z-index: 1; position: relative;
}
.rail__step.is-done .rail__mark { background: var(--success); border-color: var(--success); color: #fff; }
/* The step the document is on is *filled*, not outlined.
   Outlined on white it was the palest mark on the rail — the one step somebody
   is actually looking for read as lighter than the ones already behind it, and
   at a glance the rail looked finished at the last green circle. Filled brand
   blue: distinct from the green behind it and from the red a failure paints,
   so the three states stay three colours rather than two colours and a hole.
   The halo stays, which is what says "here" rather than merely "another one". */
.rail__step.is-current .rail__mark {
  background: var(--brand); border-color: var(--brand); color: #fff;
  box-shadow: 0 0 0 4px var(--brand-soft);
}
.rail__step.is-failed .rail__mark { background: var(--danger); border-color: var(--danger); color: #fff; }
.rail__label { font-size: 11.5px; color: var(--text-3); text-align: center; font-weight: 500; }
.rail__step.is-current .rail__label { color: var(--brand); font-weight: 620; }
/* Who exactly, under the step name — the courier's name, the branch code, how
   many waybills. A custody rail that says "BRANCH" without saying which one
   has not answered the question it was put there to answer. */
.rail__who {
  font-size: 11px; color: var(--text-4); text-align: center;
  max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail__step.is-current .rail__who { color: var(--text-2); }

/* The four piece counts as a chain: promised → taken → counted in → booked.
   Each step carries the shortfall against the one before it, because the gaps
   are the whole reason the four numbers are kept apart. */
.count-flow { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.count-flow__arrow { color: var(--text-4); flex: 0 0 auto; }
.count-flow__step {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 9px 14px; border-radius: var(--r); background: var(--surface-2);
  border: 1px solid var(--border); min-width: 92px; position: relative;
}
.count-flow__step.is-pending { opacity: .5; }
.count-flow__n { font-size: 19px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; }
.count-flow__label { font-size: 11px; color: var(--text-3); text-align: center; }
.count-flow__short {
  position: absolute; top: -8px; right: -8px;
  background: var(--warning-soft); color: var(--warning); border: 1px solid var(--warning-border);
  border-radius: var(--r-full); padding: 1px 6px; font-size: 11px; font-weight: 620;
}

/* --------------------------------------------------------------------------
   14. Rate matrix editor
   -------------------------------------------------------------------------- */
.matrix-wrap { overflow: auto; max-height: 68vh; border: 1px solid var(--border); border-radius: var(--r); }
.matrix { border-collapse: separate; border-spacing: 0; font-size: 12.5px; }
.matrix th, .matrix td { border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: 0; }
.matrix thead th {
  position: sticky; top: 0; z-index: 3; background: var(--thead-bg);
  padding: 8px 10px; font-size: 11.5px; font-weight: 620; text-transform: uppercase;
  letter-spacing: .03em; color: var(--text-3); white-space: nowrap; text-align: center; min-width: 108px;
}
.matrix thead th:first-child, .matrix tbody th {
  position: sticky; left: 0; z-index: 4; background: var(--thead-bg);
  text-align: left; min-width: 128px;
}
.matrix thead th:first-child { z-index: 5; }
.matrix tbody th {
  z-index: 2; padding: 8px 10px; font-weight: 580; font-size: 12.5px;
  color: var(--text-2); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.matrix tbody tr:hover th { background: var(--surface-3); }
.matrix__cell {
  width: 100%; height: 34px; border: none; background: transparent; padding: 0 9px;
  font-size: 12.5px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text);
  transition: background var(--t-fast);
}
.matrix__cell:hover { background: var(--surface-2); }
.matrix__cell:focus { outline: 2px solid var(--brand); outline-offset: -2px; background: var(--brand-soft); }
.matrix__cell.is-changed { background: var(--warning-soft); font-weight: 620; }
.matrix td:last-child, .matrix th:last-child { border-right: none; }

/* --------------------------------------------------------------------------
   15. Charts (inline SVG)
   -------------------------------------------------------------------------- */
.chart { width: 100%; overflow: visible; }
.chart__grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3; }
.chart__axis { fill: var(--text-4); font-size: 10.5px; }
.chart__area { fill: url(#areaGrad); }
.chart__line { fill: none; stroke: var(--brand); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart__dot { fill: var(--surface); stroke: var(--brand); stroke-width: 2; }
.chart__bar { fill: var(--brand); transition: opacity var(--t-fast); }
.chart__bar:hover { opacity: .78; }

.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--text-2); }
.legend__item { display: inline-flex; align-items: center; gap: 6px; }
.legend__swatch { width: 9px; height: 9px; border-radius: 2.5px; flex: 0 0 9px; }

.bar-row { display: grid; grid-template-columns: minmax(80px, 130px) 1fr auto; gap: 10px; align-items: center; padding: 6px 0; font-size: 12.5px; }
.bar-track { height: 7px; background: var(--surface-3); border-radius: var(--r-full); overflow: hidden; }
.bar-fill { height: 100%; border-radius: var(--r-full); background: var(--brand); transition: width var(--t); }

/* --------------------------------------------------------------------------
   16. Auth screen
   -------------------------------------------------------------------------- */
.auth { min-height: 100dvh; display: grid; grid-template-columns: 1fr 1fr; }
@media (max-width: 900px) { .auth { grid-template-columns: 1fr; } .auth__aside { display: none; } }

.auth__panel { display: grid; place-items: center; padding: 32px 24px; background: var(--surface); }
.auth__form { width: 100%; max-width: 360px; }
.auth__brand { display: flex; align-items: center; gap: 11px; margin-bottom: 28px; }
.auth__title { font-size: 22px; font-weight: 650; letter-spacing: -.02em; }
.auth__sub { color: var(--text-3); font-size: 13.5px; margin-top: 5px; margin-bottom: 22px; }

.auth__aside {
  /* The brand walked down into near-black navy. The two far stops are literals
     rather than `--brand-700`: they run darker than any interactive state
     should, and a gradient that bottoms out at the pressed-button colour looks
     like a mistake rather than a backdrop. */
  background: linear-gradient(150deg, var(--brand) 0%, #0a2452 55%, #05122c 100%);
  color: #fff; display: flex; flex-direction: column; justify-content: center;
  padding: 48px; position: relative; overflow: hidden;
}
.auth__aside::after {
  content: ''; position: absolute; inset: 0; opacity: .12;
  background-image: radial-gradient(circle at 1px 1px, #fff 1px, transparent 0);
  background-size: 26px 26px;
}
.auth__aside > * { position: relative; z-index: 1; }
.auth__aside h2 { font-size: 30px; font-weight: 680; letter-spacing: -.03em; line-height: 1.2; max-width: 460px; }
.auth__aside p { margin-top: 14px; opacity: .85; font-size: 15px; max-width: 440px; line-height: 1.6; }
.auth__points { margin-top: 32px; display: grid; gap: 13px; }
.auth__point { display: flex; align-items: flex-start; gap: 11px; font-size: 14px; opacity: .95; }
.auth__point svg { flex: 0 0 18px; margin-top: 2px; }

.alert {
  display: flex; gap: 9px; padding: 10px 12px; border-radius: var(--r-sm);
  font-size: 13px; border: 1px solid; align-items: flex-start;
}
.alert svg { flex: 0 0 16px; margin-top: 1px; }
.alert--error   { background: var(--danger-soft);  border-color: var(--danger-border);  color: var(--danger); }
.alert--success { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.alert--warning { background: var(--warning-soft); border-color: var(--warning-border); color: var(--warning); }
.alert--info    { background: var(--info-soft);    border-color: var(--info-border);    color: var(--info); }

/* --------------------------------------------------------------------------
   17. Misc components
   -------------------------------------------------------------------------- */
.tabs {
  display: flex; gap: 2px; border-bottom: 1px solid var(--border);
  /* Horizontal only. `overflow-x: auto` on its own makes `overflow-y` compute
     to `auto` as well, so the strip grew a vertical scrollbar the moment the
     horizontal one appeared and took a few pixels off the height it had to
     work with — a scrollbar over a row that is one line tall by definition,
     and one that hid half a tab label while it was there. */
  overflow-x: auto; overflow-y: hidden;
  /* A little more than the labels strictly need, so the horizontal bar has
     somewhere to sit. The tabs stretch to it (the default `align-items`) and
     centre their own text, which keeps each one's underline on the strip's
     bottom border where the active marker belongs. */
  min-height: 46px;
}
.tab {
  padding: 9px 13px; font-size: 13.5px; font-weight: 520; color: var(--text-3);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 620; }

.dropdown { position: relative; }
.dropdown__menu {
  position: absolute; right: 0; top: calc(100% + 5px); min-width: 190px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--shadow-lg); padding: 5px; z-index: 50; animation: fadeUp .13s ease both;
}
.dropdown__menu--up { top: auto; bottom: calc(100% + 5px); }
.dropdown__menu--left { right: auto; left: 0; }
.dropdown__item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 9px; border-radius: var(--r-xs); font-size: 13.5px;
  color: var(--text-2); text-align: left; transition: background var(--t-fast);
}
.dropdown__item:hover { background: var(--surface-3); color: var(--text); text-decoration: none; }
.dropdown__item--danger { color: var(--danger); }
.dropdown__item--danger:hover { background: var(--danger-soft); }
/* The same menu, opened at the pointer instead of under a trigger. `fixed`
   rather than `absolute`: the row it belongs to sits inside a scrolling table,
   and an absolutely-placed menu would ride away with it. The parent clamps the
   coordinates so a row at the bottom of the window still gets a whole menu. */
.context-menu {
  position: fixed; min-width: 210px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--shadow-lg); padding: 5px; z-index: 60; animation: fadeUp .1s ease both;
}
.dropdown__sep { height: 1px; background: var(--border); margin: 5px 0; }
.dropdown__header { padding: 8px 9px 6px; font-size: 11px; font-weight: 620; text-transform: uppercase; letter-spacing: .05em; color: var(--text-4); }

.search {
  position: relative; display: flex; align-items: center; flex: 1; max-width: 380px;
}
.search svg { position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-4); pointer-events: none; }
.search .input { padding-left: 33px; }
.search__clear { position: absolute; right: 8px; color: var(--text-4); display: grid; place-content: center; }
.search__clear:hover { color: var(--text); }

/* ── Type-ahead ─────────────────────────────────────────────────────────────
   A list of candidates under a search box, driven by the keyboard: ↑/↓ move
   `is-active`, Enter takes it. The active row is styled from the component's
   own index rather than :hover, so the highlight is in one place whether it
   arrived from the keyboard or the pointer. */
.suggest { position: relative; }
.suggest .search { max-width: none; }
.suggest .spinner { position: absolute; right: 10px; }
/* The count and the key hint sit outside the scrolling area: the hint is how
   you learn the list is keyboard-driven, and it is no use at the bottom of a
   list you have to scroll to reach. */
.suggest__list {
  position: absolute; left: 0; right: 0; top: calc(100% + 5px);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--shadow-lg); padding: 4px; z-index: 50;
  display: flex; flex-direction: column; max-height: 360px;
  animation: fadeUp .13s ease both;
}
/* Teleported to <body> and positioned against the field it belongs to. The
   list is otherwise clipped by any ancestor that scrolls — `.modal__body` is
   `overflow-y: auto`, so a list opened from the last row of a dialog was cut
   off at the footer and read as sitting behind it. An overflow clip is not a
   stacking question and no z-index fixes it; escaping the box does.

   Position, width and max-height are written inline by `anchored.js`; what is
   here is only what has to be unset for those to take. The z-index clears the
   modal's 100 — the list belongs to a field inside the dialog and must draw
   over it. */
.suggest__list--float { top: auto; right: auto; bottom: auto; z-index: 200; }
.suggest__scroll { overflow-y: auto; min-height: 0; }
.suggest__head, .suggest__foot {
  flex: none; padding: 6px 9px; font-size: 11.5px; color: var(--text-3);
}
.suggest__head { text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
/* The same heading, but inside the scrolling list rather than pinned above it —
   it marks where one group of suggestions ends and the next begins, so it has
   to scroll with them. */
.suggest__head--mid { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 8px; }
.suggest__foot { border-top: 1px solid var(--border); color: var(--text-4); }
.suggest__item {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px;
  width: 100%; padding: 7px 9px; border-radius: var(--r-xs); text-align: left;
  font-size: 13.5px; background: none; border: 0; cursor: pointer; color: inherit;
}
.suggest__item.is-active { background: var(--brand-soft); }
.suggest__awb { font-weight: 600; white-space: nowrap; }
/* The account's phone, leading its row in the customer picker — the column the
   account number used to hold. It is the identifier now, so it carries the
   weight the number carried, and for «Частное лицо» that is the literal truth:
   there is no e-mail, and the number is the only thing telling two Alishers
   apart.

   Deliberately **not** `mono`. A tabular face sets every digit on the same wide
   advance, and thirteen of those took a third of the list to say a number that
   reads perfectly well in the ordinary one. `font-variant-numeric: tabular-nums`
   keeps the column of numbers aligned down the list without buying the width. */
.suggest__tel {
  font-weight: 600; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* One line, always: a row that grows changes the height of every row below it
   and the arrow keys stop landing where the eye expects. */
.suggest__meta {
  display: flex; gap: 5px; min-width: 0;
  font-size: 12.5px; color: var(--text-2);
  white-space: nowrap; overflow: hidden;
}
.suggest__meta > :first-child { overflow: hidden; text-overflow: ellipsis; }
.suggest__meta > :last-child { flex: none; }

/* ── Pick list ──────────────────────────────────────────────────────────────
   Choosing one row from a list too long to read as a <select> and too short to
   deserve a table — a courier, inside a dialog. The row itself is the target
   rather than a radio beside it, so `is-selected` has to be unmistakable: tint
   plus a bar down the leading edge, which survives a colour-blind reader and a
   dark theme. The scroll is bounded so the confirm button stays on screen. */
.picklist { border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
.picklist__scroll { max-height: 320px; overflow-y: auto; }
.picklist__item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 12px; border: 0; border-bottom: 1px solid var(--border);
  background: none; color: inherit; text-align: left; cursor: pointer;
  transition: background var(--t-fast);
}
.picklist__item:last-child { border-bottom: none; }
.picklist__item:hover { background: var(--surface-2); }
.picklist__item.is-selected { background: var(--brand-soft); box-shadow: inset 3px 0 0 var(--brand); }
.picklist__name { display: block; font-weight: 560; }
.picklist__meta {
  display: block; font-size: 12px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.picklist__empty { padding: 20px 12px; text-align: center; color: var(--text-3); font-size: 13px; }

/* A ticket in the side panel of a waybill: number and status on one line, the
   subject under it, the kind and age under that. Full-width rows rather than a
   table — there are rarely more than a handful, and each is a link. */
.desk-row {
  display: block; padding: 9px 16px; border-bottom: 1px solid var(--border);
  color: inherit; transition: background var(--t-fast);
}
.desk-row:last-child { border-bottom: none; }
.desk-row:hover { background: var(--surface-2); }
.desk-row p { margin: 2px 0 0; }

.toolbar { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; padding: 12px 16px; border-bottom: 1px solid var(--border); }

/* A voided payment (085) stays on the list and out of every sum — struck
   through rather than hidden, because a correction is a thing that happened
   and the till it came out of has to show it. */
.table tbody tr.is-void { opacity: .55; }
.table tbody tr.is-void td { text-decoration: line-through; }
/* The badge and the reason badge beside it are not part of what was struck. */
.table tbody tr.is-void td:last-child,
.table tbody tr.is-void .badge { text-decoration: none; }

.stat-list { display: grid; gap: 0; }
.stat-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 8px 0; border-bottom: 1px dashed var(--border); font-size: 13px;
}
.stat-row:last-child { border-bottom: none; }
.stat-row__label { color: var(--text-3); }
.stat-row__value { font-weight: 580; font-variant-numeric: tabular-nums; text-align: right; }
.stat-row--total { border-top: 1px solid var(--border-2); border-bottom: none; padding-top: 10px; margin-top: 3px; font-size: 14.5px; }
.stat-row--total .stat-row__value { font-weight: 680; font-size: 16px; }

.kv { display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: 7px 16px; font-size: 13px; align-items: baseline; }
.kv dt { color: var(--text-3); }
.kv dd { font-weight: 520; word-break: break-word; }

.copyable { display: inline-flex; align-items: center; gap: 5px; }
.copyable__btn { color: var(--text-4); display: grid; place-content: center; padding: 2px; border-radius: var(--r-xs); }
.copyable__btn:hover { color: var(--brand); background: var(--brand-soft); }

.step-head { display: flex; gap: 8px; margin-bottom: 18px; flex-wrap: wrap; }
.step {
  display: flex; align-items: center; gap: 8px; padding: 7px 13px;
  border-radius: var(--r-full); background: var(--surface-3);
  font-size: 12.5px; font-weight: 520; color: var(--text-3); border: 1px solid transparent;
}
.step.is-active { background: var(--brand-soft); border-color: var(--brand-border); color: var(--brand); font-weight: 620; }
.step.is-done { background: var(--success-soft); color: var(--success); border-color: var(--success-border); }
.step__num {
  width: 19px; height: 19px; border-radius: 50%; background: currentColor;
  color: var(--surface); display: grid; place-content: center; font-size: 10.5px; font-weight: 700; flex: 0 0 19px;
}

/* --------------------------------------------------------------------------
   18. Utilities
   -------------------------------------------------------------------------- */
.row { display: flex; align-items: center; gap: 8px; }
.row--between { justify-content: space-between; }
.row--end { justify-content: flex-end; }
.row--wrap { flex-wrap: wrap; }
.row--start { align-items: flex-start; }
.col { display: flex; flex-direction: column; gap: 8px; }
.grow { flex: 1; min-width: 0; }

.gap-0 { gap: 0; } .gap-1 { gap: 4px; } .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; } .gap-5 { gap: 20px; }
.mt-0 { margin-top: 0; } .mt-1 { margin-top: 4px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mt-4 { margin-top: 16px; } .mt-5 { margin-top: 20px; }
.mb-1 { margin-bottom: 4px; } .mb-2 { margin-bottom: 8px; } .mb-3 { margin-bottom: 12px; } .mb-4 { margin-bottom: 16px; }
.p-0 { padding: 0; } .p-2 { padding: 8px; } .p-3 { padding: 12px; } .p-4 { padding: 16px; }

.text-xs { font-size: 11.5px; } .text-sm { font-size: 12.5px; } .text-base { font-size: 13.5px; } .text-lg { font-size: 16px; } .text-xl { font-size: 20px; }
/* `.text-muted` is what explanatory prose is written in across the app, so it
   reads at the hint step rather than at the ramp's third. `.text-faint` keeps
   `--text-4` — it is for a value that is present but secondary (a timestamp
   beside a name), not for a sentence explaining a panel. */
.text-muted { color: var(--text-hint); } .text-faint { color: var(--text-4); }
.text-brand { color: var(--brand); } .text-success { color: var(--success); }
.text-danger { color: var(--danger); } .text-warning { color: var(--warning); } .text-info { color: var(--info); }
.text-right { text-align: right; } .text-center { text-align: center; }
.font-medium { font-weight: 520; } .font-semibold { font-weight: 620; } .font-bold { font-weight: 700; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.tabular { font-variant-numeric: tabular-nums; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uppercase { text-transform: uppercase; letter-spacing: .04em; }

.hidden { display: none !important; }
@media (max-width: 768px) { .hide-sm { display: none !important; } }
@media (max-width: 1100px) { .hide-md { display: none !important; } }
/* Shown only where there is no mouse. For a control that duplicates something
   the pointer already offers — a row's kebab menu beside its right-click menu.
   Asked by capability rather than by width on purpose: an iPad Pro in landscape
   is wider than most laptops and still has nothing to right-click with, so a
   breakpoint would take the only affordance away from the device that needs it
   most. `hover` rules out a touchscreen, `pointer: fine` rules out a stylus. */
@media (hover: hover) and (pointer: fine) { .only-touch { display: none !important; } }

.divider { height: 1px; background: var(--border); margin: 14px 0; }
.divider--v { width: 1px; height: 20px; background: var(--border); }

.clickable { cursor: pointer; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

@media print {
  /* `.table-tools` joins the list now that it carries the filters and the
     primary action — controls nobody can use on paper. */
  .sidebar, .topbar, .page__actions, .toolbar, .table-tools,
  .pagination, .toasts { display: none !important; }
  body { background: #fff; }
  .card { border: 1px solid #ddd; box-shadow: none; break-inside: avoid; }
  .page { padding: 0; }
}

/* ── Master data: the entity rail ──────────────────────────────────────────
   Twenty-one entities. As a row of pills they wrapped to three lines and
   reordered themselves whenever the window changed, so finding one meant
   reading all of them. A fixed vertical rail keeps the order stable, which is
   what lets somebody learn where «Коды сканов» lives instead of hunting.

   Dark on purpose: this is chrome, not content, and giving it the same
   relationship to the page that the main sidebar has is what stops the eye
   reading it as another card. The colour is the brand navy at full strength
   rather than a neutral, so the two rails are siblings and not rivals.
   ------------------------------------------------------------------------ */
.md-page {
  display: grid;
  grid-template-columns: 208px minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}
.md-body { min-width: 0; }

.md-rail {
  display: flex; flex-direction: column;
  padding: 8px;
  background: #14263f;
  border-radius: var(--radius);
  /* Full height, not just as tall as its items: a stubby dark block floating
     beside a long table reads as a widget that happens to be there, and the
     whole point of the rail is that it is the room the master data lives in.
     Running the height of the screen makes it a wall, the way the main
     sidebar is.

     The arithmetic is the sticky top-bar (60px) plus the page's own 20px
     padding, and a 12px breath at the bottom. Sticky rather than fixed, so it
     stays put while a long table scrolls past and still scrolls internally
     when the entity list outgrows a short window. */
  position: sticky;
  top: calc(var(--topbar-h) + 12px);
  height: calc(100dvh - var(--topbar-h) - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
[data-theme="dark"] .md-rail { background: #0e1929; border: 1px solid var(--border); }

.md-rail__item {
  display: block; width: 100%; text-align: left;
  padding: 8px 11px; margin-bottom: 1px;
  background: none; border: 0; border-radius: 7px;
  font: inherit; font-size: 13px; line-height: 1.35;
  color: #a9bad2;
  cursor: pointer;
  transition: background var(--t), color var(--t);
}
.md-rail__item:hover { background: rgba(255, 255, 255, .07); color: #e8eef7; }
.md-rail__item.is-active {
  background: var(--brand); color: #fff; font-weight: 550;
}
[data-theme="dark"] .md-rail__item.is-active { background: var(--brand); color: #fff; }
.md-rail__item:focus-visible { outline: 2px solid #7aa5e8; outline-offset: -2px; }

/* The rail costs more than it earns on a narrow screen, where there is no
   second column to put it beside. It becomes a scrolling strip above the
   table — still one stable order, just read left to right. */
@media (max-width: 900px) {
  .md-page { grid-template-columns: 1fr; }
  .md-rail {
    flex-direction: row; position: static; height: auto;
    overflow-x: auto; overflow-y: hidden; gap: 2px; margin-bottom: 4px;
  }
  .md-rail__item { width: auto; white-space: nowrap; margin-bottom: 0; }
}

/* ── A row of radios ───────────────────────────────────────────────────────
   For a small, fixed set of choices that qualifies the fields under it — the
   pickup form's customer-type filter. A row rather than a dropdown because
   the whole choice is then readable without opening anything, and because
   four options in a select is a control that hides three of them.
   ------------------------------------------------------------------------ */
.radio-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
.radio {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--text-2); cursor: pointer; white-space: nowrap;
}
.radio input {
  width: 15px; height: 15px; margin: 0; flex: none;
  accent-color: var(--brand); cursor: pointer;
}
.radio:hover { color: var(--text); }
.radio input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* ── The pickup form's requester panel ─────────────────────────────────────
   Tinted by customer type, the way a waybill's two parties are tinted by
   `.party--from` / `.party--to`. Same reasoning: the colour is a label an
   operator reads before the words, and it is worth having on the one panel
   that decides how the rest of the form behaves.

   Only two of the three are coloured. «Частное лицо» is the ordinary case, and
   tinting the majority would mean nothing stood out.
   ------------------------------------------------------------------------ */
/* `.card.requester--x` rather than `.requester--x`: `.card` sets its own
   background, and matching both classes puts this above it on specificity
   instead of relying on which rule happens to come last. */
.requester { transition: background var(--t), border-color var(--t); }
.card.requester--eshop {
  background: var(--ctype-eshop);
  border-color: var(--ctype-eshop-border);
}
.card.requester--contract {
  background: var(--ctype-contract);
  border-color: var(--ctype-contract-border);
}

/* A radio row sitting beside a panel heading. Without the gap and the rule the
   first option reads as part of the title. */
.radio-row--after-title {
  padding-left: 16px;
  margin-left: 4px;
  border-left: 1px solid var(--border-2);
}
@media (max-width: 640px) {
  /* Wrapped under the heading, a left rule points at nothing. */
  .radio-row--after-title { padding-left: 0; margin-left: 0; border-left: 0; }
}

/* A menu item that states a rule rather than offering an action. Greyed and
   inert, but still listed: an item that disappears reads as a bug, one that is
   visibly unavailable reads as "not yet" or "not any more". */
.dropdown__item:disabled {
  opacity: .42;
  cursor: not-allowed;
}
.dropdown__item:disabled:hover { background: none; }

/* ── The disclosed filter panel ────────────────────────────────────────────
   Opened by the Filters button and rendered under the toolbar row. A class
   rather than an inline style, so any list that grows one of these picks up
   the same treatment instead of copying a colour.
   ------------------------------------------------------------------------ */
.toolbar--filters {
  background: var(--filters-bg);
  /* A gap before the column headings. Both are grey bands, and touching they
     read as one block — the panel's own border then looks like a rule inside
     it rather than its edge. The card's white shows through the margin, which
     is what separates them. */
  margin-bottom: 10px;
  border-bottom: 0;
}
.toolbar--filters .field__label,
.toolbar--filters .field__hint { color: var(--filters-ink); }
/* The hint stays subordinate to its label, but on this ground it needs more
   weight than the page's muted grey gives it. */
.toolbar--filters .field__hint { opacity: .72; }

/* A heading allowed to wrap. Column headings are `nowrap` by default, which is
   right for a register scanned across — but a one-word heading over a
   one-digit column then sets the column's width from the word, and two of them
   can push a small table into a scrollbar. The labels carry a soft hyphen so
   the break lands where a reader expects it rather than mid-syllable. */
/* `.table th.th-wrap`, not `.th-wrap`: the base rule is `.table th`, which
   outranks a lone class and would keep the heading on one line. */
.table th.th-wrap { white-space: normal; line-height: 1.25; hyphens: manual; }

/* A cell whose badge may break across two lines. A badge is `nowrap` by
   design — it is a label, not prose — but in a narrow table one long status
   sets the column's width and pushes everything else into a scrollbar. */
.table td.cell-wrap .badge { white-space: normal; text-align: center; }

/* ── An option and what it reveals ──────────────────────────────────────────
   A toggle on the booking form that turns a field on keeps that field on its
   own line, beside the toggle. The alternative — one pooled grid of every
   revealed field under a block of toggles — meant a field's position depended
   on which *other* boxes were ticked, so «Процент страховки» moved about and
   only its label said what it belonged to.

   `align-items: flex-end` lines the switch up with the inputs rather than with
   their labels, and the fixed width on the switch is what makes the fields of
   one row start where the fields of the row above it start. */
.opt-line { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
/* A rule between the lines, not around the block. These are separate services
   sold on one waybill — insurance, returned documents, returned cash, cash on
   delivery — and stacked with nothing but a gap between them they read as one
   list of ticks. Each is its own decision with its own price, and the rule says
   so. On the joins only, so the group still sits inside the two dividers that
   bracket it. Migration 086 moved the call-out and the door delivery onto the
   sender's and the consignee's panels, where each is a single `.opt-line` and
   the rule has nothing to join — which is right: there it is the first question
   about that party, not the fourth service in a list. */
.opt-line + .opt-line { border-top: 1px solid var(--border); padding-top: 8px; }
/* 370px is «Застраховать объявленную ценность» on one line with room to spare,
   plus the track and its gap. It was set by «Возврат документов / наличных
   отправителю», which 086 split into two shorter labels; the number is left
   alone because the point of a fixed width is that every row's fields start at
   the same x, and a label that wraps buys nothing back. */
.opt-line > .switch { flex: 0 0 auto; width: 370px; min-height: 36px; }
/* A row that is alone on its card, where the 370px above buys nothing.
   That width exists so the five services in «Дополнительно» start their fields
   at the same x — an alignment worth paying for in a column of five and
   meaningless for one. «Вызов курьера» and «Доставка курьером до двери» each
   sit by themselves at the foot of a party panel now, so the reservation only
   pushed «Стоимость вызова» 370px away from the box it belongs to. Here the
   switch takes the width of its own label and the fields follow it. */
.opt-line--solo > .switch { width: auto; }
.opt-line__fields { --opt-gap: 12px;
  display: flex; align-items: flex-end; gap: var(--opt-gap); flex-wrap: wrap; flex: 1 1 300px; }
/* Why a toggle is off, where the row would otherwise look like a mistake. It
   takes the place the revealed fields would have had, so the row keeps its
   height instead of the panel jumping as the note appears and goes. */
.opt-line__note { flex: 1 1 300px; margin: 0; align-self: center;
  font-size: 12px; color: var(--text-hint); }
/* Half the row each, whether the row reveals one field or two.
   Not `1 1 150px`: flexing sized every field to the space its own row happened
   to have, so «Адрес вызова» (sharing with «Стоимость вызова») came out at
   175px while «Адрес доставки», alone on its row, took 240 — two selects
   asking the same kind of question, in a column, visibly different widths.
   A fixed half means the first field starts and ends at the same x on every
   row and the second column is simply empty where a row has nothing to put in
   it. Expressed against the gap rather than in pixels so the two cannot drift
   apart, and so it holds at whatever width the panel ends up. */
.opt-line__fields .field { flex: 0 0 calc((100% - var(--opt-gap)) / 2); min-width: 140px; }
@media (max-width: 720px) {
  /* The switch stops reserving a column once the fields wrap under it anyway,
     and the fields go back to filling what is left: below this width the rows
     are stacked, so there is no column for them to line up with. */
  .opt-line > .switch { width: 100%; }
  .opt-line__fields .field { flex: 1 1 150px; }
}

/* ── The booking form's rail ────────────────────────────────────────────────
   The price and the packaging sold with it, riding beside a long form. Sticky
   on the column rather than on the price card: a card that stuck by itself
   would ride down over the panel beneath it, since sticky moves an element
   visually without moving the siblings laid out after it. The height bound and
   the inner scroll are what keep the total on screen when enough boxes are
   added to outgrow the window.

   It parks *below* the top bar, not at 16px from the viewport. The bar is
   sticky at `top: 0` and 60px tall, so a rail stuck at 16 slid under it and the
   price — the first line of the card — disappeared behind the page title. Same
   arithmetic as `.md-rail`: the bar's own height plus a breath. `100dvh` rather
   than `100vh` for the height bound, so a mobile browser's collapsing URL bar
   does not leave the card taller than the screen. */
@media (min-width: 1100px) {
  .form-rail {
    position: sticky; top: calc(var(--topbar-h) + 16px); align-self: start;
    max-height: calc(100dvh - var(--topbar-h) - 32px); overflow-y: auto;
  }
}

/* ── Branch select ──────────────────────────────────────────────────────────
   A combobox wearing an `.input`, so it lines up with the plain fields beside
   it in a form grid and inherits their focus ring rather than growing a second
   one. The clear button and the caret float over the input's own right-hand
   padding — a flex box around them would have had to reproduce the border, the
   hover and the invalid state, and would then drift from `.input` the next time
   one of those changed.

   `tabular-nums` rather than the mono face: the field holds one string —
   `2100 · Бухарский филиал` — so a monospaced font would set the branch name
   in it too, and Cyrillic prose in a code font is hard to read at 13px. Tabular
   figures fix the width of the digits alone, which is all that was wanted: the
   text stops shifting under the hand as a code is typed. */
.branch-select__box { position: relative; }
/* ── The quick-payment wall (094) ───────────────────────────────────────────
   A grid of tills chosen by their mark rather than read off a list. The tile
   *is* the label — every payment logo in this market is a wordmark, so a
   caption under it would print the name twice — which is also why the fallback
   below is drawn as a coloured plate with the name on it rather than as an
   icon: a till with no artwork degrades into a tile of the same shape instead
   of a hole in the row.
   `auto-fill`, not `auto-fit`: with three tills left after a filter, `auto-fit`
   collapses the empty tracks and stretches three plates across the whole
   dialog. */
.pay-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 10px; }
.pay-tile {
  padding: 6px; border: 2px solid var(--border); border-radius: var(--r);
  background: var(--surface); cursor: pointer; display: flex; align-items: center;
  justify-content: center; min-height: 60px;
  transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}
.pay-tile:hover { border-color: var(--brand-border); }
.pay-tile:active { transform: scale(.985); }
/* The chosen one is stated twice — a brand border and a ring — because the
   tiles are themselves brightly coloured and a border alone is lost among
   them. */
.pay-tile.is-on {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}
.pay-tile__logo { display: block; width: 100%; height: 44px; object-fit: contain; }
/* A mark published as white-on-transparent (096) — Click's is — needs
   something behind it or the wordmark disappears on a light tile. The chip is
   a fixed near-black rather than a theme token: it exists to make somebody
   else's artwork legible, and a colour that follows the theme would take the
   contrast away again in light mode, which is the case it is for. */
.pay-tile__logo--dark,
.pay-mark__img--dark { background: #14181f; border-radius: 8px; padding: 4px 8px; }
.pay-mark__img--dark { border-radius: 5px; padding: 3px 6px; }
.pay-tile__fallback {
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px; color: #fff; font-weight: 600; font-size: 14px;
  padding: 0 10px; text-align: center; line-height: 1.15;
}
/* The same mark at table size — the payments dialog's two lists. It replaced a
   colour dot and a name, and it carries no caption for the reason the tile
   does not: the artwork is a wordmark and the name would be printed twice.
   `max-width` matters more here than on a tile — a provider whose logo is long
   must not push the sums out of a compact row — and the fallback plate is
   capped and truncates rather than wrapping a till's name onto two lines and
   making one row taller than the rest. */
.pay-mark { display: inline-flex; align-items: center; vertical-align: middle; min-width: 0; }
.pay-mark__img { display: block; height: 24px; width: auto; max-width: 104px; object-fit: contain; }
.pay-mark__fallback {
  display: inline-flex; align-items: center; height: 24px; max-width: 130px;
  padding: 0 8px; border-radius: 5px; background: var(--text-4);
  color: #fff; font-size: 11px; font-weight: 600; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

/* The sum on the quick dialog is the one figure the operator checks before
   pressing a button they cannot take back, so it is read at a glance. */
.input--lg { font-size: 20px; font-weight: 620; height: 46px; font-variant-numeric: tabular-nums; }

/* The picker with a «+» beside it — see components/quick-customer.js.
   A modifier rather than a rule on «.suggest», because the same class dresses
   the branch picker and half a dozen plain search boxes, none of which has a
   button to lay out. The box takes the room the button does not; «min-width: 0»
   is what lets it, since a flex item will otherwise refuse to shrink below the
   width of its own content and push the button out of the field's cell. */
.suggest--with-add { display: flex; align-items: center; gap: 6px; }
.suggest--with-add > .branch-select__box { flex: 1 1 auto; min-width: 0; }
.branch-select__input { padding-right: 52px; font-variant-numeric: tabular-nums; }
/* A customer is found by name, not by a code of fixed width, so the field that
   searches for one takes the ordinary figures back. It shares the rest of the
   branch picker's chrome because it is the same control with a different
   engine behind it — see components/customer-select.js. */
.branch-select__input--text { font-variant-numeric: normal; }
.branch-select__caret {
  position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
  color: var(--text-4); pointer-events: none;
}
.branch-select__clear {
  position: absolute; right: 28px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--r-full);
  /* `padding: 0` is not tidiness. A `<button>` carries the UA's own
     `padding: 1px 6px`, which leaves a 6px content box inside these 18 — and
     `svg { max-width: 100% }` at the top of this file then clamps the icon to
     that, not to its `width` attribute. The icon renders as a sliver. */
  padding: 0; background: none; border: 0; color: var(--text-4); cursor: pointer;
}
.branch-select__clear > svg { flex: none; }
.branch-select__clear:hover { background: var(--surface-3); color: var(--text-2); }
/* The list is no longer a descendant of the field — it is teleported to
   <body> so a dialog cannot clip it — so its width is measured rather than
   inherited. The floor that used to live here as `min-width: 260px` is the
   `minWidth` handed to `useAnchoredPopup`, and it is there for the same
   reason: a narrow filter cell would otherwise cut the branch name in half. */

/* ── Tariff versions ────────────────────────────────────────────────────────
   A DMN bundle is picked, not scanned down a column: what a person compares is
   status, date and label, three or four fields per version and rarely more than
   a dozen rows. So each is its own card — a bordered surface with a hover and
   an unmistakable selected state — rather than a rule-separated row, which read
   as a table without being one and left the selection invisible.

   The paddings are deliberately tighter than a `.card`'s 16px: these cards sit
   *inside* a card, and inherited padding at both levels pushed the list into a
   scroll before six versions were on screen. The actions live behind one `…`
   button so the row's own height is set by its text, not by a strip of buttons
   under it. */
.tariff-list { display: flex; flex-direction: column; gap: 6px; }
.tariff-row {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 9px 11px;
  border: 1px solid var(--border-2); border-radius: var(--r-md);
  background: var(--surface); cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.tariff-row:hover { background: var(--surface-2); border-color: var(--border-strong); }
/* Tint plus a bar down the leading edge — the picklist's rule, for the picklist's
   reason: the row is the target, so the choice has to survive a dark theme and a
   colour-blind reader. `inset` rather than a real border, which would shift the
   text by 3px on selection. */
.tariff-row.is-selected {
  background: var(--brand-soft); border-color: var(--brand-border);
  box-shadow: inset 3px 0 0 var(--brand);
}
.tariff-row__main { min-width: 0; flex: 1; }
.tariff-row__head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.tariff-row__name { font-weight: 600; }
.tariff-row__meta {
  margin: 3px 0 0; font-size: 12px; line-height: 1.45; color: var(--text-3);
}
/* The menu button keeps its place while the row's text wraps beside it. */
.tariff-row__actions { flex: none; margin-top: -2px; }

/* The website, the bot and the customer cabinet at the foot of the sidebar —
   links out of the application, so they carry a small arrow-out mark and sit
   above the collapse control, separated from it by the same hairline the
   footer already draws. Collapsed to icons, the mark goes with the label. */
.nav__outlinks { display: flex; flex-direction: column; gap: 1px; padding-bottom: 6px; margin-bottom: 6px;
  border-bottom: 1px solid var(--border); }
.nav__link--out { font-size: 13px; }
.nav__out-mark { flex: 0 0 12px; color: var(--text-4); opacity: .8; }
.sidebar--collapsed .nav__out-mark { display: none; }
