/* =============================================================================
   views.css — the view-level components the design system (app.css) did not
   cover: club header, stat rows, feed post, purchase row, forms, chips, admin
   panels. Same vocabulary, same tokens — never a hard-coded hex.
   Loaded AFTER app.css so it can lean on the shell/reset defined there.
============================================================================= */

/* ── page header ─────────────────────────────────────────────────────────── */
.topbar {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-2) 0 var(--w2b-space-4);
}
.topbar__avatar {
  width: 46px;
  height: 46px;
  flex: none;
  border-radius: 0;
  background: var(--w2b-gold-fill);
  color: var(--w2b-ink);
  display: grid;
  place-items: center;
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-md);
  box-shadow: var(--w2b-shadow-2);
}
.topbar__meta { min-width: 0; flex: 1; }
.topbar__name {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-md);
  letter-spacing: var(--w2b-ls-tight);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.topbar__sub {
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-xs);
  margin-top: 2px;
}
.topbar__aside { display: flex; align-items: center; gap: var(--w2b-space-2); }

/* ── bell (notifications trigger) ────────────────────────────────────────── */
.bell {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 0;
  border: 1px solid var(--w2b-line);
  background: var(--w2b-surface-1);
  color: var(--w2b-text);
  font-size: var(--w2b-fs-md);
  display: grid;
  place-items: center;
  cursor: pointer;
}
.bell:active { transform: scale(0.94); }
.bell .notif-badge { position: absolute; top: -4px; right: -4px; }

/* ── cashback panel ───────────────────────────────────────────────────────
   Read top to bottom rather than left to right: what it is, how much, how far
   along. The ring that used to sit on the left took a third of the panel's
   width and pushed the rule text into a four-line column beside it. */
.wallet {
  display: block;
  padding: var(--w2b-space-3);
  border-radius: var(--w2b-radius-card);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  box-shadow: none;
}
.wallet__body { min-width: 0; }
.wallet__amount {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-regular);
  font-size: var(--w2b-fs-2xl);
  line-height: 1;
  letter-spacing: var(--w2b-ls-tight);
  color: var(--w2b-text);
  margin-top: 3px;
}
.wallet .meter { margin-top: var(--w2b-space-3); }
.wallet__hint {
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-xs);
  margin-top: var(--w2b-space-2);
  line-height: var(--w2b-lh-snug);
}
.wallet__actions { margin-top: var(--w2b-space-3); display: flex; gap: var(--w2b-space-2); }

/* ── stat grid ───────────────────────────────────────────────────────────── */
.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--w2b-space-2);
}
.stat {
  padding: var(--w2b-space-3);
  border-radius: var(--w2b-radius-md);
  background: var(--w2b-surface-1);
  box-shadow: var(--w2b-inset-hairline);
  text-align: center;
}
.stat__value {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-md);
  letter-spacing: var(--w2b-ls-tight);
}
.stat__label {
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  margin-top: var(--w2b-space-1);
}

/* ── badge strip (gamification) ──────────────────────────────────────────── */
.badge-strip { display: flex; flex-wrap: wrap; gap: var(--w2b-space-2); }
.badge-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--w2b-space-1);
  padding: 6px 10px;
  border-radius: 0;
  background: var(--w2b-surface-1);
  box-shadow: var(--w2b-inset-hairline);
  font-size: var(--w2b-fs-xs);
  color: var(--w2b-text-muted);
}
.badge-chip--locked { opacity: 0.42; filter: grayscale(1); }

/* ── channel chips ───────────────────────────────────────────────────────── */

/* ── стрічка: a channel post is a photograph and a caption ────────────────────
   This tab IS the Telegram channel, and it was drawn as a table row: a 46px
   thumbnail on the left of a headline. The whole reason a client follows the
   channel is the photograph, so the photograph is the card and the caption sits
   under it, the way it does in the client the post was written in.            */
.post {
  display: block;
  padding: 0;
  border-radius: var(--w2b-radius-card);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  box-shadow: none;
  overflow: hidden;
}
/* Consecutive posts used to share their rules and read as one column of paper:
   a −13px pull against the stack's 14px gap left exactly the 1px of shared
   border. It read as one continuous sheet, and the line where one publication
   ended and the next began stopped being visible at all. A few pixels separate
   them without turning the column into a stack of floating boxes. */
.stack > .post + .post { margin-top: calc(-1 * var(--w2b-space-3) + 4px); }

/* An album is a scroll-snap strip — a real gallery for six lines of CSS and no
   script. The aspect sits on the STRIP, not on each picture, so every post in
   the feed has media of exactly the same height whether it carries one photo
   or four; with more than one, the next one peeks in and says «swipe». */
.post__gallery {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  aspect-ratio: 4 / 5;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  background: var(--w2b-surface-3);
}
.post__gallery::-webkit-scrollbar { display: none; }
.post__gallery--multi { grid-auto-columns: 88%; }
.post__gallery img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  scroll-snap-align: start;
}

.post__body { min-width: 0; padding: var(--w2b-space-3); }
.post__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--w2b-space-2);
  font-size: var(--w2b-fs-2xs);
  color: var(--w2b-text-subtle);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
}
.post__when { flex: none; }
/* pre-line, because a channel caption is written with its own line breaks and
   collapsing them turns a three-line announcement into a paragraph. */
.post__caption {
  margin-top: var(--w2b-space-2);
  font-size: var(--w2b-fs-sm);
  line-height: var(--w2b-lh-body);
  color: var(--w2b-text);
  white-space: pre-line;
  overflow-wrap: anywhere;
}
.post__caption a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.post__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--w2b-space-3);
  margin-top: var(--w2b-space-3);
}
.post__price {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-regular);
  font-size: var(--w2b-fs-md);
  color: var(--w2b-text);
}

/* ── purchase row ────────────────────────────────────────────────────────── */
.row {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-3) var(--w2b-space-4);
  border-radius: var(--w2b-radius-md);
  background: var(--w2b-surface-1);
  box-shadow: var(--w2b-inset-hairline);
}
.row__icon {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--w2b-radius-sm);
  background: var(--w2b-surface-3);
  display: grid;
  place-items: center;
  font-size: 18px;
}
.row__body { min-width: 0; flex: 1; }
.row__title {
  font-weight: var(--w2b-fw-medium);
  font-size: var(--w2b-fs-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row__sub { color: var(--w2b-text-subtle); font-size: var(--w2b-fs-xs); margin-top: 2px; }
.row__amount {
  flex: none;
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-sm);
  text-align: right;
}
.row__amount span { display: block; color: var(--w2b-text-subtle); font-size: var(--w2b-fs-2xs); font-weight: var(--w2b-fw-regular); }
.row--tap { cursor: pointer; }
.row--tap:active { transform: scale(0.99); }

/* ── forms ───────────────────────────────────────────────────────────────── */
.field { display: block; }
.field__label {
  display: block;
  font-size: var(--w2b-fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  color: var(--w2b-text-subtle);
  margin-bottom: var(--w2b-space-2);
}
.field__input,
.field__select,
.field__textarea {
  /* The box is stated as arithmetic rather than as a magic height, because the
     date field below has to reproduce it exactly. */
  --field-line: 1.3;
  --field-pad-y: 12px;
  width: 100%;
  padding: var(--field-pad-y) 14px;
  line-height: var(--field-line);
  border-radius: var(--w2b-radius-md);
  border: 1px solid var(--w2b-line);
  background: var(--w2b-surface-2);
  color: var(--w2b-text);
  font-family: var(--w2b-font-body);
  font-size: var(--w2b-fs-base);
  /* The caret defaults to a hairline of currentColor — dark ink on a pale field
     at 13px, which on a phone is indistinguishable from the text beside it, and
     somebody who cannot see where they are typing stops typing. In the accent
     it is the one mark on the field that is not the colour of a letter. */
  caret-color: var(--w2b-accent);
}
/* One height for every single-line control, computed from the values above:
   text + padding + border. Without it each control is as tall as its own
   default line box, which differs per type. */
.field__input,
.field__select {
  min-height: calc(1em * var(--field-line) + 2 * var(--field-pad-y) + 2px);
}
.field__textarea { min-height: 84px; resize: vertical; }

/* input[type=date] is the one control a browser refuses to size like the rest.
   WebKit renders the value inside a shadow-DOM box that carries its own margins
   and its own metrics, and on iOS the control also claims an intrinsic
   min-width it will not shrink below — so «Дата народження» came out both
   shorter and narrower than the text fields above it. Dropping the native
   appearance, releasing the min-width and flattening the inner box make it the
   same rectangle as every other field. */
.field__input[type="date"],
.field__input[type="time"],
.field__input[type="datetime-local"] {
  -webkit-appearance: none;
  appearance: none;
  min-width: 0;
  /* A fixed height, not min-height: the shadow-DOM value box and the calendar
     icon each contribute a pixel of their own, so the control came out 47.5px
     against the 45.5px of the phone field next to it. Stating the same
     arithmetic as those fields is what makes the two rectangles identical. */
  height: calc(1em * var(--field-line) + 2 * var(--field-pad-y) + 2px);
}
.field__input::-webkit-datetime-edit,
.field__input::-webkit-date-and-time-value {
  margin: 0;
  padding: 0;
  text-align: left;
  line-height: var(--field-line);
  min-height: calc(1em * var(--field-line));
}
.field__input::-webkit-calendar-picker-indicator {
  margin-inline-start: auto;
  margin-inline-end: 0;
  /* Its default padding is what made the control taller than its own text. */
  padding: 0;
  height: 1em;
  opacity: 0.55;
  cursor: pointer;
}
.field__input::placeholder, .field__textarea::placeholder { color: var(--w2b-text-subtle); }

/* The half of the inquiry the client does not write and cannot delete: the list
   they assembled upstairs, shown as it will arrive. It has to read as a
   QUOTATION rather than as a disabled form control — a greyed-out textarea says
   "broken", a quoted block says "this is already decided". Hence the rule down
   the side and the softer ground, and no border: nothing here is a box you
   could have typed in. */
.locked {
  border-radius: var(--w2b-radius-md);
  background: var(--w2b-surface-2);
  border-inline-start: 3px solid var(--w2b-accent);
  padding: var(--w2b-space-3) var(--w2b-space-4);
}
.locked__head {
  font-size: var(--w2b-fs-xs);
  color: var(--w2b-text-muted);
  margin-bottom: var(--w2b-space-2);
}
.locked__body {
  color: var(--w2b-text);
  font-size: var(--w2b-fs-sm);
  line-height: 1.5;
  /* The client may copy it; they may not edit it. */
  user-select: text;
}
/* The client's own Telegram face, in the box the initials used to occupy. The
   box keeps its own size and shape in every theme (it is restated three times
   further up), so the photograph is told to fill whatever that turns out to be
   rather than carrying dimensions of its own. */
/* Clipped by the box, so the photograph takes whatever corner the theme in
   force gives the avatar — 14px here, square elsewhere — instead of laying a
   rectangle over a rounded one. */
.topbar__avatar { overflow: hidden; }
.topbar__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
}

/* «Запитує ціну» / «Цікавиться постами» — the same two headings the message
   carries, so the preview and the thing previewed have one shape. */
.locked__group {
  margin-bottom: 1px;
  color: var(--w2b-text-muted);
  font-size: var(--w2b-fs-xs);
  font-weight: 700;
}

/* An item name that still opens its post — used in the request history, where
   a line from three weeks ago is the only way back to the thing it names. */
.link { color: var(--w2b-accent); text-decoration: none; }
.link:hover { text-decoration: underline; }
.field__input.is-error, .field__select.is-error { border-color: var(--w2b-danger); }
.field__hint { color: var(--w2b-text-subtle); font-size: var(--w2b-fs-xs); margin-top: var(--w2b-space-2); }
/* minmax(0, 1fr), not 1fr: a bare `1fr` is minmax(auto, 1fr), so a control that
   refuses to shrink below its content — input[type=date] with its dd.mm.yyyy and
   its picker icon — widens its own column and the two halves stop matching. */
.form-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--w2b-space-3);
}
.form-grid--full { grid-template-columns: minmax(0, 1fr); }

/* ── admin panel ─────────────────────────────────────────────────────────── */
.panel {
  padding: var(--w2b-space-4);
  border-radius: var(--w2b-radius-card);
  background: var(--w2b-surface-1);
  box-shadow: var(--w2b-inset-hairline), var(--w2b-shadow-1);
}
.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--w2b-space-3);
  margin-bottom: var(--w2b-space-3);
}
.panel__title {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-base);
  letter-spacing: var(--w2b-ls-tight);
}
.panel__note { color: var(--w2b-text-subtle); font-size: var(--w2b-fs-xs); line-height: var(--w2b-lh-body); }

.seg { display: flex; gap: var(--w2b-space-1); padding: 4px; border-radius: 0; background: var(--w2b-surface-2); }
.seg__btn {
  flex: 1;
  padding: 8px 10px;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--w2b-text-muted);
  font-size: var(--w2b-fs-xs);
  font-weight: var(--w2b-fw-medium);
  cursor: pointer;
}
.seg__btn.is-active { background: var(--w2b-surface-3); color: var(--w2b-text); }

.report { white-space: pre-wrap; font-size: var(--w2b-fs-sm); line-height: var(--w2b-lh-body); color: var(--w2b-text-muted); }

/* ── misc utilities ──────────────────────────────────────────────────────── */
.muted { color: var(--w2b-text-muted); font-size: var(--w2b-fs-sm); line-height: var(--w2b-lh-body); }
.divider { height: 1px; background: var(--w2b-line); margin: var(--w2b-space-2) 0; }
.inline { display: flex; align-items: center; gap: var(--w2b-space-2); flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: var(--w2b-space-3); }
.grow { flex: 1; min-width: 0; }
.right { text-align: right; }
.pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 0;
  background: var(--w2b-surface-2);
  color: var(--w2b-text-muted);
  font-size: var(--w2b-fs-2xs);
  box-shadow: var(--w2b-inset-hairline);
}
.pill--ok { color: var(--w2b-ok-text); }
.pill--warn { color: var(--w2b-warn); }
.sheet__title {
  font-family: var(--w2b-font-display);
  font-weight: var(--w2b-fw-bold);
  font-size: var(--w2b-fs-md);
  letter-spacing: var(--w2b-ls-tight);
  margin-bottom: var(--w2b-space-4);
}

/* =============================================================================
   MAISON LAYER — the storefront look (Chanel / Hermès / D&G reference).
   Rules of the house:
     • ink on paper, one accent (orange) reserved for «в наявності»
     • straight edges, hairline rules, no glow, no gradient buttons
     • uppercase Jost with wide tracking for anything interactive,
       Cormorant Garamond for names and numbers
     • the photograph is the loudest element on the screen
============================================================================= */

/* ── type: display headings become serif, controls become wide caps ───────── */
/* A section heading is structure, not a headline. Set as a 24px serif with 32px
   of air above it, five of them cost ~370px on the «Знижки» tab — more than a
   third of the phone — to say «Промокоди» and «Покупки». It is now the rule a
   magazine uses to break a column: a small wide-tracked label and a hairline
   running to the margin. The display face is kept for the two things that are
   actually being sold — the name of the vitrine and the sum of money. */
.section-title {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-3);
  font-family: var(--w2b-font-body);
  font-weight: var(--w2b-fw-medium);
  font-size: var(--w2b-fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  color: var(--w2b-text-subtle);
  margin: var(--w2b-space-3) 0 0;
}
.section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--w2b-line);
}
.sheet__title {
  font-weight: var(--w2b-fw-regular);
  font-size: var(--w2b-fs-xl);
  letter-spacing: 0.01em;
}
.topbar__name { font-family: var(--w2b-font-display); font-weight: var(--w2b-fw-medium); }
.topbar__sub, .field__label, .stat__label {
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: var(--w2b-fs-2xs);
}
.stat__value, .wallet__amount, .row__amount { font-family: var(--w2b-font-display); }

/* ── buttons: rectangles, caps, hairlines ─────────────────────────────────── */
.btn {
  border-radius: 0;
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-weight: var(--w2b-fw-regular);
  font-size: var(--w2b-fs-xs);
  padding: 11px var(--w2b-space-4);
}
.btn--primary {
  background: var(--w2b-primary);
  color: var(--w2b-on-primary);
  box-shadow: none;
}
.btn--primary:hover { background: var(--w2b-primary-strong); box-shadow: none; }
.btn--ghost { border: 1px solid var(--w2b-line-strong); background: transparent; }
.btn--ghost:hover { background: var(--w2b-surface-3); }
.btn:active { transform: none; opacity: 0.72; }

/* ── the search field: one rule under the text, nothing else ──────────────── */
.search {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-2) 0;
  border-bottom: 1px solid var(--w2b-line-strong);
}
.search__icon { flex: none; width: 16px; height: 16px; stroke: var(--w2b-text-subtle); fill: none; stroke-width: 1.5; }
.search__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  color: var(--w2b-text);
  font-family: var(--w2b-font-body);
  font-size: var(--w2b-fs-base);
  letter-spacing: 0.02em;
  padding: 6px 0;
}
.search__input::placeholder {
  color: var(--w2b-text-subtle);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: var(--w2b-fs-2xs);
}
.search__input:focus { outline: none; }
.search__clear {
  flex: none;
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-sm);
  padding: 4px;
}

/* ── a channel row in the admin office, with its sync controls ─────────────── */
.row--sync { align-items: flex-start; flex-wrap: wrap; }
.row--sync .row__body { min-width: 0; }
/* Three buttons and a channel name do not fit across 390px, and a flex row that
   cannot wrap simply pushes «Уся історія» off the edge of a page that hides its
   overflow — an admin control that exists but cannot be pressed. They take a
   line of their own and wrap within it. */
.row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--w2b-space-2);
  flex: 1 0 100%;
  margin-top: var(--w2b-space-2);
}
/* A button inside a row must not be what sets the row's height. */
.row > .btn:not(.btn--sm) { padding: 8px var(--w2b-space-3); }
.btn--sm { padding: 6px 10px; font-size: var(--w2b-fs-2xs); }
/* The running commentary of a sync: how many rounds have landed so far. It sits
   under the channel's own line because a deep backfill is many requests and a
   button that just looks stuck is indistinguishable from one that is. */
.row__sync {
  margin-top: var(--w2b-space-1);
  font-size: var(--w2b-fs-xs);
  color: var(--w2b-text-muted);
  font-variant-numeric: tabular-nums;
}
.row__sync.is-error { color: var(--w2b-danger); }

/* ── search + filters ─────────────────────────────────────────────────────── */
.searchbar { display: flex; align-items: stretch; gap: var(--w2b-space-3); }
.searchbar .search { flex: 1; min-width: 0; }
.filterbtn {
  flex: none;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0 10px;
  border: 0;
  border-bottom: 1px solid var(--w2b-line);
  background: none;
  color: var(--w2b-text-subtle);
  cursor: pointer;
}
.filterbtn svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round;
}
.filterbtn.is-on { color: var(--w2b-text); border-bottom-color: var(--w2b-text); }
.filterbtn__count {
  min-width: 16px;
  padding: 0 4px;
  border-radius: var(--w2b-radius-pill);
  background: var(--w2b-primary);
  color: var(--w2b-on-primary);
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}

/* Applied filters, as badges that remove themselves. The whole point: a client
   should never have to work out what is filtering the vitrine by spotting which
   chip looks darker somewhere in a scrolling row. */
.applied {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--w2b-space-2);
  margin-top: var(--w2b-space-3);
}
/* fbadge, not badge: `.badge` is already the loyalty tier chip, and it carries a
   ◆ in a ::before — reusing the name put a diamond in front of every filter. */
.fbadge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 9px 5px 11px;
  border: 1px solid var(--w2b-primary);
  border-radius: 0;
  background: var(--w2b-primary);
  color: var(--w2b-on-primary);
  font-size: var(--w2b-fs-xs);
  cursor: pointer;
}
.fbadge__x { font-size: var(--w2b-fs-sm); line-height: 1; opacity: 0.7; }
.fbadge:active { transform: scale(0.97); }
.applied__clear {
  border: 0;
  background: none;
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-xs);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* The chooser itself, inside the sheet. */
.fsheet { margin-bottom: var(--w2b-space-5); }
.fsheet__label {
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: var(--w2b-fs-2xs);
  color: var(--w2b-text-subtle);
  margin-bottom: var(--w2b-space-3);
}
.fsheet__grid { display: flex; flex-wrap: wrap; gap: var(--w2b-space-2); }
.fsheet__actions { display: flex; gap: var(--w2b-space-3); margin-top: var(--w2b-space-4); }
.fsheet__actions .btn { flex: 1; }

/* ── content filters: brand and category, one level below the catalogues ────
   Deliberately a different shape from .chip. The catalogue row is uppercase
   text with an underline; these are sentence-case hairline pills, so the eye
   reads two levels of filter instead of two rows of the same thing. The accent
   colour is not used here — it belongs to «Товари в наявності» alone. */
.fchip {
  flex: none;
  padding: 5px 11px;
  border: 1px solid var(--w2b-line);
  /* Square, like every other control in this design — the pill radius exists
     for the one counter badge and nothing else. */
  border-radius: 0;
  background: none;
  color: var(--w2b-text-muted);
  font-size: var(--w2b-fs-xs);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--w2b-dur-fast) var(--w2b-ease-out),
              border-color var(--w2b-dur-fast) var(--w2b-ease-out),
              background-color var(--w2b-dur-fast) var(--w2b-ease-out);
}
.fchip__count { margin-left: 6px; opacity: 0.5; }
.fchip.is-active {
  background: var(--w2b-primary);
  border-color: var(--w2b-primary);
  color: var(--w2b-on-primary);
}
.fchip.is-active .fchip__count { opacity: 0.65; }

/* «Показати ще» — the next page of the same selection. Full width, because it
   is the one thing to do at the bottom of a vitrine. */
.more {
  width: 100%;
  margin-top: var(--w2b-space-5);
}

/* ── the vitrine: photographs first ───────────────────────────────────────── */
.vitrine-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--w2b-space-3);
  margin-top: var(--w2b-space-2);
}
.vitrine-head__title {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-xl);
  font-weight: var(--w2b-fw-regular);
}
.vitrine-head__count {
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: var(--w2b-fs-2xs);
  color: var(--w2b-text-subtle);
}

/* The photograph keeps its 3:4 — it is the merchandise and nothing is taken
   from it. Everything around it gives ground: the gutters, the caption block
   and the button below each tile were together taller than a third of the
   picture they belonged to. */
.tiles {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--w2b-space-4) var(--w2b-space-2);
}
.tile { display: flex; flex-direction: column; background: none; }
.tile__media {
  position: relative;
  aspect-ratio: 3 / 4;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  overflow: hidden;
}
.tile__media img { width: 100%; height: 100%; object-fit: cover; }
.tile__chan {
  position: absolute;
  top: 0; left: 0;
  padding: 4px 7px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--w2b-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 8px;
}
.tile--stock .tile__chan { background: var(--w2b-accent); color: #fff; }
.tile__body { padding: var(--w2b-space-2) 0 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
.tile__title {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-md);
  font-weight: var(--w2b-fw-regular);
  line-height: 1.15;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Price and article code shared no information and each took a line of its
   own. On one baseline they read as a single caption: what it is, what it
   costs. */
.tile__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--w2b-space-2);
  margin-top: 1px;
}
.tile__art {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 9px;
  color: var(--w2b-text-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tile__price { font-family: var(--w2b-font-display); font-size: var(--w2b-fs-base); white-space: nowrap; }
.tile__btn {
  margin-top: var(--w2b-space-2);
  width: 100%;
  padding: 8px;
  border: 1px solid var(--w2b-text);
  background: none;
  color: var(--w2b-text);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: 9px;
}
.tile__btn:hover { background: var(--w2b-text); color: var(--w2b-on-primary); }
.tile__btn.is-in {
  border-color: var(--w2b-line-strong);
  color: var(--w2b-text-subtle);
  background: none;
}

/* ── примірочна ───────────────────────────────────────────────────────────── */
.nav__btn--cart { position: relative; }
.nav__badge {
  position: absolute;
  top: 4px;
  left: 50%;
  margin-left: 7px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--w2b-radius-pill);
  background: var(--w2b-accent);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}
.fit-row__thumb {
  width: 50px; height: 63px;
  flex: none;
  background: var(--w2b-surface-3);
  display: flex; align-items: center; justify-content: center;
  font-size: 26px;
  overflow: hidden;
}
.fit-row__thumb img { width: 100%; height: 100%; object-fit: cover; }
.coupon {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-3);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-text);
}
.coupon__badge {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-xl);
  font-weight: var(--w2b-fw-regular);
  line-height: 1;
}
.btn--send { width: 100%; padding: 14px; font-size: var(--w2b-fs-xs); }

/* ── the campaign builder ─────────────────────────────────────────────────────
   Presets are cards rather than a list because they are chosen by recognising
   one, not by reading all of them: the emoji and the dates do most of the work,
   and the line underneath says what the campaign is FOR — a discount nobody can
   state the reason for is one nobody can decide to stop. */
.presets {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--w2b-space-2);
  margin-bottom: var(--w2b-space-3);
}
.preset {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--w2b-space-3);
  text-align: left;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  cursor: pointer;
}
.preset:active { background: var(--w2b-surface-3); }
.preset__emoji { font-size: 18px; line-height: 1; }
.preset__name {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-base);
  line-height: 1.15;
  margin-top: 2px;
}
.preset__when {
  font-size: var(--w2b-fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--w2b-text-subtle);
  font-variant-numeric: tabular-nums;
}
.preset__why {
  margin-top: 4px;
  font-size: var(--w2b-fs-xs);
  line-height: 1.35;
  color: var(--w2b-text-muted);
}

/* How many people the conditions currently reach. Sticky, because the number
   is the answer to every edit made below it and scrolling away from it is what
   makes somebody save a campaign aimed at nobody. */
.reach {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-2) var(--w2b-space-3);
  background: var(--w2b-primary);
  color: var(--w2b-on-primary);
}
.reach__count {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-md);
  flex: none;
  font-variant-numeric: tabular-nums;
}
/* Nobody is not an error — it is a fact the owner has to see before saving. */
.reach__count--bad { color: #e8836a; }
.reach__terms {
  font-size: var(--w2b-fs-2xs);
  line-height: 1.3;
  opacity: 0.75;
}

/* ── the confirmation card: a person, not a receipt ───────────────────────────
   A wish list has just left the client's hands and they cannot see where it
   went. A name over a photograph answers that; «заявку надіслано» does not.

   The avatar is a CIRCLE, and that is not an inconsistency with the square
   corners everywhere else. Square is for furniture — panels, chips, buttons.
   A portrait is a portrait: cropping a face into a rectangle is one of the
   things that makes a shop look like a database. */
.sheet--center { align-items: center; padding: var(--w2b-space-4); }
.sheet--center .sheet__panel {
  max-width: 340px;
  padding: var(--w2b-space-6) var(--w2b-space-5) var(--w2b-space-5);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  box-shadow: var(--w2b-shadow-3);
  animation: w2b-thanks-in var(--w2b-dur-slow) var(--w2b-ease-out);
}
/* No drag handle: this is not something you pull, it is something you read. */
.sheet--center .sheet__panel::before { display: none; }
@keyframes w2b-thanks-in {
  from { transform: scale(0.96); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}

.thanks { text-align: center; }
.thanks__avatar {
  position: relative;
  width: 76px; height: 76px;
  margin: 0 auto var(--w2b-space-3);
  border-radius: 50%;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--w2b-surface-3);
  /* A hairline held one pixel off the photograph — the way a framed portrait
     sits inside its mount, rather than a border drawn onto the face. */
  box-shadow: 0 0 0 1px var(--w2b-surface-1), 0 0 0 2px var(--w2b-line-strong);
}
/* Absolutely positioned, and that is not laziness — it is the only form that
   holds. The parent is a grid (to centre the initials fallback), and inside a
   grid area a percentage height on a replaced element is treated as indefinite:
   a 200×300 portrait with `height: 100%` computed to 76×114 and the circle
   clipped a band across the middle of it, chin and forehead gone. `inset: 0`
   against a positioned parent is a definite 76×76 in every engine. */
.thanks__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Faces sit above the middle of a standing photograph; a dead-centre crop of
     one is a picture of a collarbone. */
  object-position: 50% 38%;
}
.thanks__initials {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-xl);
  color: var(--w2b-text-muted);
  letter-spacing: 0.04em;
}
.thanks__name {
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-lg);
  line-height: 1.2;
}
.thanks__role {
  margin-top: 2px;
  font-size: var(--w2b-fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  color: var(--w2b-text-subtle);
}
.thanks__text {
  margin: var(--w2b-space-4) 0 0;
  font-size: var(--w2b-fs-sm);
  line-height: var(--w2b-lh-body);
  color: var(--w2b-text-muted);
}
.thanks__note {
  margin-top: var(--w2b-space-3);
  padding: var(--w2b-space-2) var(--w2b-space-3);
  border: 1px solid var(--w2b-line);
  font-size: var(--w2b-fs-xs);
  color: var(--w2b-text);
}
.thanks .btn { width: 100%; margin-top: var(--w2b-space-4); }
.thanks__direct {
  display: inline-block;
  margin-top: var(--w2b-space-3);
  font-size: var(--w2b-fs-xs);
  color: var(--w2b-text-subtle);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ── empty state: a line of prose, centred, no box ────────────────────────── */
.empty {
  padding: var(--w2b-space-8) var(--w2b-space-4);
  text-align: center;
  color: var(--w2b-text-subtle);
  font-family: var(--w2b-font-display);
  font-size: var(--w2b-fs-md);
  background: none;
  border: 0;
}

/* ── wordmark + header ────────────────────────────────────────────────────── */
/* The two header rows are furniture: they repeat identically on all five tabs
   and carry no merchandise. Together they used to cost ~105px of a 844px
   phone — an eighth of every screen, every time. Everything here is sized to
   the smallest that still reads: the mark at 11px, the tagline at 10px, the
   avatar at 34px. Nothing was removed; it is simply no longer shouting. */
.wordmark {
  text-align: center;
  padding: 0 0 var(--w2b-space-2);
  border-bottom: 1px solid var(--w2b-line);
  margin-bottom: var(--w2b-space-2);
}
.wordmark__name {
  font-family: var(--w2b-font-body);
  font-weight: var(--w2b-fw-regular);
  text-transform: uppercase;
  letter-spacing: 0.42em;
  font-size: 11px;
  text-indent: 0.42em; /* optical centring against the trailing letterspace */
}
.wordmark__sub {
  margin-top: 2px;
  font-family: var(--w2b-font-display);
  font-style: italic;
  font-size: var(--w2b-fs-2xs);
  line-height: 1.2;
  color: var(--w2b-text-subtle);
}
.wordmark__row { gap: 8px; }
.brush { width: 24px; height: 16px; }

.topbar { padding: 0 0 var(--w2b-space-2); gap: var(--w2b-space-2); }
.topbar__avatar {
  border-radius: 0;
  width: 34px; height: 34px;
  background: var(--w2b-primary);
  color: var(--w2b-on-primary);
  font-weight: var(--w2b-fw-regular);
  font-size: var(--w2b-fs-xs);
  letter-spacing: 0.04em;
  box-shadow: none;
}
.topbar__name { font-size: var(--w2b-fs-md); letter-spacing: 0.01em; line-height: 1.2; }
.topbar__sub { color: var(--w2b-text-subtle); margin-top: 1px; }

/* The bell is a drawn glyph, not an emoji: a coloured emoji is the one thing
   that breaks a monochrome storefront. */
.bell {
  width: 32px; height: 32px;
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  background: none;
  display: grid;
  place-items: center;
  position: relative;
}
.bell svg { width: 16px; height: 16px; fill: none; stroke: var(--w2b-text); stroke-width: 1.4; }
.notif-badge {
  position: absolute;
  top: -6px; right: -6px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  border-radius: var(--w2b-radius-pill);
  background: var(--w2b-accent);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}

/* ── bottom navigation: wide caps, a rule instead of a glow ───────────────── */
.nav {
  background: color-mix(in srgb, var(--w2b-bg) 92%, transparent);
  border-top: 1px solid var(--w2b-line-strong);
}
.nav__btn {
  gap: 4px;
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--w2b-text-subtle);
}
.nav__btn svg { width: 19px; height: 19px; stroke-width: 1.2; }
.nav__btn.is-active { color: var(--w2b-text); }
.nav__btn.is-active svg { stroke-width: 1.6; }
.nav__btn.is-active::after {
  content: "";
  position: absolute;
  bottom: 4px;
  width: 14px; height: 1px;
  border-radius: 0;
  background: var(--w2b-text);
  box-shadow: none;
}

/* Emoji placeholders stay until the real channel photos arrive — desaturated
   so a demo screen still reads as one palette. Real photos are untouched. */
.ph { filter: grayscale(1); opacity: 0.32; }

.demobar { background: var(--w2b-surface-1); border-bottom: 1px solid var(--w2b-line); }
.demobar__label { letter-spacing: var(--w2b-ls-caps); }
.demobar__dot { background: var(--w2b-accent); }

/* ── panels & rows: paper cards with a hairline, no radius ────────────────── */
.panel {
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  box-shadow: none;
  padding: var(--w2b-space-3);
}
.panel__title { font-family: var(--w2b-font-display); font-weight: var(--w2b-fw-regular); font-size: var(--w2b-fs-md); }
.panel__head { margin-bottom: var(--w2b-space-2); }

/* A row is a line in a list, not a card. Ten purchases used to be ten separate
   boxes with sixteen pixels of paper between them; that is a filing cabinet,
   not a statement. They now share their rules — the stack gap and one of the
   two borders are pulled back — so consecutive rows read as one continuous
   list and a screen holds fourteen of them instead of nine. The list breaks by
   itself wherever a section heading interrupts the run. */
.row {
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  box-shadow: none;
  padding: var(--w2b-space-2) var(--w2b-space-3);
  gap: var(--w2b-space-3);
}
.stack > .row + .row { margin-top: calc(-1 * var(--w2b-space-3) - 1px); }
.row__title { font-weight: var(--w2b-fw-regular); }
.row__sub { line-height: 1.35; }
.row__icon {
  border-radius: 0;
  background: none;
  border: 1px solid var(--w2b-line);
  width: 30px; height: 30px;
  font-size: 15px;
}
/* Money lines up when the figures do, and a column of prices that jitters by a
   digit is the fastest way to make a list look homemade. */
.row__amount, .stat__value, .wallet__amount, .tile__price, .post__price {
  font-variant-numeric: tabular-nums;
}
.stat {
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  box-shadow: none;
  padding: var(--w2b-space-2) var(--w2b-space-1);
}
.stat__value { font-size: var(--w2b-fs-base); }
.stat__label { margin-top: 1px; }
.field__input, .field__select, .field__textarea {
  border-radius: 0;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line-strong);
}
/* A segmented control with six labels does not fit across 390px, and flex items
   do not shrink below their own word — so «Клієнти» and «Контент» ran off the
   right edge of a page that hides its overflow, which made two admin tabs
   unreachable on a phone. They now scroll, and each button still grows to fill
   the bar when there are only three of them. The inner padding is gone so an
   active segment is a full-height block of ink, not a floating chip. */
.seg {
  border-radius: 0;
  background: none;
  border: 1px solid var(--w2b-line);
  padding: 0;
  gap: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.seg::-webkit-scrollbar { display: none; }
.seg__btn {
  flex: 1 0 auto;
  border-radius: 0;
  padding: 9px 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 9px;
}
.seg__btn + .seg__btn { border-left: 1px solid var(--w2b-line); }
.seg__btn.is-active { background: var(--w2b-primary); color: var(--w2b-on-primary); }
.pill {
  border-radius: 0;
  background: none;
  border: 1px solid var(--w2b-line);
  box-shadow: none;
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: 9px;
}
.pill--ok { color: var(--w2b-ok-text); border-color: color-mix(in srgb, var(--w2b-ok) 45%, transparent); }
.pill--warn { color: var(--w2b-accent); border-color: color-mix(in srgb, var(--w2b-accent) 45%, transparent); }
.sheet__panel { border-radius: 0; }

/* ── discount cards on paper ──────────────────────────────────────────────
   A card is the one ink-dark island in a light app. Rather than restating a
   colour per element, the card RE-BINDS the palette tokens inside its own
   scope: every nested component (code chip, dashed rule, countdown) then reads
   the inverted values automatically. The bone VIP card re-binds them back.  */
.card {
  --w2b-text:        #ffffff;
  --w2b-text-muted:  rgba(255, 255, 255, 0.72);
  --w2b-text-subtle: rgba(255, 255, 255, 0.55);
  --w2b-line:        rgba(255, 255, 255, 0.18);
  --w2b-line-strong: rgba(255, 255, 255, 0.34);
  --w2b-gold-soft:   #ffffff;
  --w2b-danger-text: #e8836a;
  border-radius: 0;
  border-color: transparent;
  color: var(--w2b-text);
  box-shadow: none;
}
.card__title { font-weight: var(--w2b-fw-regular); }
.card__percent-num { font-weight: var(--w2b-fw-regular); }
.card__code { background: rgba(255, 255, 255, 0.08); border-radius: 0; }
.card__code-value { font-family: var(--w2b-font-body); }
.card__copy {
  border-radius: 0;
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  font-size: 9px;
}

/* The bone card: dark ink on light stock. */
.card--vip {
  --w2b-text:        #11100f;
  --w2b-text-muted:  #56514c;
  --w2b-text-subtle: #857e76;
  --w2b-line:        rgba(17, 16, 15, 0.14);
  --w2b-line-strong: rgba(17, 16, 15, 0.3);
  --w2b-gold-soft:   #11100f;
}
.card--vip .card__code { background: rgba(17, 16, 15, 0.06); }

/* Tiles: names vary in length, so the buttons must still line up. */
.tile__title { min-height: 2.3em; }
.tile__btn { margin-top: auto; }
.tile { height: 100%; }

/* ── the Ukrainian brush mark beside the wordmark ─────────────────────────
   Two hand-painted strokes rather than the 🇺🇦 emoji: an emoji is an OS glyph
   that renders differently on every device and reads as a sticker on a
   monochrome storefront. The strokes carry the flag's blue over yellow, with
   uneven, tapered edges so they look painted.                              */
.wordmark__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
/* ── the flag, painted ────────────────────────────────────────────────────
   Two brush sweeps beside the wordmark. Slight rotation and unequal opacity
   keep it looking applied by hand; a perfectly level, fully saturated pair of
   bars would read as a flag icon, which is not the register of the rest. */
.wordmark__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
.brush {
  /* 30% shorter than the painted version it replaces. */
  width: 29px;
  height: 19px;
  flex: none;
  transform: rotate(-4deg);
}
/* Translucent washes: multiply lets the lower edge of the blue darken where it
   overlaps the yellow, the way wet paint does. */
.wash { mix-blend-mode: multiply; }
.wash--blue { fill: #5aa7dd; opacity: 0.82; }
.wash--yellow { fill: #f3cf4e; opacity: 0.86; }

/* =============================================================================
   WAY2BUY 2026 — soft editorial commerce
   This final layer intentionally unifies the older component and maison layers.
   Product photography stays dominant; controls are tactile, calm and mobile-first.
============================================================================= */

body {
  background:
    radial-gradient(circle at 12% -8%, rgba(183, 113, 139, 0.12), transparent 30rem),
    radial-gradient(circle at 104% 26%, rgba(221, 190, 176, 0.16), transparent 28rem),
    var(--w2b-bg);
}

.app {
  padding-right: 14px;
  padding-left: 14px;
  padding-bottom: calc(104px + var(--w2b-safe-bottom));
}

.stack { gap: 14px; }

.wordmark {
  margin: 0 -2px 10px;
  padding: 8px 0 16px;
  border-bottom: 0;
}
.wordmark__row { gap: 0; }
.wordmark__lockup {
  position: relative;
  display: inline-block;
  padding-bottom: 17px;
}
/* The name is set in a script face and in LOWER CASE, so every letter runs at
   one height instead of two capitals standing over the rest of the word. A
   script is also why it is stretched rather than letter-spaced when it has to
   grow (see fitWordmark in app.js): tracking a script apart breaks the strokes
   that join its letters, and scaling keeps them joined. */
.wordmark__name {
  display: flex;
  align-items: baseline;
  color: var(--w2b-text);
  font-family: var(--w2b-font-script);
  font-size: 40px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0;
  text-transform: lowercase;
  text-indent: 0;
  /* Set by fitWordmark once both lines have been measured. */
  transform-origin: left center;
  will-change: transform;
}
/* The 2 stands a little shorter than the letters around it — it is a digit
   wearing a flag, not a letter, and at full height it читається as a break in
   the word. Baseline alignment keeps its foot on the line while the top drops. */
.wordmark__two {
  position: relative;
  display: inline-block;
  isolation: isolate;
  min-width: 0.5em;
  font-size: 0.72em;
  text-align: center;
}
.wordmark__sub {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The four words are separate spans with no whitespace between them, and
     `space-between` only separates them while there is width left over to
     distribute. In Ukrainian «ваша річ уже існує» fits and it looks deliberate;
     in English «your piece already exists» does not, the free space runs out,
     and the tagline renders as YOURPIECEALREADYEXISTS. A gap is the separation
     that does not depend on there being room to spare. */
  gap: 0.45em;
  margin-top: 0;
  color: var(--w2b-text-subtle);
  font-family: var(--w2b-font-body);
  font-size: 8px;
  /* The lockup is sized to whichever of the two lines is wider (fitWordmark),
     so this spans exactly the name above it and `space-between` puts the first
     and last letters of both lines on the same two vertical edges. */
  font-style: normal;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* The flag sits ACROSS the 2, centred on it in both directions. The 2 is the
   shortest glyph in the word and carries no ascender, so a wash laid over its
   middle reads as part of the mark rather than as something dropped on top of
   the word. */
.wordmark__two .brush {
  position: absolute;
  /* 0.295em, and the number is measured rather than guessed. The digit's ink
     runs from the baseline up (Sacramento's 2 has no descender), so its optical
     centre is half its own height above the baseline — which lands 0.295em
     below the top of the inline box, while the BOX centre sits 0.2em lower than
     that. Centring on the box put the wash across the foot of the 2 and into
     the tail of the y. In em of the digit, so it holds if the wordmark is ever
     set larger. */
  top: 0.295em;
  left: 50%;
  z-index: 2;
  /* In em of the digit, so the flag stays in proportion whatever the wordmark
     is set in — a fixed pixel flag looked like a sticker on the larger scripts. */
  width: 0.78em;
  height: 0.56em;
  max-width: none;
  opacity: 0.9;
  pointer-events: none;
  transform: translate(-50%, -50%) rotate(-5deg);
}

.topbar {
  gap: 12px;
  margin-bottom: 8px;
  padding: 12px;
  background: rgba(255, 253, 252, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 18px;
  box-shadow: var(--w2b-shadow-1);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.topbar__avatar {
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: linear-gradient(145deg, #3c2933, #251a20);
  font-family: var(--w2b-font-body);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  box-shadow: 0 8px 20px rgba(45, 32, 39, 0.2);
}
.topbar__name {
  font-family: var(--w2b-font-body);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.topbar__sub {
  margin-top: 3px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.1em;
}
.topbar__aside { gap: 7px; }
.locale {
  position: relative;
  display: inline-flex;
  border-radius: 11px;
}
.locale__face {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 34px;
  padding: 0 8px 0 10px;
  border: 1px solid var(--w2b-line);
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.58);
  color: var(--w2b-text);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
}
.locale__chev {
  width: 11px;
  height: 11px;
  fill: none;
  stroke: var(--w2b-text-subtle);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The control itself: full-size hit target, zero pixels of its own. `opacity`
   and not `display:none` — a hidden select is not focusable and would take the
   language switch away from the keyboard and from VoiceOver. */
.locale__select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  font: inherit;
  cursor: pointer;
}
.locale__select:focus-visible + .locale__face,
.locale:has(.locale__select:focus-visible) .locale__face {
  outline: 2px solid var(--w2b-focus-ring);
  outline-offset: 2px;
}
.locale:active .locale__face { background: var(--w2b-surface-3); }
/* The picker's own list is drawn by the OS, which does not inherit the page's
   colours — say them, or a dark system theme renders white text on white. */
.locale__select option { color: #000; background: #fff; }
.bell {
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 13px;
  background: var(--w2b-surface-3);
  transition: transform var(--w2b-dur-fast) var(--w2b-ease-out),
              background var(--w2b-dur-fast) var(--w2b-ease-out);
}
.bell:active { transform: scale(0.92); background: var(--w2b-accent-soft); }
.bell svg { stroke: var(--w2b-primary); stroke-width: 1.7; }

.nav {
  bottom: max(10px, var(--w2b-safe-bottom));
  width: calc(100% - 24px);
  height: 70px;
  padding: 5px 6px;
  padding-bottom: 5px;
  background: rgba(255, 253, 252, 0.9);
  border: 1px solid rgba(62, 40, 50, 0.1);
  border-radius: 22px;
  box-shadow: 0 16px 45px rgba(53, 33, 43, 0.17);
  backdrop-filter: blur(24px) saturate(1.35);
  -webkit-backdrop-filter: blur(24px) saturate(1.35);
}
.nav__btn {
  gap: 4px;
  border-radius: 16px;
  color: var(--w2b-text-subtle);
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: none;
}
.nav__btn svg { width: 21px; height: 21px; stroke-width: 1.6; }
.nav__btn.is-active {
  color: var(--w2b-primary);
  background: var(--w2b-surface-3);
}
.nav__btn.is-active svg { stroke-width: 2; }
.nav__btn.is-active::after { display: none; }
.nav__badge { top: 5px; background: var(--w2b-accent); box-shadow: 0 3px 8px rgba(166, 61, 97, 0.28); }

.search {
  gap: 10px;
  padding: 7px 13px;
  background: rgba(255, 253, 252, 0.86);
  border: 1px solid var(--w2b-line);
  border-radius: 15px;
  box-shadow: var(--w2b-shadow-1);
}
.search__icon { width: 18px; height: 18px; stroke: var(--w2b-text-muted); }
.search__input { padding: 7px 0; font-size: 13px; font-weight: 500; }
.search__input::placeholder {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: none;
}
.search__clear {
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 9px;
  background: var(--w2b-surface-3);
}
.fchip__remove {
  display: inline-grid;
  width: 18px;
  height: 18px;
  place-items: center;
  margin: -2px -7px -2px 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  font-size: 15px;
  font-weight: 400;
  line-height: 1;
}

.vitrine-head { margin: 11px 2px 0; align-items: center; }
.vitrine-head__title {
  font-size: clamp(27px, 8vw, 34px);
  font-weight: 600;
  letter-spacing: -0.025em;
}
.vitrine-head__count {
  padding: 5px 9px;
  background: var(--w2b-surface-3);
  border-radius: 999px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: none;
}

.tiles {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.tile {
  min-width: 0;
  overflow: hidden;
  background: var(--w2b-surface-1);
  border: 1px solid rgba(54, 35, 43, 0.07);
  border-radius: 20px;
  box-shadow: 0 8px 24px rgba(55, 35, 45, 0.07);
  transition: transform var(--w2b-dur-base) var(--w2b-ease-out),
              box-shadow var(--w2b-dur-base) var(--w2b-ease-out);
}
.tile:active { transform: scale(0.985); }
.tile__media {
  aspect-ratio: 0.78;
  background: linear-gradient(145deg, #f0e9e6, #faf7f5);
  border: 0;
  border-radius: 0;
  cursor: zoom-in;
  touch-action: pan-y;
  -webkit-touch-callout: none;
}
.tile__media img {
  user-select: none;
  -webkit-user-drag: none;
  transition: transform 500ms var(--w2b-ease-out);
}
@media (hover: hover) {
  .tile:hover { transform: translateY(-3px); box-shadow: var(--w2b-shadow-2); }
  .tile:hover .tile__media img { transform: scale(1.025); }
}
.tile__chan {
  top: 9px;
  left: 9px;
  max-width: calc(100% - 50px);
  overflow: hidden;
  padding: 6px 9px;
  background: rgba(255, 253, 252, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 999px;
  color: var(--w2b-text-muted);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-overflow: ellipsis;
  text-transform: none;
  white-space: nowrap;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.tile--stock .tile__chan { background: rgba(166, 61, 97, 0.9); }
/* The hint on every card. It must say the same thing the loupe will do, so the
   figure comes from a custom property that app.js rewrites when the factor is
   changed — one property, every card, no re-render. */
.tile__media:has(img)::after,
.fit-row__thumb:has(img)::after {
  content: var(--w2b-zoom-label, "1.5x");
  position: absolute;
  right: 9px;
  bottom: 9px;
  display: grid;
  place-items: center;
  width: 31px;
  height: 31px;
  color: var(--w2b-text);
  background: rgba(255, 253, 252, 0.86);
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 11px;
  box-shadow: 0 4px 15px rgba(39, 31, 35, 0.1);
  font-size: 9px;
  font-weight: 700;
  pointer-events: none;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.tile__body { gap: 5px; padding: 12px 12px 13px; }
.tile__title {
  min-height: 2.65em;
  font-family: var(--w2b-font-body);
  font-size: 12px;
  font-weight: 700;
  line-height: 1.32;
  letter-spacing: -0.012em;
}
.tile__art { color: var(--w2b-text-subtle); font-size: 8px; font-weight: 600; letter-spacing: 0.08em; }
.tile__price {
  margin-top: 2px;
  font-family: var(--w2b-font-body);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.tile__btn {
  min-height: 39px;
  margin-top: 7px;
  padding: 9px 7px;
  color: var(--w2b-on-primary);
  background: var(--w2b-primary);
  border: 0;
  border-radius: 12px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: none;
  transition: transform var(--w2b-dur-fast) var(--w2b-ease-out), opacity var(--w2b-dur-fast);
}
.tile__btn::before { content: "+"; font-size: 14px; font-weight: 400; margin-right: 5px; vertical-align: -1px; }
.tile__btn:active { transform: scale(0.96); }
.tile__btn.is-in {
  color: var(--w2b-accent);
  background: var(--w2b-accent-soft);
  border: 0;
}
.tile__btn.is-in::before { content: "✓"; font-size: 11px; }

/* Main-channel posts become visual stories instead of compact message rows. */
.post {
  display: block;
  overflow: hidden;
  padding: 0;
  background: var(--w2b-surface-1);
  border: 1px solid rgba(54, 35, 43, 0.07);
  border-radius: 24px;
  box-shadow: var(--w2b-shadow-1);
}
.post__thumb,
.post__gallery {
  position: relative;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  border-radius: 0;
  overflow: hidden;
}
.post__thumb img,
.post__gallery img { width: 100%; height: 100%; object-fit: cover; user-select: none; -webkit-user-drag: none; }
.post__body { padding: 16px; }
.post__head { font-size: 9px; font-weight: 600; letter-spacing: 0.07em; }
.post__title { margin-top: 8px; font-size: 16px; font-weight: 700; }
.post__text { margin: 7px 0 0; font-size: 12px; line-height: 1.55; }
.post__foot { margin-top: 14px; }
.post__price { color: var(--w2b-text); font-family: var(--w2b-font-body); font-size: 17px; font-weight: 700; }

.btn {
  min-height: 42px;
  padding: 11px 16px;
  border-radius: 13px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.045em;
  text-transform: none;
}
.btn--primary {
  background: var(--w2b-primary);
  box-shadow: 0 8px 18px rgba(45, 32, 39, 0.15);
}
.btn--ghost { background: var(--w2b-surface-2); border: 1px solid var(--w2b-line); }
.btn:active { transform: scale(0.97); opacity: 0.82; }

.section-title {
  margin: 22px 2px 4px;
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.02em;
}
.panel,
.row,
.wallet,
.stat {
  background: rgba(255, 253, 252, 0.9);
  border: 1px solid rgba(54, 35, 43, 0.07);
  border-radius: 18px;
  box-shadow: var(--w2b-shadow-1);
}
.panel { padding: 17px; }
.panel__title { font-family: var(--w2b-font-body); font-size: 14px; font-weight: 700; }
.panel__note { margin-bottom: 0; font-size: 12px; }
.row { padding: 12px; }
.row__icon { border-radius: 12px; }
.row__title { font-size: 12px; font-weight: 700; }
.row__amount { font-family: var(--w2b-font-body); font-weight: 700; }
.wallet { padding: 18px; border-radius: 22px; }
.wallet__amount { color: var(--w2b-text); font-size: 31px; font-weight: 600; }
.wallet__caption { font-size: 11px; }
.wallet__hint { font-size: 10px; }
.stats { gap: 8px; }
.stat { padding: 12px 7px; }
.stat__value { font-family: var(--w2b-font-body); font-size: 13px; font-weight: 700; }
.stat__label { font-size: 7px; letter-spacing: 0.07em; }
.badge-chip { background: var(--w2b-surface-1); border: 1px solid var(--w2b-line); box-shadow: none; }

.fit-row__thumb {
  position: relative;
  width: 68px;
  height: 86px;
  border-radius: 14px;
  cursor: zoom-in;
  touch-action: pan-y;
}
.fit-row__thumb:has(img)::after { right: 5px; bottom: 5px; width: 25px; height: 25px; border-radius: 8px; font-size: 8px; }
.coupon { border: 0; border-radius: 20px; background: var(--w2b-accent-soft); }
.coupon__badge { color: var(--w2b-accent); font-weight: 600; }

.field__label { margin: 0 0 7px 2px; font-size: 9px; font-weight: 700; letter-spacing: 0.07em; }
.field__input,
.field__select,
.field__textarea {
  padding: 13px 14px;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line-strong);
  border-radius: 14px;
  font-size: 13px;
  transition: border-color var(--w2b-dur-fast), box-shadow var(--w2b-dur-fast);
}
.field__input:focus,
.field__select:focus,
.field__textarea:focus {
  outline: 0;
  border-color: var(--w2b-accent);
  box-shadow: 0 0 0 3px var(--w2b-accent-soft);
}
.seg { padding: 4px; background: var(--w2b-surface-3); border: 0; border-radius: 14px; }
.seg__btn { border-radius: 11px; font-size: 9px; text-transform: none; letter-spacing: 0.03em; }
.seg__btn.is-active { background: var(--w2b-surface-1); color: var(--w2b-text); box-shadow: var(--w2b-shadow-1); }
.pill { border: 0; border-radius: 999px; background: var(--w2b-surface-3); font-size: 8px; }

.card {
  overflow: hidden;
  padding: 22px;
  border: 0;
  border-radius: 24px;
  box-shadow: 0 16px 38px rgba(43, 28, 36, 0.16);
}
.card--birthday { background: linear-gradient(145deg, #522639, #281a21); }
.card--holiday { background: linear-gradient(145deg, #30493f, #1d2924); }
.card--generic { background: linear-gradient(145deg, #3b2d35, #21191e); }
.card--vip { background: linear-gradient(145deg, #f3ece5, #dfd1c7); color: var(--w2b-text); }
.card__rim { width: 56px; height: 3px; top: 18px; left: 22px; border-radius: 999px; }
.card__head { margin-top: 8px; }
.card__emoji { display: grid; place-items: center; width: 34px; height: 34px; background: rgba(255,255,255,0.1); border-radius: 11px; font-size: 16px; }
.card__label { font-size: 9px; letter-spacing: 0.1em; }
.card__percent-num { font-size: 54px; font-weight: 600; }
.card__title { font-size: 22px; font-weight: 600; }
.card__code { border-radius: 13px; }
.card__copy { border-radius: 9px; letter-spacing: 0.04em; text-transform: none; }

.sheet__panel {
  padding: 10px 16px calc(26px + var(--w2b-safe-bottom));
  background: var(--w2b-bg);
  border-radius: 30px 30px 0 0;
}
.sheet__panel::before { width: 42px; height: 5px; margin: 2px auto 18px; background: var(--w2b-line-strong); }
.sheet__title { font-size: 28px; font-weight: 600; letter-spacing: -0.02em; }
.toast { color: var(--w2b-on-primary); background: rgba(45, 32, 39, 0.94); border: 0; border-radius: 14px; }
.demobar { margin: 8px 12px; border: 1px solid var(--w2b-line); border-radius: 14px; box-shadow: var(--w2b-shadow-1); }

/* Long-press image loupe. The image inside is positioned by app.js so the
   pixel under the finger remains in the exact centre at 2x magnification. */
.product-loupe {
  position: fixed;
  z-index: 120;
  width: 152px;
  height: 152px;
  padding: 5px;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.72) translateY(12px);
  transform-origin: 50% 100%;
  transition: opacity 120ms ease-out, transform 220ms var(--w2b-ease-out);
  will-change: left, top, transform;
}
.product-loupe::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--w2b-surface-1);
  border: 2px solid var(--w2b-accent);
  border-radius: 50%;
  box-shadow: 0 20px 55px rgba(49, 27, 39, 0.27), 0 0 0 5px rgba(255, 253, 252, 0.72);
}
/* The zoom factor on the loupe: a LABEL. The loupe is pointer-events: none —
   the finger under it keeps driving the magnifier — so nothing inside it can be
   pressed. What changes the factor is the 🔍 chip in the search row
   (.filterbtn--zoom); this only says what the chip currently means. */
.product-loupe__zoom {
  position: absolute;
  right: 1px;
  bottom: 9px;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  color: var(--w2b-on-primary);
  background: var(--w2b-accent);
  border: 3px solid var(--w2b-surface-1);
  border-radius: 50%;
  font-family: inherit;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
  pointer-events: none;
}

/* `display: flex` on .filterbtn outranks the browser's own [hidden] rule, so a
   hidden chip would otherwise stay on screen. Same shape as .sheet[hidden] and
   .toast[hidden] elsewhere in this codebase. */
.filterbtn[hidden] { display: none; }

/* ── the bar that stays ────────────────────────────────────────────────────
   Search, the filter button and the badges of what is applied, held at the top
   while the vitrine scrolls under them. Four hundred photographs used to sit
   between a client and the controls that produced them: narrowing a selection
   meant scrolling all the way back up, and the way you find out whether that is
   a problem is by watching somebody give up instead.

   The negative margin pulls the bar out to the page edges so nothing shows
   through beside it, and the padding puts the content back where it was. */
.vbar {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: 0 calc(-1 * var(--w2b-space-4));
  padding: var(--w2b-space-2) var(--w2b-space-4) var(--w2b-space-2);
  background: var(--w2b-bg);
}
/* A hairline that appears only once the bar is actually holding — otherwise
   every screen would open with a rule under the search box for no reason. */
.vbar::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 1px;
  background: var(--w2b-line);
  opacity: 0;
  transition: opacity var(--w2b-dur-fast);
}
.vbar.is-stuck::after { opacity: 1; }
/* Empty until something is applied — and then it must not add a gap of its own
   on top of the row's. */
.vbar .applied { margin-top: var(--w2b-space-2); }
.vbar #appliedBar:empty { display: none; }

/* The badge of a catalogue reads as the heading the other badges sit under. */
.fbadge--channel { font-weight: 700; }

/* «В наявності» is the one coloured thing in the app, in the sheet as well as
   in the row it replaced. */
.fchip--stock { border-color: var(--w2b-accent); color: var(--w2b-accent); }

/* What choosing a catalogue does to everything below it, said once. */
.fsheet__hint {
  margin-top: var(--w2b-space-2);
  color: var(--w2b-text-subtle);
  font-size: var(--w2b-fs-xs);
  line-height: 1.4;
}

/* The chip that changes it, in the search row beside the filter button. */
.filterbtn--zoom,
.filterbtn--layout {
  gap: 3px;
  font-size: var(--w2b-fs-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* ── три вигляди вітрини ───────────────────────────────────────────────────
   One markup, three layouts. `.tiles--grid` is the default shape already
   declared above; the other two restate only what differs, so a change to a
   card is a change to all three. */

/* Список — a line each, for finding rather than browsing. The picture shrinks
   to a stamp that says WHICH thing this is; holding the row is what shows the
   thing itself (see .peek). */
.tiles.tiles--list {
  grid-template-columns: 1fr;
  gap: 8px;
}
.tiles--list .tile {
  flex-direction: row;
  align-items: stretch;
}
.tiles--list .tile__media {
  /* A fixed square, stated as both sides and taken OUT of the stretch.
     Left to itself the thumbnail is sized by the photograph's own proportions —
     a 4:5 picture makes a 95px row, a square one makes 76 — and a list whose
     rows change height per photograph is a list that is hard to read down.
     `object-fit: cover` on the image absorbs the difference instead. */
  flex: 0 0 76px;
  align-self: center;
  width: 76px;
  height: 76px;
  aspect-ratio: auto;
  border-radius: 0;
}
/* The catalogue badge needs a corner, and in a row there is not one to spare. */
.tiles--list .tile__chan { display: none; }
/* Title and price stacked on the left, «Хочу» on the right, everything on one
   line of reading. A grid rather than a wrapper element: the three children are
   the same three the other two views use, and a list must not fork the markup.

   Stated on .tile__body directly, because the base rule sets `display: flex`
   and a column direction that would otherwise survive into here. */
.tiles.tiles--list .tile__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "title btn" "meta btn";
  align-content: center;
  align-items: center;
  gap: 2px 10px;
  padding: 8px 12px;
}
.tiles--list .tile__title {
  grid-area: title;
  /* Reserved two lines to square off a grid cell; a row is not one. */
  min-height: 0;
}
.tiles--list .tile__meta { grid-area: meta; }
.tiles--list .tile__btn {
  grid-area: btn;
  align-self: center;
  min-height: 34px;
  margin-top: 0;
  padding: 8px 14px;
}
/* The 1.5x hint belongs to the loupe, and the loupe is not what a held row
   does here. */
.tiles--list .tile__media:has(img)::after { content: none; }

/* Великі — one across, the photograph at the size the channel posted it. The
   loupe works exactly as it does in the grid; there is simply more to magnify. */
.tiles.tiles--feed {
  grid-template-columns: 1fr;
  gap: 18px;
}
.tiles--feed .tile__media {
  aspect-ratio: 4 / 5;
  /* No loupe here. The photograph is already the width of the screen, so
     magnifying it enlarges what is on show rather than revealing anything —
     and a hint promising 1.5x over a picture the gesture no longer answers on
     is worse than no hint. The gesture is switched off in app.js to match. */
  cursor: auto;
}
.tiles--feed .tile__media:has(img)::after { content: none; }
.tiles--feed .tile__title { min-height: 0; font-size: 14px; }
.tiles--feed .tile__price { font-size: 16px; }
.tiles--feed .tile__body { padding: 14px 14px 15px; }
.tiles--feed .tile__btn { min-height: 44px; font-size: 11px; }

/* ── the held row's answer ─────────────────────────────────────────────────
   Not a magnifier: a magnifier over an 84px thumbnail enlarges a thumbnail.
   What the list is missing is the photograph, so it gets the photograph —
   about a third of the screen, for as long as the finger is down. */
.peek {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: var(--w2b-space-4);
  background: rgba(39, 31, 35, 0.55);
  opacity: 0;
  /* The finger under it keeps driving the gesture, exactly as with the loupe. */
  pointer-events: none;
  transition: opacity var(--w2b-dur-fast) var(--w2b-ease-out);
}
.peek.is-visible { opacity: 1; }
.peek__card {
  max-width: min(88vw, 420px);
  overflow: hidden;
  background: var(--w2b-surface-1);
  border-radius: 20px;
  box-shadow: 0 18px 48px rgba(30, 18, 24, 0.32);
  transform: scale(0.96);
  transition: transform var(--w2b-dur-fast) var(--w2b-ease-out);
}
.peek.is-visible .peek__card { transform: scale(1); }
.peek__image {
  display: block;
  width: 100%;
  /* A third of the screen, stated as one height rather than a range, so every
     article opens to the same size whatever shape it was photographed in —
     `contain` letterboxes the difference and shows the whole thing either way.
     A range looked reasonable and then took half the screen on a real phone,
     because the tall end of it is what most catalogue photographs hit. */
  height: 34vh;
  object-fit: contain;
  background: var(--w2b-surface-3);
}
.peek__title {
  padding: 10px 14px 13px;
  color: var(--w2b-text);
  font-size: var(--w2b-fs-sm);
  font-weight: 700;
  text-align: center;
}
.product-loupe__viewport {
  position: absolute;
  inset: 7px;
  z-index: 1;
  overflow: hidden;
  background: var(--w2b-surface-3);
  border-radius: 50%;
}
.product-loupe__image {
  position: absolute;
  max-width: none;
  pointer-events: none;
  user-select: none;
}
.product-loupe.is-visible { opacity: 1; transform: scale(1) translateY(0); }
.product-loupe.is-flipped { transform-origin: 50% 0; }
/* While the loupe is up the page must not move under the finger. The
   non-passive touchmove handler in app.js is what actually cancels the scroll
   — `touch-action` is only consulted when a gesture BEGINS, so this cannot
   stop the gesture already in progress. It is here for the one after it, and
   because a locked page that also declares itself locked is easier to reason
   about than one that does not. */
body.is-zooming { overscroll-behavior: none; touch-action: none; }
body.is-zooming .tile__media img,
body.is-zooming .fit-row__thumb img { transition: none; }

@media (max-width: 370px) {
  .app { padding-right: 10px; padding-left: 10px; }

  .tiles { gap: 9px; }
  .tile { border-radius: 17px; }
  .tile__body { padding: 10px; }
  .tile__title { font-size: 11px; }
  .tile__price { font-size: 13px; }
  .tile__chan { max-width: calc(100% - 44px); padding: 5px 7px; font-size: 7px; }
  .tile__media:has(img)::after { right: 7px; bottom: 7px; width: 28px; height: 28px; }
  .topbar__sub { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

@media (prefers-reduced-motion: reduce) {
  .tile,
  .tile__media img,
  .product-loupe { transition-duration: 1ms !important; }
}

/* Components introduced on the PostgreSQL branch, expressed in the same
   soft-commerce language as the rest of the redesigned storefront. */
.searchbar { gap: 8px; }
.filterbtn {
  position: relative;
  width: 48px;
  padding: 0;
  justify-content: center;
  color: var(--w2b-text-muted);
  background: rgba(255, 253, 252, 0.86);
  border: 1px solid var(--w2b-line);
  border-radius: 15px;
  box-shadow: var(--w2b-shadow-1);
}
.filterbtn svg { width: 20px; height: 20px; stroke-width: 1.7; }
.filterbtn.is-on {
  color: var(--w2b-on-primary);
  background: var(--w2b-primary);
  border-color: var(--w2b-primary);
}
.filterbtn__count {
  position: absolute;
  top: -6px;
  right: -5px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  color: var(--w2b-on-primary);
  background: var(--w2b-accent);
  border: 2px solid var(--w2b-bg);
  border-radius: 999px;
  font-size: 8px;
  line-height: 14px;
}

.applied { gap: 7px; margin-top: 10px; }
.fbadge {
  gap: 7px;
  padding: 7px 10px 7px 12px;
  color: var(--w2b-accent);
  background: var(--w2b-accent-soft);
  border: 0;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
}
.fbadge__x { opacity: 0.65; }
.applied__clear { color: var(--w2b-text-muted); font-size: 10px; }

.fsheet__label { font-weight: 700; letter-spacing: 0.08em; }
.fsheet__grid { gap: 7px; }
.fchip {
  padding: 8px 12px;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
}
.fchip.is-active {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--w2b-on-primary);
  background: var(--w2b-primary);
  border-color: var(--w2b-primary);
}
.fchip.is-active .fchip__count { margin-left: 0; }
.fchip__remove { margin-right: -6px; }
.more { margin-top: 2px; border-radius: 14px; }

.post__gallery {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.post__gallery--multi { grid-auto-columns: 88%; }
.post__caption { margin-top: 10px; font-size: 12px; line-height: 1.6; }
.post__when { font-size: 9px; }

.presets { gap: 8px; }
.preset {
  padding: 12px;
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 15px;
  box-shadow: var(--w2b-shadow-1);
}
.preset__name { font-weight: 700; }
.preset__when,
.preset__why { font-size: 10px; }

.thanks__avatar {
  border-radius: 50%;
  box-shadow: 0 12px 30px rgba(55, 35, 45, 0.16);
}
.thanks__name { font-family: var(--w2b-font-display); font-size: 28px; font-weight: 600; }
.thanks__role { color: var(--w2b-accent); font-weight: 700; letter-spacing: 0.08em; }
.thanks__note { color: var(--w2b-accent); background: var(--w2b-accent-soft); border: 0; border-radius: 14px; }
.thanks__direct { color: var(--w2b-accent); font-weight: 700; }

.row--sync { border-radius: 18px; }
.row__actions { gap: 7px; }
.row__sync { color: var(--w2b-text-muted); }

.field__label--split {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.field__source {
  flex: none;
  padding: 4px 7px;
  color: var(--w2b-accent);
  background: var(--w2b-accent-soft);
  border-radius: 999px;
  font-size: 8px;
  letter-spacing: 0.035em;
}
.autofill-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: -2px 2px 0;
  color: var(--w2b-text-muted);
  font-size: 10px;
  line-height: 1.45;
}
.autofill-note > span:first-child {
  flex: none;
  display: grid;
  place-items: center;
  width: 17px;
  height: 17px;
  color: var(--w2b-on-primary);
  background: var(--w2b-accent);
  border-radius: 50%;
  font-size: 9px;
  font-weight: 700;
}

/* ── аналітика: the gap, drawn ────────────────────────────────────────────────
   A month is one bar, and the bar carries the ratio inside itself: the whole
   width is what people picked up, the filled part is what they actually asked
   about. Two numbers printed side by side do not show a ratio — the eye has to
   divide them — and this screen exists to make one ratio obvious. */
.months { display: flex; flex-direction: column; gap: var(--w2b-space-2); }
.month {
  display: grid;
  grid-template-columns: 58px 1fr auto;
  align-items: center;
  gap: var(--w2b-space-3);
  font-size: var(--w2b-fs-2xs);
  font-variant-numeric: tabular-nums;
}
.month__key { color: var(--w2b-text-subtle); letter-spacing: 0.04em; }
.month__bar {
  position: relative;
  height: 10px;
  width: var(--w2b-bar, 0%);
  min-width: 2px;
  border-radius: var(--w2b-radius-xs);
  background: var(--w2b-surface-3);
  overflow: hidden;
}
.month__bar::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--w2b-bar-fill, 0%);
  background: var(--w2b-primary);
}
.month__n { color: var(--w2b-text-muted); }

/* The advice reads as sentences because somebody has to act on it. The label
   on the left is the verb — add, check, quiet — so the list can be skimmed for
   the one kind of finding you have time for today. */
.tip {
  display: flex;
  gap: var(--w2b-space-3);
  padding: var(--w2b-space-3);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: var(--w2b-radius-md);
}
.tip + .tip { margin-top: var(--w2b-space-2); }
.tip__kind {
  flex: none;
  align-self: flex-start;
  padding: 3px 8px;
  border-radius: var(--w2b-radius-xs);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: var(--w2b-ls-caps);
  background: var(--w2b-surface-3);
  color: var(--w2b-text-muted);
}
.tip--stock .tip__kind { background: var(--w2b-primary); color: var(--w2b-on-primary); }
.tip--check .tip__kind { background: var(--w2b-accent-soft); color: var(--w2b-gold-strong); }
.tip__text { font-size: var(--w2b-fs-sm); line-height: var(--w2b-lh-body); color: var(--w2b-text); }

/* ── Заявки: a deal card you can answer with a swipe ───────────────────────
   The card is the only thing that moves; the two answers lie underneath it and
   are revealed by it moving off them, so the whole gesture is one transform and
   nothing reflows. `touch-action: pan-y` is what makes the two gestures
   coexist: the browser keeps the vertical scroll and hands the horizontal drag
   to the script, which is why nothing here has to prevent a default. */
.deal {
  position: relative;
  overflow: hidden;
  border-radius: var(--w2b-radius-md);
}
.deal + .deal { margin-top: var(--w2b-space-2); }
.deal__card {
  position: relative;
  touch-action: pan-y;
  will-change: transform;
}
.deal__hint {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  padding: 0 var(--w2b-space-4);
  font-size: var(--w2b-fs-sm);
  font-weight: 700;
  letter-spacing: var(--w2b-ls-caps);
  opacity: 0;
  transition: opacity .12s ease;
}
.deal__hint--yes { left: 0;  color: var(--w2b-ok); }
.deal__hint--no  { right: 0; color: var(--w2b-danger-text); }
/* Visible only while a drag is actually under way, and the one being committed
   to is the one that lights up — so letting go says which answer it will be. */
.deal.is-swiping .deal__hint { opacity: .45; }
.deal.is-yes .deal__hint--yes,
.deal.is-no  .deal__hint--no { opacity: 1; }

/* The card a reminder link pointed at. It fades on its own: the highlight
   answers "which one did the message mean" and then stops being useful. */
.deal.is-focused .deal__card {
  box-shadow: inset 0 0 0 2px var(--w2b-warn);
  animation: deal-focus 2.4s ease-out 1 forwards;
}
@keyframes deal-focus {
  0%, 60% { box-shadow: inset 0 0 0 2px var(--w2b-warn); }
  100%    { box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
  .deal__card { transition: none !important; }
  .deal.is-focused .deal__card { animation: none; }
}

/* ── запит: статус як позначка в куті ──────────────────────────────────────
   The client's own request card. Its status used to be a word in the amount
   column, which is a column: it reserved width on every card whether or not
   anything needed saying, and the question the client actually wrote had to
   share the line with it.

   The mark is absolutely positioned instead, so the text runs the full width of
   the card and the status costs it nothing. `.row` is a flex line shared by
   purchases, promos and channels — the positioning context is put on the
   modifier alone, so nothing else in that list starts containing absolute
   children. The body keeps a right pad the width of the mark plus its gutter,
   because a two-line question would otherwise run under the clock. */
.row--inquiry { position: relative; align-items: flex-start; }
.row--inquiry .row__body { padding-right: 24px; }
.inqmark {
  position: absolute;
  top: var(--w2b-space-2);
  right: var(--w2b-space-3);
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  /* Amber while it waits. It is a state, not a warning: nothing is wrong with a
     request that has just been sent. */
  color: var(--w2b-warn);
}
.inqmark svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Answered. The check is drawn a hair heavier than the clock — it is the state
   the client is waiting to see, and it should read from across the room. */
.inqmark--done { color: var(--w2b-ok); }
.inqmark--done svg { stroke-width: 2; }

/* ── примірочна: the balance line above its tabs ───────────────────────────
   «Бонуси» is one level in now, so the amount she can spend would have needed
   a tap to find. This is that amount, on one line, tappable, and it leads to
   the tab it is about. It renders only when there is something to say — see
   fitBalanceHtml() — because a strip that announces nothing still costs a line
   of a 390px screen.

   Full width and left-aligned rather than a centred pill: it is a row of the
   list below it, not a button competing with «Дізнатися наявність і ціну». */
.fitbal {
  display: flex;
  align-items: center;
  gap: var(--w2b-space-2);
  width: 100%;
  padding: var(--w2b-space-2) var(--w2b-space-3);
  background: var(--w2b-surface-1);
  border: 1px solid var(--w2b-line);
  border-radius: 0;
  font: inherit;
  color: var(--w2b-text);
  text-align: left;
  cursor: pointer;
}
.fitbal__text {
  flex: 1;
  min-width: 0;
  font-size: var(--w2b-fs-sm);
  font-variant-numeric: tabular-nums;
}
.fitbal__more { flex: none; color: var(--w2b-text-subtle); }
.fitbal:active { background: var(--w2b-surface-2); }

/* Four tabs across 390px. They fit — «Речі», «Запити», «Покупки», «Бонуси»
   are short words — but a count turns one of them into «Запити 1», so the row
   is allowed to scroll rather than squeeze a label into two lines. */
.seg--fit { margin-bottom: var(--w2b-space-3); }

/* ── компактність: екран телефона — це 844px, і вони не наші ──────────────
   What this section is for: a 390×844 phone, inside Telegram, which has already
   spent ~90px on its own header before this app draws anything. Measured on the
   catalogue, the first bag started at y=317 — a third of the visible page went
   to furniture. The wordmark moved to the join screen (see topbarHtml); these
   are the rest of the savings, and every one of them is a size, never a
   removal: nothing here hides information.

   Tap targets are left alone. The bottom navigation keeps its height and the
   buttons keep their padding — a compact app that is hard to hit is not
   compact, it is cramped. */
.topbar {
  padding: 9px 10px;
  margin-bottom: 6px;
  gap: 10px;
}
/* The avatar is a face, not a button: it needs to be recognisable, not
   tappable, so it can lose the six pixels the topbar height is made of. */
.topbar__avatar { width: 38px; height: 38px; border-radius: 12px; }
.topbar__sub { margin-top: 1px; }

/* A section heading is a signpost, and 28px of display type is a headline.
   «ЗАПРОС НАЛИЧИЯ И ЦЕНЫ» was running to two lines at 390px — a heading taller
   than the row it introduces. The rule beside it does the separating work, so
   the type can come down and keep doing the naming work. */
.section-title {
  margin: 13px 2px 5px;
  font-size: 19px;
  letter-spacing: -0.015em;
}
/* First heading on a view sits right under the tabs — it needs no air above it,
   the segmented control already provides the break. */
.stack > .section-title:first-child { margin-top: 2px; }
