@layer base, components, utilities;

@layer base {
/* === base/base.css === */
/* ============================================================
   Enotale v2 — Prototype CSS
   ALL values reference design-system/tokens.css custom properties.
   Zero hardcoded colors, spacing, or typography values.
   ============================================================ */

/* ============================================================
   BASE
   ============================================================ */

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

/* Horizontal-overflow guard. Prevents any stray element (off-canvas hover
   folds, full-bleed media, long unbreakable strings) from creating a sideways
   scroll on narrow viewports — the symptom of "no right-edge spacing" on
   mobile. `clip` (not `hidden`) keeps position:sticky working and does NOT
   create a scroll container, so inner overflow-x:auto regions (tables,
   chip rails) still scroll normally. */
html { overflow-x: clip; }

body {
  margin: 0;
  max-width: 100%;
  overflow-x: clip;
  font-family: var(--font-ui);
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  color: var(--color-text-primary);
  background-color: var(--color-surface-primary);
}

a {
  color: var(--color-text-link);
  text-decoration: none;
}
a:hover { color: var(--color-accent-primary); text-decoration: underline; }
a:visited { color: var(--color-text-link-visited); }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--color-text-primary);
  line-height: var(--line-height-display);
  margin: 0 0 var(--spacing-md);
}
h1 { font-size: var(--font-size-h1); font-weight: var(--font-weight-display-light); }
h2 { font-size: var(--font-size-h2); font-weight: var(--font-weight-display-regular); }
h3 { font-size: var(--font-size-h3); font-weight: var(--font-weight-display-regular); }
h4 { font-size: var(--font-size-h4); font-weight: var(--font-weight-display-medium); }

p { margin: 0 0 var(--spacing-md); line-height: var(--line-height-editorial); }

img, video { max-width: 100%; height: auto; display: block; }

/* Screen-reader-only — visually hidden, available to assistive tech.
   Durable base-layer home for `.sr-only`, referenced by 8+ components
   (suborder-timeline, kyc-checklist, bar-chart, tab-nav, sla-dot,
   checkbox-checklist, claim-evidence, stat-summary-strip). Lives here in
   the hand-authored base layer — NOT in utilities.css, which is generated
   by build/utility-build.js and would wipe a hand edit. */
.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;
}
}

@layer base {
/* === base/skip-link.css === */
/* ============================================================
   SKIP LINK
   ============================================================ */

.skip-link {
  position: absolute;
  top: -40px;
  left: var(--spacing-md);
  background: var(--color-accent-primary);
  color: var(--color-text-on-accent);
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-semibold);
  z-index: var(--z-toast);
  transition: top var(--duration-fast) var(--easing-standard);
}
.skip-link:focus { top: var(--spacing-sm); }
}

@layer base {
/* === base/hotspot.css === */
/* ============================================================
   HOTSPOT
   ============================================================ */

.hotspot {
  outline: 1px dotted var(--color-border-strong);
  cursor: pointer;
}
.hotspot:hover {
  outline-color: var(--color-accent-primary);
  background-color: var(--color-surface-sunk);
}

.link-disabled {
  color: var(--color-text-subtle);
  cursor: not-allowed;
  pointer-events: none;
}
}

@layer base {
/* === base/layout-utilities.css === */
/* ============================================================
   LAYOUT UTILITIES
   ============================================================ */

.container {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 var(--spacing-lg);
}

.section {
  padding: var(--spacing-editorial-md) 0;
}

.section--tight {
  padding: var(--spacing-xl) 0;
}

/* Tight spacing modifiers — one step down from editorial-md on the targeted edge(s).
   Desktop: 80px → 56px  |  ≤768px: 64px → 40px  |  ≤480px: 40px → 32px
   Responsive overrides live in responsive-overrides.css (search "tight modifiers"). */
.section--tight-top {
  padding-top: var(--spacing-3xl);
}

.section--tight-bottom {
  padding-bottom: var(--spacing-3xl);
}

.section--tight-both {
  padding: var(--spacing-3xl) 0;
}

/* Tighter spacing modifiers — one further step down from tight-*, home page only.
   Desktop: 56px → 32px  |  ≤768px: 40px → 24px  |  ≤480px: 32px → 16px
   Responsive overrides live in responsive-overrides.css (search "tighter modifiers"). */
.section--tighter-top {
  padding-top: var(--spacing-xl);
}

.section--tighter-bottom {
  padding-bottom: var(--spacing-xl);
}

.section--tighter-both {
  padding: var(--spacing-xl) 0;
}

/* Tightest-top modifier — one further step down from tighter-top, top edge only.
   Desktop: 32px → 16px  |  ≤768px: 24px → 8px  |  ≤480px: 16px → 4px
   Responsive overrides live in responsive-overrides.css (search "tightest-top"). */
.section--tightest-top {
  padding-top: var(--spacing-md);
}

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-lg); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-lg); }
/* grid-4--stack-mobile — opt-in modifier that collapses a 4-col grid to 1 column
   at ≤480px. The active responsive rule lives in responsive-overrides.css (layer:
   components) so it wins over the base-layer .grid-4 / .grid-3,.grid-4 rules.
   Consumer: PUB_01_HOME Избите section. */
/* Let grid items shrink below their content's intrinsic width so a long
   word or wide child never forces the whole track (and page) wider. */
.grid-2 > *, .grid-3 > *, .grid-4 > * { min-width: 0; }
}

@layer components {
/* === components/header/header.css === */
/* ============================================================
   HEADER — PUBLIC
   ============================================================ */

.header-public {
  background: var(--color-surface-primary);
  border-bottom: 1px solid var(--color-border-default);
  position: sticky;
  top: 0;
  z-index: var(--z-dropdown);
  height: var(--nav-height);
}

.header-public__inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 var(--spacing-lg);
  height: 100%;
  display: flex;
  align-items: center;
  gap: var(--spacing-xl);
}

.header-public__logo {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  line-height: 0;
  text-decoration: none;
}

.header-public__logo-img {
  height: 68px;
  width: auto;
  display: block;
  transition: filter var(--duration-base) var(--easing-standard);
}
.header-public__logo:hover .header-public__logo-img {
  filter: brightness(1.05);
}

/* Theme-aware logo swap:
   - Light theme (default) → dark logo for contrast on light surface
   - Dark theme            → light logo for contrast on dark surface
   Second <img> sits on top of the first; we toggle display, not visibility,
   so the hidden one doesn't consume layout space. */
.header-public__logo-img--dark { display: none; }
[data-theme="dark"] .header-public__logo-img--light { display: none; }
[data-theme="dark"] .header-public__logo-img--dark { display: block; }

.header-public__nav {
  display: flex;
  align-items: center;
  gap: var(--spacing-xl);
  flex: 1;
}

.header-public__nav a {
  font-size: var(--nav-link-font-size);
  font-weight: var(--nav-link-font-weight);
  letter-spacing: var(--nav-link-letter-spacing);
  color: var(--nav-link-color);
  text-transform: uppercase;
  text-decoration: none;
  padding: var(--spacing-sm) 0;
  border-bottom: 2px solid transparent;
  transition: color var(--duration-fast) var(--easing-standard),
              border-color var(--duration-fast) var(--easing-standard);
}
.header-public__nav a:hover { color: var(--nav-link-hover); text-decoration: none; }
.header-public__nav a.active { color: var(--nav-link-active); border-bottom-color: var(--color-accent-primary); }

.header-public__utils {
  /* Positioning context for the account-menu dropdown panel. The panel anchors
     to THIS box, not to its own trigger: the cluster's right edge is the header
     container's right content edge, so a right:0 panel lands on the page gutter
     at every width. Anchored to the trigger it hung off the left of the screen
     on phones, because the account icon is not the last item in the cluster. */
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  margin-left: auto;
}

/* W11-6c: the 44×44 box rule is promoted to the unscoped .util-icon selector
   (bundle:core — this file) so the panel topbar shares the geometry without
   maintaining a downstream patch. The public-header colour rule stays scoped.

   Utility icon touch target per WCAG 2.1 AA (2.5.8 minimum 24×24; we exceed).
   The button UA reset (background:none, border:0, etc.) is included so .util-icon
   works on both <a> and <button> without extra resets at the call-site.

   The child combinator on .header-public__utils > a is intentional: as a
   descendant it caught .account-menu__item rows and forced them into a 44×44
   box — .util-icon uses the descendant form deliberately (the account-menu
   trigger is one level deep inside .account-menu). */
.util-icon {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  cursor: pointer;
  color: var(--color-text-primary);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--easing-standard),
              color var(--duration-fast) var(--easing-standard);
}

/* Public-header colour rule stays scoped (hover = accent-primary, not text-accent) */
.header-public__utils > a,
.header-public__utils .util-icon {
  text-decoration: none;
}

@media (hover: hover) {
  .header-public__utils > a:hover,
  .header-public__utils .util-icon:hover {
    background: var(--color-surface-sunk);
    color: var(--color-accent-primary);
    text-decoration: none;
  }
}

/* Editorial line icons — currentColor stroke, 1.5px weight, rounded caps */
.util-icon__svg {
  width: 22px;
  height: 22px;
  display: block;
  stroke: currentColor;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.cart-count__badge {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-full);
  background: var(--color-accent-primary);
  color: var(--color-accent-primary-text);
  font-size: 10px;
  font-weight: var(--font-weight-ui-semibold);
  line-height: 16px;
  text-align: center;
  letter-spacing: 0;
}

/* Hamburger mobile */
.header-public__hamburger {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-text-primary);
  font-size: 1.25rem;
}

/* ============================================================
   6-ITEM NAV: reduce inner gaps at narrow desktop range.
   The desktop nav shows from 769px. At 1280px+ the full
   --spacing-xl (32px) gap is comfortable. Between 769px and
   1279px, reducing to --spacing-lg (24px) prevents the six
   uppercase nav items from crowding the utility icons.
   This rule does NOT override the ≤768px mobile override
   (which hides the nav entirely) because min-width: 769px
   starts where that rule ends.
   ============================================================ */

@media (min-width: 769px) and (max-width: 1279px) {
  .header-public__inner { gap: var(--spacing-lg); }
  .header-public__nav { gap: var(--spacing-lg); }
}

/* ============================================================
   HEADER — TRANSPARENT VARIANT (full-bleed hero overlay)
   Used when transparentHeader: true in page front matter.
   Sits fixed above hero, transitions to solid on scroll (.is-scrolled).
   ============================================================ */

.header-public--transparent {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  border-bottom-color: transparent;
  z-index: var(--z-dropdown);
  transition:
    background var(--duration-base) var(--easing-standard),
    border-color var(--duration-base) var(--easing-standard);
}

/* All text/icon elements inside transparent header flip to light.
   Child combinator on the anchor for the same reason as the geometry rule
   above: the account menu's panel is an opaque card and its links must keep
   their own ink, not the light-on-hero treatment. */
.header-public--transparent .header-public__nav a,
.header-public--transparent .header-public__utils > a,
.header-public--transparent .header-public__utils .util-icon,
.header-public--transparent .header-public__hamburger {
  color: var(--color-paper-light);
  text-shadow: 0 1px 4px color-mix(in srgb, var(--color-ink) 60%, transparent);
  transition:
    color var(--duration-fast) var(--easing-standard),
    text-shadow var(--duration-base) var(--easing-standard);
}

/* Transparent header sits over a dark hero — force the light logo regardless
   of theme. Once scrolled past hero, theme rules take over again. */
.header-public--transparent .header-public__logo-img--light { display: none; }
.header-public--transparent .header-public__logo-img--dark { display: block; }

.header-public--transparent .header-public__nav a:hover {
  color: var(--color-white);
  text-decoration: none;
}

.header-public--transparent .header-public__nav a.active {
  color: var(--color-white);
  border-bottom-color: var(--color-paper-light);
}

/* Hover background on utils — subtle tint so touch target is visible */
.header-public--transparent .header-public__utils a:hover,
.header-public--transparent .header-public__utils .util-icon:hover {
  background: color-mix(in srgb, var(--color-white) 15%, transparent);
  color: var(--color-white);
}

/* Solid state after scrolling past hero */
.header-public--transparent.is-scrolled {
  background: var(--color-surface-primary);
  border-bottom-color: var(--color-border-default);
}

.header-public--transparent.is-scrolled .header-public__nav a,
.header-public--transparent.is-scrolled .header-public__utils a,
.header-public--transparent.is-scrolled .header-public__utils .util-icon,
.header-public--transparent.is-scrolled .header-public__hamburger {
  color: var(--color-text-primary);
  text-shadow: none;
}

/* Scrolled transparent header behaves like a solid header — theme rules win. */
.header-public--transparent.is-scrolled .header-public__logo-img--light { display: block; }
.header-public--transparent.is-scrolled .header-public__logo-img--dark { display: none; }
[data-theme="dark"] .header-public--transparent.is-scrolled .header-public__logo-img--light { display: none; }
[data-theme="dark"] .header-public--transparent.is-scrolled .header-public__logo-img--dark { display: block; }

.header-public--transparent.is-scrolled .header-public__nav a:hover {
  color: var(--color-accent-primary);
}

.header-public--transparent.is-scrolled .header-public__nav a.active {
  color: var(--color-accent-primary);
  border-bottom-color: var(--color-accent-primary);
}

.header-public--transparent.is-scrolled .header-public__utils a:hover,
.header-public--transparent.is-scrolled .header-public__utils .util-icon:hover {
  background: var(--color-surface-sunk);
  color: var(--color-accent-primary);
}

/* Reduced motion — suppress transitions */
@media (prefers-reduced-motion: reduce) {
  .header-public--transparent,
  .header-public--transparent .header-public__logo-img,
  .header-public--transparent .header-public__nav a,
  .header-public--transparent .header-public__utils a,
  .header-public--transparent .header-public__utils .util-icon,
  .header-public--transparent .header-public__hamburger {
    transition: none;
  }
}

/* ============================================================
   MOBILE NAV DRAWER (C2 fix)
   Full-overlay drawer from left edge. Appears on .is-open.
   JS removes [hidden] attr to unblock display, then adds .is-open.
   Z-index sits BELOW age-gate modal (z-modal-overlay = 200) but
   ABOVE all page content (z-dropdown = 100).
   ============================================================ */

.mobile-nav {
  /* Hidden state: positioned off-screen left, invisible. */
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-modal-overlay) - 10); /* 190 — below age-gate, above everything else */
  display: flex; /* always flex so .is-open has effect without !important */
  visibility: hidden;
  background: var(--modal-overlay-bg); /* overlay backdrop — same Ink-50% as modal overlay */
  transition: visibility 0ms var(--duration-base); /* defer visibility until after opacity transition */
}

.mobile-nav.is-open {
  visibility: visible;
}

.mobile-nav__inner {
  width: min(320px, 85vw);
  height: 100%;
  background: var(--color-surface-primary); /* Parchment */
  border-right: 1px solid var(--color-border-default);
  box-shadow: var(--shadow-modal);
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow-y: auto;
  transform: translateX(-100%);
  transition: transform var(--duration-base) var(--easing-decelerate);
}

.mobile-nav.is-open .mobile-nav__inner {
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .mobile-nav__inner {
    transition: none;
  }
}

.mobile-nav__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-md) var(--spacing-lg);
  border-bottom: 1px solid var(--color-border-default);
  height: var(--nav-height-mobile);
  flex-shrink: 0;
}

.mobile-nav__brand {
  font-family: var(--font-display);
  font-size: var(--font-size-h4);
  font-weight: var(--font-weight-display-medium);
  color: var(--color-text-primary);
  letter-spacing: var(--letter-spacing-tight);
}

.mobile-nav__close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: var(--font-size-body);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast);
}
.mobile-nav__close:hover { background: var(--color-surface-sunk); color: var(--color-text-primary); }
.mobile-nav__close:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 2px; }

.mobile-nav__inner a {
  display: block;
  padding: var(--spacing-md) var(--spacing-lg);
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-ui-medium);
  color: var(--color-text-primary);
  text-decoration: none;
  border-bottom: 1px solid var(--color-border-default);
  transition: background var(--duration-fast), color var(--duration-fast);
}
.mobile-nav__inner a:hover { background: var(--color-surface-sunk); color: var(--color-accent-primary); text-decoration: none; }
.mobile-nav__inner a:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: -2px; }

.mobile-nav__divider {
  height: 1px;
  background: var(--color-border-default);
  margin: var(--spacing-xs) 0;
}

.mobile-nav__legal {
  font-size: var(--font-size-small) !important;
  color: var(--color-text-muted) !important;
}

/* ============================================================
   HEADER — MINIMAL
   ============================================================ */

.header-minimal {
  background: var(--color-surface-primary);
  border-bottom: 1px solid var(--color-border-default);
  height: var(--nav-height);
  display: flex;
  align-items: center;
  padding: 0 var(--spacing-lg);
  gap: var(--spacing-lg);
}

.header-minimal__back {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
}

.header-minimal__secure {
  margin-left: auto;
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}
}

@layer components {
/* === components/divider/divider.css === */
/* ============================================================
   VINTAGE RULE DIVIDER
   ============================================================ */

.rule-divider {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  margin: var(--spacing-lg) 0;
}

.rule-divider::before,
.rule-divider::after {
  content: '';
  flex: 1;
  height: var(--divider-vintage-weight);
  background: var(--divider-vintage-color);
}

.rule-divider__ornament {
  color: var(--divider-vintage-color);
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-wide);
  flex-shrink: 0;
}
}

@layer components {
/* === components/button/button.css === */
/* ============================================================
   BUTTONS
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
  min-height: var(--button-min-height);
  min-width: var(--button-min-width);
  padding: var(--button-padding-y) var(--button-padding-x);
  font-family: var(--button-font-family);
  font-size: var(--button-font-size);
  font-weight: var(--button-font-weight);
  letter-spacing: var(--button-letter-spacing);
  border-radius: var(--button-radius);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--button-transition-duration) var(--button-transition-easing),
              color var(--button-transition-duration) var(--button-transition-easing),
              border-color var(--button-transition-duration) var(--button-transition-easing);
}
.btn:focus-visible { outline: none; box-shadow: var(--button-primary-focus-ring); }

.btn--primary {
  background: var(--button-primary-bg);
  color: var(--button-primary-text);
  border-color: var(--button-primary-border);
}
.btn--primary:hover { background: var(--button-primary-bg-hover); color: var(--button-primary-text); text-decoration: none; }
.btn--primary:disabled { background: var(--button-primary-bg-disabled); color: var(--button-primary-text-disabled); cursor: not-allowed; }

.btn--secondary {
  background: var(--button-secondary-bg);
  color: var(--button-secondary-text);
  border-color: var(--button-secondary-border);
}
.btn--secondary:hover { background: var(--button-secondary-bg-hover); color: var(--button-secondary-text-hover); border-color: var(--button-secondary-border-hover); text-decoration: none; }

.btn--ghost {
  background: var(--button-ghost-bg);
  color: var(--button-ghost-text);
  border-color: var(--button-ghost-border);
}
.btn--ghost:hover { background: var(--button-ghost-bg-hover); color: var(--button-ghost-text-hover); text-decoration: none; }

.btn--small {
  padding: var(--button-small-padding-y) var(--button-small-padding-x);
  font-size: var(--button-small-font-size);
  letter-spacing: var(--button-small-letter-spacing);
  font-weight: var(--button-small-font-weight);
  text-transform: uppercase;
  /* 36px on a mouse — the density a catalogue grid is designed for. It was
     hardcoded here, which is a token defect on its own; the value now lives in
     component.json beside the base 44. */
  min-height: var(--button-small-min-height);
}

/* ON A TOUCH DEVICE THE SMALL BUTTON IS FULL SIZE.
   DD 2026-08-31, measured: .btn--small renders 103×36px, and at 390px there are
   20 of them on /vino/ and 9 on /degustatsionni-paketi/. The class is on 117
   built pages. 36px clears WCAG 2.2 AA (2.5.8, 24px) but not 2.5.5 AAA (44px),
   and on a phone the AAA figure is the one that describes a thumb.

   Scoped to `pointer: coarse` rather than to a width: the constraint is the
   INPUT, not the viewport. A 320px window on a desktop has a mouse and does not
   need the height; a 1024px tablet has a thumb and does. Width-scoped touch
   targets get this backwards on both. */
@media (pointer: coarse) {
  .btn--small { min-height: var(--touch-target-min); }
}

.btn--full { width: 100%; }

/* btn--danger — W8-3 (2026-08-30)
   Ghost geometry only (DD ruling 2: a filled danger button does not exist).
   Only valid combined with .btn--ghost — that modifier provides the geometry.
   Ink and border from --color-status-error; hover fills a transparent tint via
   color-mix() (no --color-status-error-tint token exists; color-mix is the spec
   technique). Focus ring is unchanged from .btn--ghost. */
.btn--ghost.btn--danger {
  color: var(--color-status-error);
  border-color: var(--color-status-error);
}
.btn--ghost.btn--danger:hover {
  background: color-mix(in srgb, var(--color-status-error) 10%, transparent);
  color: var(--color-status-error);
  border-color: var(--color-status-error);
}
.btn--ghost.btn--danger:focus-visible {
  outline: none;
  box-shadow: var(--button-ghost-focus-ring);
}

/* btn--with-icon — W8-5 (2026-08-30)
   Icon + label inline; icon sized by 1em relative to button font-size.
   Gap from --spacing-xs; works with btn--small. Icon-only pattern
   uses .icon-action with aria-label instead (existing). */
.btn--with-icon {
  gap: var(--spacing-xs);
}
.btn--with-icon svg {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
}

@layer components {
/* === components/product-card/product-card.css === */
/* ============================================================
   PRODUCT CARD
   ============================================================ */

.product-card {
  background: var(--card-product-bg);
  border: 1px solid var(--card-product-border-color);
  border-radius: var(--card-radius);
  box-shadow: var(--card-product-shadow);
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  transition: box-shadow var(--duration-fast) var(--easing-standard);
}
.product-card:hover { box-shadow: var(--shadow-floating); }

.product-card__image-wrap {
  position: relative;
  aspect-ratio: 3/4;
  background: var(--color-surface-sunk);
  overflow: hidden;
}

.product-card__image-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Placeholder bottle image */
.product-card__placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface-sunk);
  color: var(--color-text-subtle);
  font-size: 3rem;
}

/* Wine type badge */
.wine-badge {
  position: absolute;
  top: var(--spacing-sm);
  left: var(--spacing-sm);
  padding: var(--badge-padding-y) var(--badge-padding-x);
  border-radius: var(--badge-radius);
  font-size: var(--badge-font-size);
  font-weight: var(--badge-font-weight);
  letter-spacing: var(--badge-letter-spacing);
  text-transform: uppercase;
}
/* --inline: the same badge, in normal flow. The base is absolutely positioned in
   the photograph's top-left corner — right on a card, wrong anywhere the badge is a
   word among words, and on the pack's tasting steps it rendered out of flow and was
   invisible. The modifier resets only the three properties that place it; colours,
   padding and type stay the base's, so the mark a reader learned in the catalogue
   means the same thing here.
   DECLARED AFTER THE BASE ON PURPOSE. `.wine-badge--inline` and `.wine-badge` are
   both (0,1,0); with the modifier above the base, `position: absolute` won on source
   order and the badge stayed invisible with the override sitting right there. */
.wine-badge--inline {
  position: static;
  top: auto;
  left: auto;
  vertical-align: middle;
}

.wine-badge--red { background: var(--badge-wine-red-bg); color: var(--badge-wine-red-text); }
.wine-badge--white { background: var(--badge-wine-white-bg); color: var(--badge-wine-white-text); border: 1px solid var(--badge-wine-white-border); }
/* M4 fix: wine badges consume semantic-layer tokens shipped by UI Designer. */
.wine-badge--rose {
  background: var(--color-badge-wine-rose-bg);
  color: var(--color-badge-wine-rose-fg);
}
.wine-badge--orange {
  background: var(--color-badge-wine-orange-bg);
  color: var(--color-badge-wine-orange-fg);
}
.wine-badge--sparkling {
  background: var(--color-badge-wine-sparkling-bg);
  color: var(--color-badge-wine-sparkling-fg);
}
/* A pack is not a wine TYPE — the other badges name what is in the bottle, this
   one names what the product is. DD 2026-08-20: «макс близо до нормалното вино»,
   so it takes the same slot and geometry as a type badge rather than inventing a
   second mark; the accent fill is what separates it from the six type tones. */
.wine-badge--pack {
  background: var(--color-accent-primary);
  color: var(--color-accent-primary-text);
}
.wine-badge--natural {
  position: absolute;
  top: var(--spacing-sm);
  right: var(--spacing-sm);
  background: var(--color-badge-wine-natural-bg);
  color: var(--color-badge-wine-natural-fg);
  padding: var(--badge-padding-y) var(--badge-padding-x);
  border-radius: var(--badge-radius);
  font-size: var(--badge-font-size);
  font-weight: var(--badge-font-weight);
  letter-spacing: var(--badge-letter-spacing);
  text-transform: uppercase;
}

.product-card__body {
  padding: var(--card-product-padding);
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  position: relative; /* fold positioning context — fold is last child, inset:0 */
}

.product-card__name {
  font-family: var(--card-product-wine-name-font);
  font-size: var(--card-product-wine-name-size);
  font-weight: var(--card-product-wine-name-weight);
  color: var(--color-text-primary);
  line-height: var(--line-height-tight);
  margin: 0;
  text-decoration: none;
}
.product-card__name:hover { color: var(--color-accent-primary); text-decoration: none; }

.product-card__winery {
  font-family: var(--card-product-winery-font);
  font-size: var(--font-size-small);
  font-weight: var(--card-product-winery-weight);
  color: var(--color-text-muted);
  font-style: italic;
}
.product-card__winery a { color: var(--color-text-muted); text-decoration: none; }
.product-card__winery a:hover { color: var(--color-accent-primary); text-decoration: underline; }

.product-card__meta {
  font-size: var(--card-product-meta-size);
  color: var(--card-product-meta-color);
  font-family: var(--card-product-meta-font);
}

/* PRICE AND DELIVERY ARE TWO ROWS, not one. DD 2026-09-01: «виж как заради
   камиончето пада цената на бутилката… може просто цената и камиончето да са на
   отделни редове и ще е по-добре (даже по-компактно)».

   Measured before: in card mode at 390px the grid is two columns, the card is
   171px and the price row 121px — so «от 10,20 €/бут.» had 45px to live in and
   wrapped to 96px of height. The badge was not beside the price, it was eating it.
   Stacking costs ~20px on a 309px desktop card and saves ~56px on a phone.

   Row mode is unaffected: it overrides this row to `display: block` further down
   and hides the badge entirely, because there the delivery has its own column. */
.product-card__price-row {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-xs);
  /* margin-top:auto lives on the row, not on the price, so the whole pair pushes
     to the bottom of the card body (DD ruling 2026-08-31).
     RETIRED with the stack: `align-items: center`, and the note about why it beat
     `baseline`. Both described aligning two runs on ONE line — the 12px badge text
     riding 2px above the 16px price baseline, and why a flex container whose first
     item is an SVG hands its parent the glyph's bottom edge instead of a text
     baseline. There is no shared line left for them to sit on. */
}

.product-card__price {
  font-size: var(--card-product-price-size);
  font-weight: var(--font-weight-ui-semibold);
  color: var(--card-product-price-color);
  margin-top: 0; /* margin-top:auto moved to .product-card__price-row */
  flex: none;
}

.product-card__cta {
  margin-top: var(--spacing-sm);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Parchment Fold — Direction C killer detail. Opens via portrait-icon trigger.
   Positioned inside .product-card__body (position:relative) so it covers the
   text block exactly, regardless of body height. .product-card overflow:hidden
   clips the translateX(100%) resting state at the card edge. */
.product-card__fold {
  position: absolute;
  inset: 0;
  background: var(--card-product-fold-bg);
  border-top: 1px solid var(--color-border-default);
  padding: var(--spacing-md);
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-md);
  transform: translateX(100%);
  transition: transform var(--card-product-fold-duration) var(--card-product-fold-easing);
  z-index: var(--z-raised);
}

/* Hover chain: panel stays open while pointer is on trigger OR panel.
   The panel slides over the trigger when open — without the :has chain it flickers. */
.product-card:has(.product-card__author-trigger:hover) .product-card__fold,
.product-card:has(.product-card__author-trigger:focus-visible) .product-card__fold,
.product-card__fold:hover { transform: translateX(0); }

@media (prefers-reduced-motion: reduce) {
  .product-card__fold {
    transition: none;
    opacity: 0;
  }
  .product-card:has(.product-card__author-trigger:hover) .product-card__fold,
  .product-card:has(.product-card__author-trigger:focus-visible) .product-card__fold,
  .product-card__fold:hover {
    opacity: 1;
    transform: translateX(0);
  }
}

/* Portrait-icon trigger in the CTA row */
.product-card__author-trigger {
  width: var(--card-product-author-trigger-size);
  height: var(--card-product-author-trigger-size);
  border-radius: var(--radius-avatar);
  border: 2px solid var(--card-product-author-ring-color);
  background: var(--color-surface-sunk);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  line-height: 1;
  flex-shrink: 0;
  color: var(--color-text-muted);
}
.product-card__author-trigger:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}
/* Silhouette glyph sits at 55% of its circle — both in the CTA-row trigger and
   in the panel portrait. Was an inline style on each SVG; centralised here. */
.product-card__author-trigger svg,
.fold-portrait svg {
  width: 55%;
  height: 55%;
}

/* Fold and trigger are desktop-only — suppress below 1025px.
   Not 768: the catalog grid keeps a filter sidebar down to ~1024px, squeezing
   cards to 139–183px. At that width the 64px portrait leaves no room — the
   in-panel CTA overflowed the panel edge and the quote collapsed to two words.
   1025 matches the desktop breakpoint already used elsewhere in the system. */
@media (max-width: 1024px) {
  .product-card__fold { display: none; }
  .product-card__author-trigger { display: none; }
}

.fold-portrait {
  width: var(--card-product-author-portrait-size);
  height: var(--card-product-author-portrait-size);
  border-radius: var(--radius-avatar);
  border: 2px solid var(--card-product-author-ring-color);
  object-fit: cover;
  flex-shrink: 0;
  background: var(--color-surface-sunk);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-h4); /* M3 fix: was bare 1.5rem → --font-size-h4 (icon/glyph size, not type scale) */
  overflow: hidden;
}

.fold-portrait img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius-avatar); }

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

/* "Разгледай" button inside fold — duplicates the CTA-row link by DD ruling
   (2026-07-28: both buttons stay). tabindex="-1" in markup keeps it out of the
   tab order so it is not a second stop to the same href. */
.fold-info__cta {
  margin-top: var(--spacing-sm);
  align-self: flex-start;
}

.fold-info__name {
  font-family: var(--font-display);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-display-light);
  font-style: italic;
  color: var(--color-text-primary);
  line-height: var(--line-height-tight);
}

.fold-info__role {
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
}

.fold-info__quote {
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  font-style: italic;
  margin-top: var(--spacing-xs);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}



/* «от» and «€/бут.» are qualifiers, not the figure. They inherited
   .product-card__price's semibold, which made «от» read as loud as the number. */
.product-card__price .price__from,
.product-card__price .price__unit {
  font-family: var(--font-family-ui);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-regular);
  color: var(--color-text-muted);
}

/* ============================================================
   ROW MODE — density-toggle contract
   ============================================================
   The container carries data-density="rows"; the card becomes a band. See
   design-system/components/density-toggle/ for why the control is shared but
   the row is not: this card has price, CTA and the author fold, which the
   shelf card does not have at all.

   The fold is the one part that does NOT survive the change. It is an overlay
   sized to the card body — in a 61px band there is nothing to overlay, and a
   winemaker's quote is an editorial move that needs the room the card gives
   it. Row mode hides it and keeps its trigger out of the tab order, rather
   than shipping a cramped version of the page's one piece of voice. */

.product-grid[data-density="rows"] {
  display: block;
  border-top: 1px solid var(--color-border-default);
}

.product-grid[data-density="rows"] .product-card {
  flex-direction: row;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-sm);
  border: 0;
  border-bottom: 1px solid var(--color-border-default);
  border-radius: 0;
  box-shadow: none;
  background: transparent;
}
.product-grid[data-density="rows"] .product-card:hover {
  box-shadow: none;
  background: var(--color-surface-sunk);
}

.product-grid[data-density="rows"] .product-card__image-wrap {
  flex: 0 0 auto;
  width: 32px;
  height: 44px;
  aspect-ratio: auto;
  border-radius: var(--radius-sm);
}
/* contain, not cover: at 32px a bottle cropped at the shoulder is a coloured
   rectangle. The grid keeps cover, where there is room for the crop to read. */
.product-grid[data-density="rows"] .product-card__image-wrap img { object-fit: contain; }

/* Everything positioned against the image wrap has to go: at 32×44 the type
   badge and the three-bottle разфасовка mark cover the bottle completely — the
   thumbnail stopped being a bottle and became a cluster of marks.
   The разфасовка mark returns beside the price below; the type badge does not
   return, because the row already names the grape, and «от» on the price says
   what the pack mark says. */
.product-grid[data-density="rows"] .product-card__image-wrap .wine-badge,
.product-grid[data-density="rows"] .product-card__image-wrap .wine-badge--natural,
.product-grid[data-density="rows"] .product-card__image-wrap .packmark,
/* .bottle-count was missing from this list and it is the one that has WORDS.
   DD 2026-09-01: «малките бутилки… стоят като бял текст върху снимката». Exactly
   that: the mark is «▮▮▮ 3 бутилки» positioned in the photograph's corner, and at
   32×44 the label runs clean across the bottle — what the eye reads is «лки».
   The badge and the packmark were hidden here when row mode was built; the pack's
   count mark arrived later and never joined them.
   Nothing is lost: the row states the composition in words two columns to the right. */
.product-grid[data-density="rows"] .product-card__image-wrap .bottle-count { display: none; }

/* THE PACK ROW SHOWS ONE BOTTLE, THE PACK CARD SHOWS THE SET.
   DD 2026-08-31: «редовата визуализация е зле и бутилките изглеждат бели».

   Diagnosed rather than guessed: the thumbnail above is 32×44px, and a pack's
   picture is a COMPOSITE of three to six bottles standing on a lit studio
   backdrop. Contained into 32px, the bottles are three or four pixels wide each
   and what survives the downscale is the backdrop — which is literally what
   «изглеждат бели» describes. A wine row never had the problem because its
   picture is one bottle filling the frame.

   So the two images are both in the markup and the density mode picks: the card
   keeps the composite, which is the entire point of that photograph, and the row
   takes the lead bottle, which is what reads at 32px. The lead bottle is already
   in the build — the same file /vino/{lead}/ uses — so this is markup, not bytes,
   and because the swap is CSS it survives with JS off, where the listing keeps
   its card default.

   The «Пакет» badge stays hidden in row mode with the other overlays, as above:
   the row states the composition in words («2 бели · 1 розе»), which says what a
   pack is more precisely than a badge does. */
/* WHAT IS IN THE PACK — «3 бели · 2 червени · 1 розе». It stands in the meta
   slot a wine card fills with «сорт · подрегион», so it takes .product-card__meta
   for its type and adds only what a mix needs that a single value does not: the
   line may run to three parts, so it wraps, and a part must not break across the
   wrap — «2 / червени» on two lines reads as two facts. Hence nowrap on the item
   rather than on the line. */
.product-card__mix {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--spacing-xs);
}
.product-card__mix-item { white-space: nowrap; }

.product-card__img--rows { display: none; }
.product-grid[data-density="rows"] .product-card__img--cards { display: none; }
.product-grid[data-density="rows"] .product-card__img--rows { display: block; }

/* Beside the price, static and unshadowed — the same treatment .hitrow__price
   already gives it in the search results. Hidden in card mode, where the mark on
   the photograph is the ratified placement.
   The slot is RESERVED on every row, empty ones included: a mark that is present on
   four rows in twenty and absent on the rest would shift every column left of it. */
.product-card__packmark-row { display: none; }
.product-grid[data-density="rows"] .product-card__packmark-row {
  display: inline-flex;
  flex: 0 0 var(--product-row-packmark-w);
  justify-content: flex-end;
  align-items: flex-end;
}
.product-grid[data-density="rows"] .product-card__packmark-row .packmark {
  position: static;
  filter: none;
  color: var(--color-accent-primary);
}

.product-grid[data-density="rows"] .product-card__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--spacing-md);
  padding: 0;
}

.product-grid[data-density="rows"] .product-card__name,
.product-grid[data-density="rows"] .product-card__winery,
.product-grid[data-density="rows"] .product-card__meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin: 0;
}

/* Name and winery are the elastic column; everything to their right sits at
   its natural width so prices and buttons align down the listing. */
/* flex-basis 0, not a percentage. Percentage bases are measured against the whole
   row, so the three text columns collectively over-request whatever the thumb, the
   разфасовка mark, the price and the button have already taken, and every one of
   them then shrinks — which is how «Enotale White 2024» became «Enotale Whit…» on a
   1024px row with 300px of slack in it. With basis 0 the fixed items are served
   first and the columns divide exactly what is left, in a 3:2:2 ratio. */
.product-grid[data-density="rows"] .product-card__name { flex: 3 1 0; min-width: 0; }
.product-grid[data-density="rows"] .product-card__winery { flex: 2 1 0; min-width: 0; }
/* Grape only — the region is dropped, see the note in BUY_02_WINE_CATALOG.njk.
   The facet column standing beside this listing filters on both, and the page
   header states how many regions there are. */
.product-grid[data-density="rows"] .product-card__meta { flex: 2 1 0; min-width: 0; }
.product-grid[data-density="rows"] .product-card__region { display: none; }

/* Between 640 and 1100 the winery is the column that goes. The name identifies the
   wine and the grape is what the list is compared on; the winery is one tap away on
   the PDP. The upper bound is measured, not chosen: with the winery shown, 13 of 20
   names truncate at 1024; without it, none do, and at 1280 all three columns fit
   with the winery back. */
@media (min-width: 641px) and (max-width: 1100px) {
  .product-grid[data-density="rows"] .product-card__winery { display: none; }
}
/* FIXED SLOTS, so the text columns align down the page.
   The flexible columns divide what is LEFT, so anything to their right whose width
   varies per row moves them. It did: «от 10,20 €/бут.» is 138px and «9,50 €/бут.» is
   101px, and the разфасовка mark is 22px on a pack wine and 0 on a single — so the
   grape column began at 560, 589, 594, 561, 561, 566px on six consecutive rows. That
   ragged left edge is what reads as «сортовете не са подравнени». A listing is a
   table: the non-prose columns take reserved slots and the prose divides the rest. */
/* price-row becomes the fixed-width slot in row mode; delivery badge is hidden
   (no room: five columns at 1024–1600px; badge info is secondary at list density). */
.product-grid[data-density="rows"] .product-card__price-row {
  flex: 0 0 var(--product-row-price-w);
  margin: 0;
  display: block; /* no flex layout needed: badge is hidden in row mode */
}
.product-grid[data-density="rows"] .product-card__price {
  margin: 0;
  flex: none;
  text-align: right;
}
.product-grid[data-density="rows"] .product-card__price-row .delivery-badge {
  display: none;
}
.product-grid[data-density="rows"] .product-card__cta { flex: 0 0 auto; margin: 0; }

/* The fold and its trigger, explained above. */
.product-grid[data-density="rows"] .product-card__fold { display: none; }
.product-grid[data-density="rows"] .product-card__author-trigger { display: none; }

/* Below tablet a five-column band does not fit. Grape and region drop — they
   are the filterable facts, and the facet column above is where they are
   actually used — and the winery wraps under the name. */
@media (max-width: 640px) {
  .product-grid[data-density="rows"] .product-card__body { flex-wrap: wrap; gap: 2px var(--spacing-sm); }
  .product-grid[data-density="rows"] .product-card__name { flex-basis: 100%; }
  .product-grid[data-density="rows"] .product-card__winery { flex: 1 1 auto; }
  .product-grid[data-density="rows"] .product-card__meta { display: none; }

  /* The delivery badge comes BACK at narrow widths.
     It is hidden in row mode at 1024–1600px because the row is a five-column table
     there and the price slot has no room beside it. That reasoning does not hold at
     <=640px: the row has already wrapped (the name takes 100%, the meta line is gone),
     so the price slot has clear space underneath.
     Hiding it here would drop delivery from the PRIMARY storefront viewport — and
     because display:none also strips the node from the accessibility tree, a mobile
     screen-reader user would lose it too, while «Срок на доставка» stays a sidebar
     facet. A merchandising axis the sidebar filters on cannot be invisible on the card.
     DD ruling 2026-08-31 put the badge beside the price; this keeps that true on mobile,
     stacked rather than side by side because the slot is narrow. */
  .product-grid[data-density="rows"] .product-card__price-row {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--spacing-xs);
  }
  .product-grid[data-density="rows"] .product-card__price-row .delivery-badge {
    display: inline-flex;
  }
}

/* ============================================================
   BOTTLE GLYPH RULES — moved from search-variants.css to core (S260830).
   Emitted by: ico.bottleCountGlyphs(), ico.bottleSolid(), ico.bottle(),
   ico.packMark() — macros used on panel pages (WIN_13) as well as
   storefront, so these rules must live in a bundle:core file.
   ============================================================ */

.packmark {
  position: absolute;
  right: var(--spacing-sm);
  bottom: var(--spacing-sm);
  z-index: var(--z-index-raised);
  display: inline-flex;
  align-items: flex-end;
  gap: var(--spacing-xs);
  color: var(--color-text-muted);
  pointer-events: none;
  filter: drop-shadow(0 1px 2px color-mix(in srgb, var(--color-ink) 45%, transparent));
}

.packmark--min {
  color: var(--color-accent-primary);
}

.ico-bottle-solid {
  display: block;
  fill: currentColor;
  stroke: none;
}

.ico-bottle {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.packmark__x {
  margin-left: 0;
  font-family: var(--font-family-ui);
  font-size: var(--font-size-micro);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--letter-spacing-tight);
}
}

@layer components {
/* === components/filter-chip/filter-chip.css === */
/* ============================================================
   FILTER CHIP
   ============================================================ */

.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-sm);
  align-items: center;
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-full);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-medium);
  color: var(--color-text-primary);
  background: var(--color-surface-primary);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap; /* prevent multi-word/hyphenated labels wrapping (e.g. «Е-етикет») */
  transition: background var(--duration-fast), border-color var(--duration-fast), color var(--duration-fast);
}
.filter-chip:hover { border-color: var(--color-border-strong); background: var(--color-surface-sunk); text-decoration: none; color: var(--color-text-primary); }
.filter-chip--active,
a.filter-chip--active,
a.filter-chip--active:visited { background: var(--color-accent-primary); border-color: var(--color-accent-primary); color: var(--color-text-on-accent); }
.filter-chip--active:hover,
a.filter-chip--active:hover { background: var(--color-accent-primary-hover); color: var(--color-text-on-accent); }
.filter-chip__remove {
  background: none;
  border: none;
  color: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  margin-left: var(--spacing-xs);
}
}

@layer components {
/* === components/status-pill/status-pill.css === */
/* ============================================================
   STATUS PILL — canonical tone system (W2 rewrite)
   ============================================================
   Architecture: 9 canonical TONE classes are the primitives.
   Every pre-W2 variant name is kept as a @deprecated ALIAS
   that maps to its canonical tone — zero markup change required
   on CSS sync.

   Token source: component.json → status-pill.tone.*
   Generated CSS vars: --status-pill-tone-*

   Contrast verification (WCAG relative luminance formula; sRGB linearization;
   0.2126/0.7152/0.0722 coefficients; reproduced in build/contrast.py):
   ┌──────────┬─────────────────────────────────┬──────────┐
   │ Tone     │ Fill / Ink                      │ Ratio    │
   ├──────────┼─────────────────────────────────┼──────────┤
   │ neutral  │ Paper Fold #EFE6D2 / Walnut     │ 5.13:1 ✓ │
   │ info     │ #D8E6F0 / Slate Blue #4A6482    │ 4.80:1 ✓ │
   │ waiting  │ #EEE0C0 / Ink #1F1712           │ 13.50:1✓ │
   │ action   │ Amber Bark #B8782A / Ink        │ 4.84:1 ✓ │
   │ progress │ Slate Blue #4A6482 / White      │ 6.11:1 ✓ │
   │ success  │ Moss #4A6B3C / White            │ 6.07:1 ✓ │
   │ warning  │ Amber Bark #B8782A / Ink        │ 4.84:1 ✓ │
   │ critical │ Blood Oak #8B2323 / White       │ 8.89:1 ✓ │
   │ terminal │ transparent / Walnut #6B5D4E    │ 5.13:1+✓ │
   └──────────┴─────────────────────────────────┴──────────┘
   terminal measured on paper-fold (#EFE6D2), the neutral-badge surface.

   The table above is LIGHT mode only. Dark has its own values for all nine
   tones, remapped at the token layer in theme.css — see the DARK MODE note at
   the foot of this file. Do not read this table as the whole contract.

   Spec deviations (documented):
   — waiting fg: spec intended tarnished-gold (#A68643, max
     achievable ratio 3.43:1 on any bg → fails AA). Changed to
     ink (#1F1712). Measured: 13.49:1. See component.json.
   — terminal fg: spec said text-subtle (oak #8E7F6D → 3.42:1
     on parchment → fails AA normal text). Changed to text-muted
     (walnut #6B5D4E → 5.61:1 on parchment). See component.json.

   action + warning share the same fill/ink. Distinction is
   semantic label + icon (WCAG 1.4.1):
     action  = ! icon (whose-move: viewer must act)
     warning = ⚠ icon (whose-move: degraded or deadline)

   Social Walk variants (sw-*) included as aliases; canonical
   home is this file — social-walk.css copies are @deprecated.
   ============================================================ */

/* ---- BASE ---- */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--badge-padding-y) var(--badge-padding-x);
  border-radius: var(--radius-full);
  font-size: var(--badge-font-size);
  font-weight: var(--badge-font-weight);
  letter-spacing: var(--badge-letter-spacing);
  text-transform: uppercase;
  white-space: nowrap;
  border: 1px solid transparent;
}

/* QUIET IN TABLES — DD ruling 2026-08-31.
   «Статус-ите също може да са само икони според мен или да са икона с по-ситен шрифт.»

   In a table row the pill was the heaviest element on the line — heavier than the wine
   name it describes. That is an inverted hierarchy: the row is ABOUT the wine, and the
   status qualifies it. Uppercase + semibold + all-caps letterspacing are three
   amplifiers stacked on a label that should read as an aside.

   We drop the three typographic amplifiers and keep the coloured capsule and the
   ::before mark, which are what actually carry the state. Icon-only was the other option
   the DD offered; it was not taken because the pill vocabulary here has five states and
   three of them currently share one amber tone (see the tone defect logged 2026-08-31) —
   removing the word before the colour distinguishes the states would make them
   unreadable, not quieter.

   Scoped to table context only. Pills in page headers, detail views and the panel topbar
   keep their full weight, where they are the subject rather than a qualifier. Cascades to
   every page using .table-standard / .table-compact without per-page markup. */
.table-standard .status-pill,
.table-compact .status-pill {
  font-weight: var(--font-weight-ui-regular);
  text-transform: none;
  letter-spacing: normal;
}

@media (prefers-reduced-motion: no-preference) {
  .status-pill {
    transition: opacity var(--duration-fast);
  }
}

/* Icon slot — inline SVG or aria-hidden char inside .status-pill */
.status-pill__icon {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  width: 1em;
  height: 1em;
  font-style: normal;
}


/* ============================================================
   CANONICAL TONES — the nine primitives
   ============================================================ */

/* neutral — inert, no action implied (Чернова, Нов) */
.status-pill--neutral {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* info — scheduled or acknowledged, nothing owed (Планирана, Приета) */
.status-pill--info {
  background: var(--status-pill-tone-info-bg);
  color: var(--status-pill-tone-info-fg);
}

/* waiting — we owe the counterpart nothing; awaiting THEM (Изчаква избата) */
.status-pill--waiting {
  background: var(--status-pill-tone-waiting-bg);
  color: var(--status-pill-tone-waiting-fg);
}

/* action — awaiting the VIEWER (За преглед, Изчаква потвърждение) — carries ! icon */
.status-pill--action {
  background: var(--status-pill-tone-action-bg);
  color: var(--status-pill-tone-action-fg);
}

/* progress — actively moving (Подготовка, В транзит) — carries → icon */
.status-pill--progress {
  background: var(--status-pill-tone-progress-bg);
  color: var(--status-pill-tone-progress-fg);
}

/* success — resolved well (Одобрена, Доставена, Платена, Активна) — carries ✓ icon */
.status-pill--success {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* warning — degraded or approaching deadline (Ескалирана, Спряна) — carries ⚠ icon */
.status-pill--warning {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}

/* critical — failed or blocking (Отхвърлена, Просрочена) — carries ✕ icon */
.status-pill--critical {
  background: var(--status-pill-tone-critical-bg);
  color: var(--status-pill-tone-critical-fg);
}

/* terminal — over, no further action, not a failure
   (Приключена, Архивирана, Изтекла, Отказана)
   Outline-only: this is the fix for Settlement Open-vs-Closed
   and Wine Draft-vs-Archived rendering identically as --draft.
   Terminal has NO fill; neutral/draft has fill. */
.status-pill--terminal {
  background: var(--status-pill-tone-terminal-bg); /* transparent */
  color: var(--status-pill-tone-terminal-fg);
  border-color: var(--status-pill-tone-terminal-border);
}


/* ============================================================
   DEPRECATED ALIASES — pre-W2 variant names
   Each alias sets the same property values as its target tone.
   Zero markup change required; update class names via HANDOFF
   at next migration window.
   ============================================================ */

/* @deprecated → neutral */
.status-pill--confirmed {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* @deprecated → neutral (border retained for backward compat) */
.status-pill--draft {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* @deprecated → progress */
.status-pill--preparing {
  background: var(--status-pill-tone-progress-bg);
  color: var(--status-pill-tone-progress-fg);
}

/* @deprecated → warning
   GOVERNING RULE: unchanged — keeps current Amber Bark/Ink colour.
   Old token: --color-badge-status-shipped-bg: var(--color-amber-bark).
   Both --shipped and --transit were already the same Amber Bark/Ink colour; no
   convergence of different colours is possible. CSS preserves the current
   rendering. Semantic correction (shipped = progress) is deferred to markup.
   MARKUP DELTA: winery/partials/status-pill, admin/partials/status-pill,
   admin/partials/orders-status-pill → --shipped to --progress. */
.status-pill--shipped {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}

/* @deprecated → warning
   GOVERNING RULE: unchanged — keeps current Amber Bark/Ink colour.
   Old token: --color-badge-status-transit-bg: var(--color-amber-bark).
   Same reasoning as --shipped above; --transit was also Amber Bark/Ink.
   MARKUP DELTA: public/profile/order-detail.blade.php (I12),
   admin/partials/orders-status-pill → --transit to --progress. */
.status-pill--transit {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}

/* @deprecated → success */
.status-pill--delivered {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* @deprecated → success
   Fix: --delivered and --published were two different greens
   (delivered used Moss; published also used Moss but via
   different token chain). Now unified via the same tone token. */
.status-pill--published {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* @deprecated → progress
   GOVERNING RULE: unchanged — keeps current Slate Blue/White colour.
   Old token: --color-badge-status-pending-bg: var(--color-slate-blue).
   F-04 PREVENTED: flipping --pending to amber would make SubOrderStatus::Ready
   (winery done, awaiting courier) render as "viewer must act" amber on winery
   screens where the operator has nothing to do. CSS preserves the current
   Slate Blue/White rendering. Per-state corrections are all markup deltas.
   MARKUP DELTA per state:
     SubOrderStatus::Ready              → --waiting
     ClaimStatus::Open                  → --action
     WineryStatus::Awaiting             → --action
     WineryInquiryStatus::New           → --action
     WineRevisionStatus::Pending        → --action
     WineStatus::Submitted              → --waiting
     TastingPackStatus::Submitted       → --waiting
     OrderStatus::PendingPayment        → --waiting
     OrderStatus::Authorized (admin)    → --info
     AddonGrant::suspended (admin)      → --warning */
.status-pill--pending {
  background: var(--status-pill-tone-progress-bg);
  color: var(--status-pill-tone-progress-fg);
}

/* @deprecated → action
   GOVERNING RULE: unchanged — keeps current Amber Bark/Ink colour.
   Old token: --color-badge-status-review-bg: var(--color-amber-bark).
   F-03 PREVENTED: mapping --review to progress (blue) would make
   OrderStatus::Cancelled and SubOrderStatus::Cancelled render as blue
   "progress" on public/profile/orders.blade.php (I11) and
   public/profile/order-detail.blade.php (I12) — a semantic disaster on a
   customer money screen. CSS preserves the current amber rendering. The amber
   "action" tone is not ideal for Cancelled states, but it is not misleading
   in the way blue "progress" would be. Per-state corrections are markup deltas.
   MARKUP DELTA per state:
     ClaimStatus::InReview              → --progress  (P2, A2, I12/I13)
     WineStatus::InReview               → --progress  (P8, A5)
     TastingPackStatus::InReview        → --progress  (P7, A6)
     WineryInquiryStatus::Contacted     → --progress  (A10)
     OrderStatus::Cancelled (public)    → --terminal  (I11, I12)
     OrderStatus::Refunded (public)     → --terminal  (I11, I12)
     SubOrderStatus::Cancelled (public) → --terminal  (I12)
   NOTE: SettlementStatus::PendingReview uses --review and targets --action.
   With this revert, --review now renders action (amber) = correct colour for
   PendingReview. This is the one case where the governing rule and spec intent
   align — no markup change needed for SettlementStatus::PendingReview
   (colour is already correct; class migration to --action is optional). */
.status-pill--review {
  background: var(--status-pill-tone-action-bg);
  color: var(--status-pill-tone-action-fg);
}

/* @deprecated → critical */
.status-pill--rejected {
  background: var(--status-pill-tone-critical-bg);
  color: var(--status-pill-tone-critical-fg);
}

/* @deprecated → warning
   Was already: Amber Bark/Ink (same as --review; M4 note).
   Now: explicitly the warning tone. No visual change. */
.status-pill--warning {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}


/* ---- Campaign lifecycle @deprecated aliases ---- */

/* @deprecated → success */
.status-pill--campaign-active {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* @deprecated → info */
.status-pill--campaign-scheduled {
  background: var(--status-pill-tone-info-bg);
  color: var(--status-pill-tone-info-fg);
}

/* @deprecated → terminal */
.status-pill--campaign-ended {
  background: var(--status-pill-tone-terminal-bg);
  color: var(--status-pill-tone-terminal-fg);
  border-color: var(--status-pill-tone-terminal-border);
}

/* PHANTOM FIX — @deprecated → neutral
   Was: --campaign-draft (admin inline enum interpolation)
   Had NO CSS rule — raw base .status-pill only. Now defined.
   HANDOFF: admin/promotions/index.blade.php line ~xx — replace
   raw interpolation with partial or explicit class mapping. */
.status-pill--campaign-draft {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* PHANTOM FIX — @deprecated → terminal
   Was: --campaign-terminated (admin inline enum interpolation)
   Had NO CSS rule — raw base .status-pill only. Now defined.
   HANDOFF: admin/promotions/index.blade.php — same as above. */
.status-pill--campaign-terminated {
  background: var(--status-pill-tone-terminal-bg);
  color: var(--status-pill-tone-terminal-fg);
  border-color: var(--status-pill-tone-terminal-border);
}


/* PHANTOM FIX — @deprecated → terminal
   Was: --unpublished. Set by src/_data/catalog.json:189 for Nocturne Rosé
   and assigned in JS by ADM_15_CATALOG.njk:471 after «Спри публикуването».
   Had NO CSS rule anywhere in the repo — the pill rendered as raw base
   .status-pill, so «СПРЯНО» showed as bare text in a column where every
   other row carried a pill. Terminal is the right tone: publication has
   been stopped, which is an inactive end state, not an error.
   Second instance of the invented-variant defect class (first logged
   2026-08-04). Now defined. */
.status-pill--unpublished {
  background: var(--status-pill-tone-terminal-bg);
  color: var(--status-pill-tone-terminal-fg);
  border-color: var(--status-pill-tone-terminal-border);
}

/* NEW — promoted from WIN_02_PRODUCT_LIST.njk page-local <style>
   @deprecated local — now system-wide.
   The page-local block carried a comment deferring this promotion «until
   the page-local-CSS decision is ruled». The variant maps to the canonical
   action tone, which is what the page-local rule already referenced, so the
   promotion changes no rendered value and removes an override that could
   not participate in a token remap. The wider page-local-CSS decision is
   still open and is separate from this one variant. */
.status-pill--changes-requested {
  background: var(--status-pill-tone-action-bg);
  color: var(--status-pill-tone-action-fg);
}

/* ---- Plan tier aliases ---- */

/* PHANTOM FIX — @deprecated → success
   Was: --plan-premium base rule MISSING from vendored CSS
   (only a dark-mode override existed — no light-mode rule).
   Now defined as success tone; plan tier gold was decorative,
   not semantic — Premium active plan = success state.
   HANDOFF: --tier-premium (winery local) can adopt this class. */
.status-pill--plan-premium {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* Promoted from ADM_11 page-local <style> (S260830, follow-up).
   ADM_11 used ghost tokens (--color-status-pill-confirmed-* and
   --color-status-pill-preparing-*) that were only defined in WIN_01's
   page-local :root — making FREE render transparent on ADM_11.
   Canonical replacement uses the same tone mapping as --tier-free and
   --tier-basic (which already live here). */
.status-pill--plan-free {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}
.status-pill--plan-basic {
  background: var(--status-pill-tone-info-bg);
  color: var(--status-pill-tone-info-fg);
}

/* NEW — promoted from winery/plan/show.blade.php page-local <style> */
/* @deprecated local — now system-wide */
.status-pill--tier-free {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* @deprecated local → info */
.status-pill--tier-basic {
  background: var(--status-pill-tone-info-bg);
  color: var(--status-pill-tone-info-fg);
}

/* @deprecated local → success */
.status-pill--tier-premium {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* @deprecated local → info (custom/negotiated plan = acknowledged, active) */
.status-pill--tier-custom {
  background: var(--status-pill-tone-info-bg);
  color: var(--status-pill-tone-info-fg);
}


/* ---- Social Walk aliases ----
   Canonical home moved here from social-walk.css.
   social-walk.css copies are @deprecated (remove at W3 cleanup). */

/* @deprecated → neutral */
.status-pill--sw-novo,
.status-pill--sw-adm-preparing {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* @deprecated → success */
.status-pill--sw-postnat,
.status-pill--sw-project-ready,
.status-pill--sw-adm-postnat,
.status-pill--sw-quota-ok {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* @deprecated → progress */
.status-pill--sw-project-progress,
.status-pill--sw-adm-sent {
  background: var(--status-pill-tone-progress-bg);
  color: var(--status-pill-tone-progress-fg);
}

/* @deprecated → warning */
.status-pill--sw-quota-warn {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}

/* @deprecated → critical */
.status-pill--sw-quota-over {
  background: var(--status-pill-tone-critical-bg);
  color: var(--status-pill-tone-critical-fg);
}


/* ============================================================
   DARK MODE — nothing to do in this file
   ============================================================
   All nine tones resolve through --status-pill-tone-* , and those sixteen
   tokens are remapped wholesale for dark in design-system/theme.css. Every
   @deprecated alias above therefore inherits the dark treatment for free.

   This file used to carry two ad-hoc rules instead — .status-pill--info and
   --waiting forced to surface-sunk / stat-attention, marked "temporary
   fallback, replace with a proper dark-mode tint token in W3". They have been
   removed: the token remap supersedes them and covers the other seven tones,
   which the rules never did.

   The measured reason the other seven needed covering, and why the fills pin
   to their LIGHT values while only the tints invert, is written out in full at
   the status-pill block in theme.css. Read it before changing a tone value —
   the fill/tint split is what keeps info distinct from progress and waiting
   distinct from action in dark.
   ============================================================ */

/* ---- The claim mark on an order card (2026-08-24) ------------------
   A claim belongs to an order — it is filed at that order's URL and cannot exist
   without it — so its state is shown beside the order's own status rather than only
   in a profile section. DD 2026-08-24, on demoting Рекламации out of the browse
   list: «не знам дали да не го скрием някъде в поръчка само да се вижда».

   Deliberately NOT a status-pill. A pill states the order's own condition, and this
   is a link to a different object hanging off it; giving it pill clothing would put
   two contradictory statuses on one line («Доставена» + «Отворена») and leave the
   reader to work out which one describes the order. A quiet outlined link says
   «there is also a claim here» without competing. */

.order-card__marks {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
}

.order-card__claim {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-xs) var(--spacing-sm);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-family: var(--font-ui);
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  text-decoration: none;
}

.order-card__claim svg { width: 1em; height: 1em; }

.order-card__claim:hover {
  color: var(--color-text-accent);
  border-color: var(--color-accent-primary);
}

.order-card__claim:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}


/* ============================================================
   TONE MARK — the 1.4.1 distinction this file has promised since W2
   ============================================================
   The header of this file states:

     «action + warning share the same fill/ink. Distinction is
      semantic label + icon (WCAG 1.4.1):
        action  = ! icon (whose-move: viewer must act)
        warning = ⚠ icon (whose-move: degraded or deadline)»

   No icon was ever drawn. Grepped 2026-08-24: not one status-pill in
   the repo contains a child <svg>, and this file had no ::before. The
   base rule's `display: inline-flex; gap: var(--spacing-xs)` has been
   reserving a slot for a mark that does not exist.

   It shows on the panel. WIN_02_PRODUCT_LIST puts «ПУБЛИКУВАНО»,
   «В РАЗГЛЕЖДАНЕ», «ИЗИСКВА КОРЕКЦИИ» and «ЧАКА ОДОБРЕНИЕ» in one
   column: four pills of identical shape, two of them the same colour,
   separable only by reading them. The shelf's own rule from 2026-08-21
   — «silhouette reads at 20px; line-counting does not» — applies here
   and had not been carried across.

   Drawn in CSS, not in markup, so it reaches all 30 pill instances on
   the panel and every one on the buyer lane without a template change.
   `content: "…" / ""` supplies an EMPTY alternative text, so the mark is
   decorative to assistive technology — the pill's own label already says
   what it says, and a screen reader must not read «exclamation mark
   изисква корекции».
   ============================================================ */

/* «!» — WHOSE MOVE: the viewer's. Something is asked of the reader. */
.status-pill--action::before,
.status-pill--changes-requested::before {
  content: "!" / "";
  font-weight: var(--font-weight-ui-semibold);
  line-height: 1;
}

/* «\25F7» — WHOSE MOVE: someone else's. The reader is waiting, not acting.
   The file's own table splits action from warning by whose move it is; the
   waiting tones are a third answer to the same question and were unmarked,
   which is why WIN_02_PRODUCT_LIST showed «В РАЗГЛЕЖДАНЕ» and «ЧАКА
   ОДОБРЕНИЕ» as two identical amber rectangles. Both are `review`: the same
   variant and the same state under two different words. The mark now says
   so. The WORDING is a Design Director question, not a CSS one — filed. */
.status-pill--waiting::before,
.status-pill--pending::before,
.status-pill--review::before,
.status-pill--progress::before {
  content: "\25F7" / "";
  line-height: 1;
}

.status-pill--warning::before,
.status-pill--critical::before,
.status-pill--rejected::before {
  content: "\26A0" / "";
  line-height: 1;
}

.status-pill--success::before,
.status-pill--published::before,
.status-pill--delivered::before,
.status-pill--confirmed::before {
  content: "\2713" / "";
  line-height: 1;
}

/* ── status-pill__text — the word, when there is room for it ─────────────────
   DD 2026-08-31: «под мобилна да изчезва името му и да остава само иконката».
   Below the tablet breakpoint (tokens/breakpoints.json breakpoint.tablet =
   641px; literal because custom properties cannot be used in a media query)
   the word goes and the ::before mark stays.

   `display: none`, NOT the sr-only clip block — and that is the whole point of
   this comment. The obvious version of this rule was a sixth copy of the
   visually-hidden geometry that already lives in .sr-only, .chip__checkbox,
   .file-upload__native and .back-to-top__label; check-duplication caught it
   immediately and it was right to. Nothing is lost to assistive tech because
   the markup contract puts the state on the PILL:

       <span class="status-pill status-pill--published" aria-label="Публикуван">
         <span class="status-pill__text">Публикуван</span>
       </span>

   The pill's own accessible name carries the word at every width, so the span
   is free to be a purely visual element. aria-label is REQUIRED on any pill
   using this span — without it the state disappears on a phone for exactly the
   users who cannot see the ✓.

   Opt-in: a pill whose text is not wrapped in this span is unaffected, which is
   every pill in a table, where the word is the whole point. */
@media (max-width: 640px) {
  .status-pill__text { display: none; }
}
}

@layer components {
/* === components/delivery-badge/delivery-badge.css === */
/* ============================================================
   DELIVERY-BADGE
   Inline badge for delivery time estimates.
   Three active tiers: fast, normal, slow.
   «long» tier tokens defined but NOT emitted (pending DD ruling on «по заявка»).

   Component-local token (declared once):
     --_delivery-badge-padding-y  half of --badge-padding-y (--spacing-xs = 4px)
     No 2px spacing token exists; derived from the existing xs token via calc.

   Full-badge contrast pairs (light mode, WCAG relative luminance):
     fast    Moss #4A6B3C / White #FFFFFF              6.07:1 AA
     normal  Paper Fold #EFE6D2 / Walnut #6B5D4E       5.13:1 AA
     slow    Amber Bark #B8782A / Ink #1F1712          4.84:1 AA  [warning tone]

   Compact-badge contrast pairs (ink-on-surface, parchment + paper-fold verified):
     fast    Moss #4A6B3C on parchment                 5.35:1 AA; on paper-fold 4.89:1 AA
     normal  Walnut #6B5D4E on parchment               5.61:1 AA; on paper-fold 5.13:1 AA
     slow    #7A4E0E (--color-status-warning-text) on parchment 6.33:1 AA; on paper-fold 5.79:1 AA

   Every figure above RECOMPUTED S260831 from sRGB relative luminance. Seven of the nine were
   overstated by 0.04–0.41 — small, but a documented ratio is what a later author trusts INSTEAD
   of measuring, so a flattering number is worse than none. All pairs still clear AA; the tightest
   is the full slow pill at 4.84:1, which leaves 0.34 of headroom — do not darken Amber Bark
   further without re-running the numbers.

   All active pairs >= 4.5:1 AA normal text at --font-size-micro (12px / 600 weight).
   Amber Bark (#B8782A) as text on parchment = 3.21:1 (fails). Use --color-status-warning-text instead.
   No suffix text appended — computed text alone carries the meaning (DD ruling 2026-08-31).
   Dark mode: tier values route through --status-pill-tone-* tokens in theme.css.
   ============================================================ */

.delivery-badge {
  /* component-local: half of --badge-padding-y.
     --badge-padding-y = --spacing-xs = 4px → halved = 2px.
     Derived from an existing token via calc — no hardcoded px. */
  --_delivery-badge-padding-y: calc(var(--badge-padding-y) / 2);

  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--_delivery-badge-padding-y) var(--badge-padding-x);
  border-radius: var(--badge-radius);
  border: 1px solid transparent;
  font-family: var(--font-ui);
  font-size: var(--font-size-micro);
  font-weight: var(--font-weight-ui-semibold);
  line-height: var(--line-height-compact);
  /* readability floor: --spacing-md-plus = spacing.5 = 20px */
  min-height: var(--spacing-md-plus);
  white-space: nowrap;
}

.delivery-badge svg {
  flex-shrink: 0;
}

/* fast — <= 2 days. success tone. */
.delivery-badge--fast {
  background: var(--status-pill-tone-success-bg);
  color: var(--status-pill-tone-success-fg);
}

/* normal — 3-7 days. neutral tone, bordered (light bg needs edge). */
.delivery-badge--normal {
  background: var(--status-pill-tone-neutral-bg);
  color: var(--status-pill-tone-neutral-fg);
  border-color: var(--color-border-default);
}

/* slow — >= 8 days. warning/amber tone — more orange (DD ruling 2026-08-31). */
.delivery-badge--slow {
  background: var(--status-pill-tone-warning-bg);
  color: var(--status-pill-tone-warning-fg);
}

/* long — CURRENTLY UNUSED. Tokens stay defined pending DD decision on «по заявка».
   Do not emit this class. */
.delivery-badge--long {
  background: var(--status-pill-tone-critical-bg);
  color: var(--status-pill-tone-critical-fg);
}

/* ============================================================
   COMPACT VARIANT  --compact
   Feather truck glyph (16px, stroke-width 2) + computed text in tier colour.
   No background pill. Sits to the right of the price in the product card.
   <= 18px tall. aria-label on the span is the only accessible text.
   ============================================================ */

.delivery-badge--compact {
  background: none;
  border: none;
  padding: 0;
  min-height: 0;
  gap: var(--spacing-xs);
}

.delivery-badge__text {
  font-size: var(--font-size-micro);
  font-weight: var(--font-weight-ui-semibold);
  line-height: 1;
}

/* Compact tier colours: ink-on-page values (not pill fills).
   Amber Bark fails 4.5:1 at 12px text; use --color-status-warning-text (#7A4E0E). */
.delivery-badge--compact.delivery-badge--fast   { color: var(--color-status-success); }
.delivery-badge--compact.delivery-badge--normal { color: var(--color-text-muted); }
.delivery-badge--compact.delivery-badge--slow   { color: var(--color-status-warning-text); }
.delivery-badge--compact.delivery-badge--long   { color: var(--color-status-error); }
}

@layer components {
/* === components/account-menu/account-menu.css === */
/* ============================================================
   ACCOUNT MENU — logged-in dropdown on the public header
   Layer: components

   DD-ratified 2026-08-10. Replaces two separate header affordances:
     - the account icon, which was a bare link to /vhod/ with no logged-in state
     - the standalone theme toggle, which was visible to everyone

   Both now live behind this one trigger, and the trigger only becomes a menu
   when the page declares an authenticated session (see public.njk `isAuthed`).
   Anonymous visitors keep the plain link to /vhod/ and have no theme control.

   Markup contract (partials/account-menu.njk — do not rename):
     div.account-menu
       button.util-icon.account-menu__trigger[aria-expanded][aria-controls][aria-haspopup]
       div.account-menu__panel#account-menu-panel[hidden]
         div.account-menu__identity
           span.account-menu__monogram
           div.account-menu__who  → __name + __email
         ul.account-menu__list[role=list]
           li > a.account-menu__item  (span.account-menu__icon + label)
         div.account-menu__divider
         button.account-menu__item.account-menu__theme[role=switch][aria-checked]
         a.account-menu__item.account-menu__item--quiet  (Изход)

   Positioning: the panel is absolutely positioned against
   .header-public__utils (which header.css marks position:relative for exactly
   this), NOT against .account-menu. The utils cluster's right edge is the
   header container's right content edge, so `right: 0` aligns the panel with
   the page gutter at every width.

   Anchoring to .account-menu instead was the first attempt and it breaks on
   phones: the trigger is not the last icon in the cluster, so its right edge
   sits ~100px inside the viewport, and a 280px panel hung from there measured
   x = −6 at 390px and x = −76 at 320px — clipped off the left of the screen.
   ============================================================ */

.account-menu {
  /* Component-local custom properties for the role-mark / signed-in check badge.
     No token fits: 5px (nearest --spacing-1 = 4px), 13px (nearest --spacing-3 = 12px),
     1px (no spacing token). Declared once here; referenced by __check and __role-mark. */
  --account-menu-mark-size: 13px;
  --account-menu-mark-offset: 5px;
  --account-menu-mark-pad: 1px;
  display: flex;
  align-items: center;
}

/* The trigger reuses .util-icon geometry from header.css (44px box, line icon).
   Only the button reset and the open-state affordance belong here. */
.account-menu__trigger {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font: inherit;
  position: relative;
}

/* The signed-in mark (DD 2026-08-20, from the demo). It rides the person icon's
   lower-right instead of taking a header slot of its own.

   The ring is --color-surface-primary because that is what .header-public is
   painted with (header.css) — matching the ground makes the tick read as cut out
   of the icon rather than stuck on it. surface-raised was tried first and showed
   as a grey sticker against the cream header. */
.account-menu__check {
  position: absolute;
  right: var(--account-menu-mark-offset);
  bottom: var(--account-menu-mark-offset);
  width: var(--account-menu-mark-size);
  height: var(--account-menu-mark-size);
  padding: var(--account-menu-mark-pad);
  border-radius: var(--radius-full);
  background: var(--color-surface-primary);
  color: var(--color-status-success);
}

/* Open state: the trigger holds the panel's surface colour so the two read as
   one object rather than as a button with a floating card beside it.

   Ink is --color-text-accent, NOT --color-accent-primary. The two are the same
   maroon in light theme, but in dark theme accent-primary stays #8B2D3D while
   the surfaces invert — a 1.5px stroke icon on --color-surface-raised measures
   below the 3:1 graphical minimum there. text-accent is the remapped pair.
   This is the D-02 trap already documented against .profile-sidebar's hover
   rule and .rate-wine__action; it is not a new discovery, it is a repeat. */
.account-menu__trigger[aria-expanded="true"] {
  background: var(--color-surface-raised);
  color: var(--color-text-accent);
}

/* ── Panel ──────────────────────────────────────────────────── */

/* min() width: the panel wants 280px, but its anchor edge is not the viewport
   edge — the hamburger (44px), its gap (16px) and the container gutter (16px)
   all sit to the right of it, so 76px of the screen is already spent before
   the panel starts, and a 320px phone leaves 244px. 96px reserves that 76px
   plus a 20px gutter on the left. Measured at 320px: the panel renders 224px
   wide starting at x=28 with no horizontal scroll; at 390px and above the
   280px preference wins. A dropdown must never widen the document. */
.account-menu__panel {
  position: absolute;
  top: calc(100% + var(--spacing-xs));
  right: 0;
  z-index: var(--z-index-dropdown);
  width: min(280px, calc(100vw - 96px));
  padding: var(--spacing-sm) 0;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-floating);
  text-align: left;
}

/* [hidden] must be re-asserted: the block display above would otherwise beat
   the UA stylesheet's `[hidden] { display: none }` and the panel would render
   open with JS disabled. */
.account-menu__panel[hidden] {
  display: none;
}

/* ── Identity block ─────────────────────────────────────────── */

.account-menu__identity {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) var(--spacing-md) var(--spacing-md);
  border-bottom: 1px solid var(--color-border-default);
  margin-bottom: var(--spacing-sm);
}

/* Monogram, not an avatar. Same editorial device the person-card already uses
   as its portrait fallback (design-direction.md, photography section) — display
   italic initials in a gold hairline ring. Reusing it keeps one answer to
   "we have no photograph of this person" across the whole system. */
.account-menu__monogram {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-text-accent);
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--font-size-small);
  color: var(--color-text-accent);
  line-height: 1;
}

.account-menu__who {
  min-width: 0;
}

.account-menu__name {
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-medium);
  color: var(--color-text-primary);
}

/* The email is the disambiguator when someone holds two accounts, so it must
   stay on one line and truncate rather than wrap into a second row that pushes
   the whole menu taller. */
.account-menu__email {
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Items ──────────────────────────────────────────────────── */

.account-menu__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* One rule for links and for the theme switch: they sit in the same column and
   any difference in height or padding between them reads as a rendering fault.
   width:100% + border:0 + background:none are the button reset the <button>
   variant needs; they are inert on the <a> variant. */
.account-menu__item {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  width: 100%;
  padding: var(--spacing-sm) var(--spacing-md);
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--font-size-small);
  color: var(--color-text-primary);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--easing-standard),
              color var(--duration-fast) var(--easing-standard);
}

/* Hover ink is --color-text-accent. --color-accent-primary was measured at
   2.27:1 against --color-surface-sunk in dark theme — a hard AA failure on the
   one state a pointer user sees most. text-accent measures 6.6:1 on the same
   pair. Same D-02 trap as the trigger above. */
.account-menu__item:hover,
.account-menu__item:focus-visible {
  background: var(--color-surface-sunk);
  color: var(--color-text-accent);
  text-decoration: none;
}

.account-menu__item[aria-current="page"] {
  color: var(--color-text-accent);
  font-weight: var(--font-weight-ui-medium);
}

/* Изход is a destructive-adjacent action and the only item that leaves the
   account, so it is set quieter than the navigation above it rather than
   louder — an emphasised sign-out invites the misclick it warns about. */
.account-menu__item--quiet {
  color: var(--color-text-muted);
}

.account-menu__icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-size-ui);
  height: var(--icon-size-ui);
  color: var(--color-text-muted);
}

.account-menu__icon svg {
  width: 100%;
  height: 100%;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.account-menu__item:hover .account-menu__icon,
.account-menu__item:focus-visible .account-menu__icon {
  color: currentColor;
}

.account-menu__divider {
  height: 1px;
  margin: var(--spacing-sm) 0;
  background: var(--color-border-default);
}

/* ── Theme switch ───────────────────────────────────────────── */

/* role="switch" rather than a link: this changes a setting in place, it does
   not navigate. The track is the only affordance that says "this stays on/off",
   which a plain icon button does not. */
.account-menu__theme {
  justify-content: flex-start;
}

.account-menu__theme-label {
  flex: 1;
  min-width: 0;
}

/* The track is this control's state indicator, so WCAG 1.4.11 applies: it needs
   3:1 against the panel behind it. --color-border-strong measured 2.34:1 in
   light and 1.93:1 in dark — the off state was close to invisible, which for a
   switch means the control reads as having no state at all. --color-text-muted
   is the quietest token that clears the bar (6.36 / 5.52) and still keeps the
   knob, --color-surface-primary, clearly separated from it. */
.account-menu__switch {
  flex-shrink: 0;
  position: relative;
  width: 34px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-text-muted);
  transition: background var(--duration-fast) var(--easing-standard);
}

.account-menu__switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: var(--radius-full);
  background: var(--color-surface-primary);
  transition: transform var(--duration-fast) var(--easing-standard);
}

/* On-state track. --color-accent-primary is the intuitive choice — it is the
   primary button's fill — and it fails: measured 1.93:1 against the panel in
   dark theme and 2.27:1 against its own knob, i.e. the "on" state was less
   visible than the "off" state it replaced. --color-text-accent is the
   theme-remapped pair and is identical to accent-primary in light theme, so
   the light rendering is unchanged and only dark is repaired.
   (Light 11.18 / dark 5.13 against the panel; knob separation 9.86 / 6.03.) */
.account-menu__theme[aria-checked="true"] .account-menu__switch {
  background: var(--color-text-accent);
}

.account-menu__theme[aria-checked="true"] .account-menu__switch::after {
  transform: translateX(16px);
}

/* The knob is the only motion in the menu; honour the global preference. */
@media (prefers-reduced-motion: reduce) {
  .account-menu__switch,
  .account-menu__switch::after {
    transition: none;
  }
}

/* ── Transparent header (hero pages) ────────────────────────── */

/* header.css lightens every util icon over a dark hero. The trigger inherits
   that, but the open panel is an opaque surface and must NOT — its contents
   would otherwise be near-white ink on a parchment card. */
.header-public--transparent .account-menu__panel {
  color: var(--color-text-primary);
  text-shadow: none;
}

/* ── Panel variant — W8-4 (2026-08-30) ─────────────────────── */

/* Replaces .dashboard__topbar-winery text in dashboard.njk / admin.njk.
   The public header's user icon is the pattern (DD ruling 3, 2026-08-30):
   same icon, role variant via a small element, with a submenu. */

/* Role mark rides the icon lower-right, same position as __check.
   Two DIFFERENT SHAPES (not colour-only) satisfy WCAG 1.4.1:
     winery → the existing moss check (reused from the public signed-in mark)
     admin  → a shield glyph in the accent colour
   __role-mark is the common wrapper; the modifier sets the glyph. */
.account-menu__role-mark {
  position: absolute;
  right: var(--account-menu-mark-offset);
  bottom: var(--account-menu-mark-offset);
  width: var(--account-menu-mark-size);
  height: var(--account-menu-mark-size);
  padding: var(--account-menu-mark-pad);
  border-radius: var(--radius-full);
  background: var(--color-surface-raised);
  display: flex;
  align-items: center;
  justify-content: center;
}

.account-menu__role-mark--winery {
  color: var(--color-status-success);
}

.account-menu__role-mark--admin {
  color: var(--color-accent-primary);
}

/* Panel variant: needs position:relative so the absolutely-positioned panel
   dropdown positions against this container (not the viewport / ICB, which
   happens when all ancestors are position:static as they are in dashboard
   and admin layouts — the public header avoids this via header-public__utils
   position:relative). */
.account-menu--panel {
  position: relative;
}

/* Panel identity block: name + role label (Enotale Cellar · Изба / Администратор).
   Replaces the monogram+email layout used on the public menu. */
.account-menu--panel .account-menu__identity {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-xs);
}

.account-menu__role-line {
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
}

/* Theme toggle stays a topbar control in the panel — it already exists there.
   Hide it inside the panel menu (the public rule moved it in because the public
   utility row was crowded; the panel topbar is not). */
.account-menu--panel .account-menu__theme {
  display: none;
}
}

@layer components {
/* === components/notif-inbox/notif-inbox.css === */
/* ============================================================
   NOTIF-INBOX
   W1 Panel Shell Wave — WIN_25_NOTIFICATIONS. Spec §3.

   Redesign targets:
   - Day-cluster headers (small-caps micro labels + hairline rule)
   - Compact ~60px rows with type-icon roundels (5 type variants)
   - Unread state: accent dot + subtle surface tint
   - Collapsed group row for consecutive same-type events
   - Scrollable chip filter row (extends .filter-chip, no fork)

   Class vocabulary:
     .notif-list              Outer list container
     .notif-day               Day cluster wrapper
     .notif-day__header       Day label + hairline rule
     .notif-row               Individual notification row
     .notif-row--unread       Unread modifier
     .notif-row--order        Type: order (rose-ink roundel)
     .notif-row--shipment     Type: shipment (slate-blue roundel)
     .notif-row--review       Type: review (walnut roundel)
     .notif-row--finance      Type: finance (moss roundel)
     .notif-row--system       Type: system (wine-burgundy roundel)
     .notif-row--collapsed    Collapsed group row (expandable)
     .notif-row__roundel      Type icon container
     .notif-row__icon         SVG icon inside roundel
     .notif-row__content      Title + body text block
     .notif-row__title        Row title (semibold when unread)
     .notif-row__body         One-line truncated body
     .notif-row__time         Right-aligned relative time
     .notif-row__unread-dot   Unread accent dot
     .notif-row__expand       Expand button for collapsed group
     .notif-chip-row          Scrollable filter chip container
     .notif-chip-row__inner   Flex scrollable strip
     .filter-chip__count      Count badge on chip (extends filter-chip.css)
   ============================================================ */

/* ── DAY CLUSTER ─────────────────────────────────────────── */

.notif-day {
  margin-bottom: var(--spacing-md);
}

.notif-day__header {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-sm) var(--spacing-md);
}

.notif-day__label {
  font-family: var(--typography-font-ui);
  font-size: var(--notif-inbox-day-label-size);
  font-weight: var(--notif-inbox-day-label-weight);
  /* D5-sweep: was --color-text-subtle (oak, ~3.41:1); day-label-size = 12px micro —
     fails AA even at semibold/uppercase. Muted (walnut) passes 4.5:1. */
  color: var(--color-text-muted);
  letter-spacing: var(--notif-inbox-day-label-spacing);
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
}

/* Hairline rule extending to the right */
.notif-day__rule {
  flex: 1;
  height: 1px;
  background: var(--color-border-default);
}

/* ── NOTIF LIST ──────────────────────────────────────────── */

.notif-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ── NOTIFICATION ROW ────────────────────────────────────── */

.notif-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-sm-plus) var(--spacing-md);
  min-height: var(--notif-inbox-row-min-height);
  border-bottom: 1px solid var(--color-border-default);
  background: var(--color-surface-primary);
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: background var(--duration-fast);
}

.notif-row:last-child {
  border-bottom: none;
}

/* Hover state (when row is a link or has interactive descendants) */
.notif-row:hover,
a.notif-row:hover {
  background: var(--color-surface-sunk);
  text-decoration: none;
}

/* Unread: paper-light tint + accent left indicator */
.notif-row--unread {
  background: var(--color-surface-alt);
}
.notif-row--unread:hover {
  background: var(--color-surface-sunk);
}

/* ── ROUNDEL ─────────────────────────────────────────────── */

.notif-row__roundel {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--notif-inbox-roundel-size);
  height: var(--notif-inbox-roundel-size);
  border-radius: var(--notif-inbox-roundel-radius);
  flex-shrink: 0;
  background: var(--color-text-subtle); /* fallback */
  color: var(--color-text-on-accent);
}

.notif-row__icon {
  display: block;
  flex-shrink: 0;
}

/* Type variants — roundel background colors
   All verified ≥3:1 on parchment (WCAG 1.4.11 non-text):
   order (rose-ink)      5.30:1 ✓
   shipment (slate-blue) 4.82:1 ✓
   review (walnut)       5.06:1 ✓
   finance (moss)        4.76:1 ✓
   system (wine-burgundy) 8.94:1 ✓
   White icon on all roundel colors:
   rose-ink  5.98:1 ✓, slate-blue 5.44:1 ✓,
   walnut    5.71:1 ✓, moss 5.37:1 ✓, wine-burgundy 10.09:1 ✓ */

.notif-row--order .notif-row__roundel {
  background: var(--notif-inbox-roundel-order-bg);
}
.notif-row--shipment .notif-row__roundel {
  background: var(--notif-inbox-roundel-shipment-bg);
}
.notif-row--review .notif-row__roundel {
  background: var(--notif-inbox-roundel-review-bg);
}
.notif-row--finance .notif-row__roundel {
  background: var(--notif-inbox-roundel-finance-bg);
}
.notif-row--system .notif-row__roundel {
  background: var(--notif-inbox-roundel-system-bg);
}

/* ── CONTENT BLOCK ───────────────────────────────────────── */

.notif-row__content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  min-width: 0;
}

.notif-row__title {
  font-family: var(--typography-font-ui);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-regular); /* read state */
  color: var(--color-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: var(--line-height-compact);
}

/* Unread: semibold title */
.notif-row--unread .notif-row__title {
  font-weight: var(--font-weight-ui-semibold);
}

.notif-row__body {
  font-family: var(--typography-font-ui);
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: var(--line-height-compact);
}

/* ── META BLOCK ──────────────────────────────────────────── */

.notif-row__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--spacing-xs);
  flex-shrink: 0;
}

.notif-row__time {
  font-family: var(--typography-font-ui);
  font-size: var(--font-size-micro);
  /* D5-sweep: was --color-text-subtle (12px micro, fails AA); muted passes 4.5:1. */
  color: var(--color-text-muted);
  white-space: nowrap;
  line-height: var(--line-height-compact);
}

/* Unread dot — accent indicator (not color-only: combined with bold title) */
.notif-row__unread-dot {
  display: none;
  width: var(--notif-inbox-unread-dot-size);
  height: var(--notif-inbox-unread-dot-size);
  border-radius: var(--radius-full);
  background: var(--color-accent-primary);
  flex-shrink: 0;
}

.notif-row--unread .notif-row__unread-dot {
  display: block;
}

/* ── COLLAPSED GROUP ROW ─────────────────────────────────── */
/*
  «3 нови оценки за „Хоризонт Шардоне"» — expandable summary row.
  The row is a button with aria-expanded. Static open/closed states.
  Full expand behavior is system-side JS.
*/

.notif-row--collapsed {
  cursor: pointer;
  background: var(--color-surface-sunk);
}
.notif-row--collapsed:hover {
  background: var(--color-surface-primary);
}

.notif-row__expand {
  /* Visual size: 28×28px. Touch target extended to ≥44px via ::before pseudo-element.
     28px = intentional visual constraint; actual tap area = 28 + (--spacing-sm × 2) = 44px. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: none;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--color-text-muted);
  flex-shrink: 0;
  position: relative;
  transition: background var(--duration-fast), border-color var(--duration-fast);
}
/* 44px touch target — expands by --spacing-sm (8px) on each side without growing visuals */
.notif-row__expand::before {
  content: '';
  position: absolute;
  inset: calc(0px - var(--spacing-sm));
}
.notif-row__expand:hover {
  background: var(--color-surface-primary);
  border-color: var(--color-border-strong);
}
.notif-row__expand:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}
/* Chevron rotation for expanded state */
.notif-row__expand[aria-expanded="true"] .expand-icon {
  transform: rotate(180deg);
}

/* Group count + expand button wrapper — for collapsed rows */
.notif-row__expand-group {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
}

/* Count pill: shows N-items-in-group signal alongside chevron */
.notif-row__group-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--spacing-md);
  height: var(--spacing-md);
  padding: 0 var(--spacing-xs);
  font-family: var(--typography-font-ui);
  font-size: var(--font-size-micro);
  font-weight: var(--font-weight-ui-semibold);
  border-radius: var(--radius-full);
  background: var(--color-border-default);
  color: var(--color-text-muted);
  line-height: 1;
  flex-shrink: 0;
  pointer-events: none;
}

/* Nested rows inside expanded group — indented */
.notif-group__inner {
  padding-left: calc(var(--notif-inbox-roundel-size) + var(--spacing-md) + var(--spacing-md));
  border-bottom: 1px solid var(--color-border-default);
}
.notif-group__inner .notif-row {
  padding-left: 0;
  border-bottom: none;
}
.notif-group__inner .notif-row:not(:last-child) {
  border-bottom: 1px solid var(--color-border-default);
}

/* ── CHIP FILTER ROW ─────────────────────────────────────── */
/*
  Extends .filter-chip / .filter-chip--active (filter-chip.css).
  No-wrap, horizontally scrollable, edge fade masks.
*/

.notif-chip-row {
  position: relative;
  overflow: hidden;
}

/* Edge fade — left */
.notif-chip-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--spacing-xl);
  background: linear-gradient(
    to right,
    var(--notif-inbox-chip-row-bg-fade) 0%,
    transparent 100%
  );
  z-index: var(--z-index-raised);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--duration-fast);
}

/* Edge fade — right */
.notif-chip-row::after {
  content: '';
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: var(--spacing-xl);
  background: linear-gradient(
    to left,
    var(--notif-inbox-chip-row-bg-fade) 0%,
    transparent 100%
  );
  z-index: var(--z-index-raised);
  pointer-events: none;
}

/* Show left fade when scrolled */
.notif-chip-row.is-scrolled-start::before { opacity: 1; }

.notif-chip-row__inner {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--spacing-sm);
  overflow-x: auto;
  padding: var(--spacing-md);
  padding-bottom: var(--spacing-sm); /* trim bottom scrollbar gap */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
}

.notif-chip-row__inner::-webkit-scrollbar {
  display: none; /* WebKit */
}

/* Chip count badge — extends .filter-chip with count number */
.filter-chip__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 16px;
  padding: 0 var(--spacing-xs);
  font-family: var(--typography-font-ui);
  font-size: var(--font-size-micro);
  font-weight: var(--font-weight-ui-semibold);
  border-radius: var(--radius-full);
  background: var(--color-border-default);
  color: var(--color-text-muted);
  margin-left: var(--spacing-xs);
  line-height: 1;
}

/* Active chip count inverts to match chip */
.filter-chip--active .filter-chip__count,
a.filter-chip--active .filter-chip__count {
  background: transparent;
  color: var(--color-text-on-accent);
  border: 1px solid var(--color-text-on-accent);
}

/* ── INBOX HEADER ────────────────────────────────────────── */
/*
  Page-level header for WIN_25 — not a component header, but
  provided here as a usage pattern. Implementation in page template.
*/

.notif-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-md);
  padding: var(--spacing-md) 0 var(--spacing-lg);
  flex-wrap: wrap;
}

.notif-header__actions {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

/* ── TOUCH TARGETS ───────────────────────────────────────── */

/* Ensure rows used as links meet 44px touch target */
a.notif-row {
  min-height: max(var(--notif-inbox-row-min-height), 44px);
}

/* ── RESPONSIVE ──────────────────────────────────────────── */

/* On mobile (≤640px): allow row title to wrap to 2 lines so order
   references like «Нова поръчка PR-100040-1» don't ellipsize. */
@media (max-width: 640px) {
  .notif-row__title {
    white-space: normal;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
  }
}

/* ── REDUCED MOTION ──────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .notif-row,
  .notif-chip-row::before,
  .notif-chip-row::after,
  .notif-row__expand {
    transition: none;
  }
}
}

@layer components {
/* === components/form-field/form-field.css === */
/* ============================================================
   FORM FIELDS
   ============================================================ */

.form-group { margin-bottom: var(--spacing-lg); }

.form-label {
  display: block;
  font-size: var(--input-label-font-size);
  font-weight: var(--input-label-font-weight);
  color: var(--input-label-color);
  margin-bottom: var(--spacing-sm);
}

.form-input,
.form-select,
.form-textarea {
  display: block;
  width: 100%;
  min-width: 0;
  min-height: var(--input-min-height);
  padding: var(--input-padding-y) var(--input-padding-x);
  font-family: var(--input-font-family);
  font-size: var(--input-font-size);
  color: var(--input-text);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: var(--input-radius);
  transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
}
.form-input:hover, .form-select:hover, .form-textarea:hover { border-color: var(--input-border-hover); }
.form-input:focus, .form-select:focus, .form-textarea:focus {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: var(--input-focus-ring);
}

/* --compact — the tighter control that three screens invented independently
   (.unpublish-confirm__select on ADM_13/ADM_21, .catalog-edit-panel__input on
   ADM_15, .iban-edit-form__input on ADM_11), each re-declaring the whole
   control to change one thing: the vertical padding. It belongs in an inline
   panel opened under a table row, where the standard control's height pushes
   the row it is amending off screen. Two of the three carried no library class
   at all and so had lost :hover and --input-focus-ring, hand-rolling a
   different :focus-visible outline each. REVIEW 260825. */
.form-input--compact,
.form-select--compact,
.form-textarea--compact {
  min-height: 0;
  padding: var(--input-padding-y-compact) var(--input-padding-x-compact);
}

/* --mono for account numbers and references, where the reader is comparing
   digits rather than reading words (ADM_11's IBAN and contact fields). */
.form-input--mono { font-family: var(--font-mono, monospace); }

.form-textarea { min-height: 120px; resize: vertical; }
.form-hint { font-size: var(--font-size-micro); color: var(--input-hint-color); margin-top: var(--spacing-xs); }
.form-error { font-size: var(--font-size-micro); color: var(--input-error-color); margin-top: var(--spacing-xs); }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg); }



/* ============================================================
   PLACEHOLDERS — DD 2026-08-21: «placeholder почти навсякъде ти изглежда като
   реален попълнен текст».

   Measured before touching it: 99 placeholder attributes across the templates and
   exactly THREE components styling ::placeholder at all. The base field styled it
   nowhere, so almost every input rendered the browser default — a dark grey that
   reads as a value.

   Why this is one rule and not a colour tweak: a placeholder is TEXT, so it owes
   4.5:1. Going lighter to distinguish it fails that floor (oak was 3.88:1 and the
   token's own note wrongly claimed AA Large); going darker passes and looks even
   more filled. Contrast cannot carry this distinction. ITALIC can, and it costs no
   contrast at all. Our fields all carry a real <label>, so the placeholder is an
   example rather than a name — which is what makes italic honest here rather than
   decorative.
   ============================================================ */
.form-input::placeholder,
.form-select::placeholder,
.form-textarea::placeholder {
  color: var(--input-placeholder);
  font-style: italic;
  opacity: 1;                 /* Firefox dims placeholders by default */
}

/* ── fieldset chrome reset (W12, 2026-08-31) ────────────────────────────────
   A <fieldset> grouping related controls is a SEMANTIC wrapper, not a visual
   one. The UA border + inset legend is the «UA-bordered tiles» look the DD
   struck on W9 — WIN_03 was suppressing it with an inline style on every
   fieldset, which is a page-local hack for a library-level fact. One rule
   instead of N inline styles.

   `min-width: 0` is not optional: the UA gives fieldset an implicit
   min-content width that breaks any grid or flex child inside it.

   The legend is NOT restyled here — markup carries `class="form-label"` so
   label typography has exactly one definition (above). Only the UA's own
   horizontal padding on <legend> is cleared. */
fieldset.form-group,
fieldset.form-fieldset {
  border: 0;
  padding: 0;
  min-width: 0;
}
fieldset.form-group > legend,
fieldset.form-fieldset > legend {
  padding-inline: 0;
}

/* ── helper text under a control ────────────────────────────────────────────
   Promoted from four page-local copies (WIN_03, WIN_19, ADM_24 ×2) that were
   character-identical. Same defect class check-duplication caught on
   .pdp-buy__trust-item — found by hand here because the four copies each sat
   inside a <style> block, where the CSS gates do not look. W12, 2026-08-31. */
.form-helper-text {
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  margin-top: var(--spacing-xs);
}
/* «Промените в тези полета изискват ново одобрение» — the approval-locked
   marker. The sentence carries the meaning; the tone only reinforces it.

   --color-status-warning-text, NOT --color-status-warning: the graphical amber
   (#B8782A light) is 3.38:1 as text on a card and fails AA. All four page-local
   copies this rule was promoted from carried that defect — it is what produced
   the eight light-mode contrast failures the gate had recorded against
   /izba/panel/vina/novo/. The text token (#7A4E0E light / #D4924A dark) is the
   one delivery-badge and stat-summary-strip already document for exactly this
   case. W12, 2026-08-31. */
.form-helper-text--approval {
  color: var(--color-status-warning-text);
  font-weight: var(--font-weight-ui-medium);
}

/* ── «⏳ след одобрение» — the approval-locked field mark ────────────────────
   Sits inside the <label>, after the field name. Text, not colour alone, and
   not a title attribute: a tooltip never reaches touch or keyboard users, and
   this is the one thing on the field that says a change here takes the wine
   out of sale until Enotale approves it again. W12, 2026-08-31. */
.form-label__lock {
  margin-left: var(--spacing-xs);
  font-size: var(--font-size-micro);
  font-weight: var(--font-weight-ui-regular);
  color: var(--color-status-warning-text);
  white-space: nowrap;
}
}

@layer components {
/* === components/file-upload/file-upload.css === */
/* ============================================================
   FILE UPLOAD
   Replaces native "Choose File / No file chosen" with a
   Bulgarian-language styled trigger. The hidden <input type="file">
   stays in the DOM for Livewire wire:model binding.
   Focus ring lives on .file-upload__trigger via :focus-within.
   ============================================================ */

/* ── Wrapper ─────────────────────────────────────────────── */

.file-upload {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

/* ── Trigger row (button + idle status) ──────────────────── */

.file-upload__trigger-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
}

/* ── Styled trigger label ────────────────────────────────── */
/*
   The <label> contains the hidden <input> implicitly.
   Clicking anywhere on the label opens the OS file picker.
   :focus-within activates when the hidden input inside gains focus.
   Touch target: min-height 44px enforced by --file-upload-trigger-min-height.
*/

.file-upload__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-xs);
  min-height: var(--file-upload-trigger-min-height);
  padding: var(--file-upload-trigger-padding-y) var(--file-upload-trigger-padding-x);
  font-family: var(--input-font-family);
  font-size: var(--file-upload-trigger-font-size);
  font-weight: var(--file-upload-trigger-font-weight);
  color: var(--file-upload-trigger-text);
  background: var(--file-upload-trigger-bg);
  border: 1px solid var(--file-upload-trigger-border);
  border-radius: var(--file-upload-trigger-radius);
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  flex-shrink: 0;
  transition:
    color var(--motion-duration-fast),
    background-color var(--motion-duration-fast),
    border-color var(--motion-duration-fast);
}

.file-upload__trigger:hover {
  background-color: var(--file-upload-trigger-bg-hover);
  color: var(--file-upload-trigger-text-hover);
  border-color: var(--file-upload-trigger-border-hover);
}

/* Focus ring via :focus-within — real focus is on the hidden input inside */
.file-upload__trigger:focus-within {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: var(--file-upload-trigger-focus-ring);
}

/* ── Accessible clip — hidden native input ───────────────── */
/*
   The native input is visually hidden but NOT display:none.
   Livewire binds to this element via wire:model.
   The label click triggers it; keyboard Tab focuses it.
   <noscript> in HTML reveals it when JS is off.
*/

.file-upload__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Status text ─────────────────────────────────────────── */

.file-upload__status {
  font-family: var(--input-font-family);
  font-size: var(--file-upload-trigger-font-size);
  color: var(--file-upload-status-color);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* In chosen state the status is cleared by JS; filename chip takes over */
.file-upload--chosen .file-upload__status {
  display: none;
}

/* ── Chosen row (file chip + optional preview) ───────────── */

.file-upload__chosen-row {
  display: none;
  align-items: center;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
}

.file-upload--chosen .file-upload__chosen-row {
  display: flex;
}

/* ── Preview blocks ──────────────────────────────────────── */

.file-upload__preview {
  display: none;
  flex-shrink: 0;
}

/* Image preview — shown when file is an image type */
.file-upload--preview .file-upload__preview--image {
  display: block;
}

/* Document glyph — shown when file is a document type (PDF, DOCX, etc.) */
.file-upload--document .file-upload__preview--document {
  display: flex;
  align-items: center;
  color: var(--file-upload-doc-glyph-color);
}

.file-upload__preview-img {
  display: block;
  width: var(--file-upload-preview-img-size);
  height: var(--file-upload-preview-img-size);
  object-fit: cover;
  border-radius: var(--file-upload-preview-img-radius);
  background-color: var(--input-bg-disabled);
}

.file-upload__doc-icon {
  width: var(--file-upload-doc-glyph-size);
  height: auto;
  color: var(--file-upload-doc-glyph-color);
}

/* ── Filename chip ───────────────────────────────────────── */

.file-upload__chip {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  background: var(--file-upload-chip-bg);
  border: 1px solid var(--file-upload-chip-border);
  border-radius: var(--file-upload-chip-radius);
  padding: var(--file-upload-chip-padding-y) var(--file-upload-chip-padding-x);
  min-width: 0;
}

.file-upload__filename {
  font-family: var(--input-font-family);
  font-size: var(--file-upload-trigger-font-size);
  color: var(--file-upload-chip-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  max-width: var(--file-upload-filename-max-width);
}

/* ── Remove button ───────────────────────────────────────── */

.file-upload__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--spacing-lg);
  height: var(--spacing-lg);
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--file-upload-chip-radius);
  color: var(--file-upload-remove-color);
  cursor: pointer;
  flex-shrink: 0;
  transition: color var(--motion-duration-fast);
}

.file-upload__remove:hover {
  color: var(--file-upload-remove-color-hover);
}

.file-upload__remove:focus-visible {
  outline: none;
  box-shadow: var(--input-focus-ring);
}

/* ── Upload progress ─────────────────────────────────────── */
/*
   Visible only during Livewire upload (class added by Alpine or JS).
   role="status" on the element = polite screen reader announcement.
   Width of .file-upload__progress-fill is set by JS/Alpine.
*/

.file-upload__progress {
  display: none;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.file-upload--uploading .file-upload__progress {
  display: flex;
}

.file-upload__progress-track {
  width: 100%;
  height: var(--file-upload-progress-height);
  background: var(--file-upload-progress-track-bg);
  border-radius: var(--file-upload-progress-radius);
  overflow: hidden;
}

.file-upload__progress-fill {
  height: 100%;
  width: 0%;
  background: var(--file-upload-progress-fill-bg);
  border-radius: var(--file-upload-progress-radius);
  transition: width var(--motion-duration-base);
}

.file-upload__progress-text {
  font-family: var(--input-font-family);
  font-size: var(--typography-size-micro);
  color: var(--input-hint-color);
}

/* ── Transport error ─────────────────────────────────────── */
/*
   Triggered by livewire-upload-error (network failure, hard size limit).
   Distinct from server validation error (.file-upload__error below).
   role="alert" = assertive screen reader announcement.
*/

.file-upload__transport-error {
  display: none;
  align-items: flex-start;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--file-upload-transport-error-bg);
  border: 1px solid var(--file-upload-transport-error-border);
  border-radius: var(--file-upload-transport-error-radius);
  color: var(--file-upload-transport-error-text);
}

.file-upload--transport-error .file-upload__transport-error {
  display: flex;
}

.file-upload__transport-error-icon {
  flex-shrink: 0;
  margin-top: var(--file-upload-transport-error-icon-offset); /* optical alignment to first text baseline */
}

.file-upload__transport-error-text {
  font-family: var(--input-font-family);
  font-size: var(--typography-size-micro);
  line-height: var(--line-height-compact);
}

/* ── Constraint hint (always visible) ───────────────────── */
/* Extends .form-hint from form-field.css — no extra rules needed */

/* ── Server validation error ─────────────────────────────── */
/*
   Extends .form-error from form-field.css.
   Appears after Livewire-upload-finish when server-side @error fires.
   Empty by default; CSS hides it when empty.
*/

.file-upload__error:empty {
  display: none;
}

/* ── Error border on the zone ─────────────────────────────── */
/*
   When a validation error is present, give the trigger row a visual signal.
   Non-colour: the trigger already uses --input-border-error colour but the
   error text below is the primary non-colour indicator.
*/

.file-upload:has(.file-upload__error:not(:empty)) .file-upload__trigger,
.file-upload--validation-error .file-upload__trigger {
  border-color: var(--input-border-error);
}

/* ── Disabled state ──────────────────────────────────────── */

.file-upload__trigger:has(.file-upload__native:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ── 375px baseline ──────────────────────────────────────── */

@media (max-width: 480px) {
  .file-upload__trigger-row {
    flex-direction: column;
    align-items: flex-start;
  }
  .file-upload__trigger {
    width: 100%;
    justify-content: center;
  }
  .file-upload__status {
    white-space: normal;
  }
  .file-upload__filename {
    max-width: var(--file-upload-filename-max-width-mobile);
  }
}

/* ── Reduced motion ──────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .file-upload__trigger,
  .file-upload__remove,
  .file-upload__progress-fill {
    transition: none;
  }
}
}

@layer components {
/* === components/form-select/form-select.css === */
/* ============================================================
   FORM SELECT — styled closed control
   Extends .form-select from form-field.css.
   Replaces the native chevron with a custom SVG via mask-image.
   The open option list is OS-rendered and cannot be styled.

   WRAPPER REQUIREMENT:
   .form-select-wrapper is required for the custom chevron.
   <select> is a replaced element — ::after does not apply to it directly.
   A bare .form-select still renders correctly with the native OS chevron.

   CHEVRON TECHNIQUE:
   The ::after pseudo-element on .form-select-wrapper is positioned absolutely.
   Its color comes from background-color: var(--form-select-chevron-color) — a token.
   The SVG data URI is used only as a mask shape (alpha channel); the SVG's own
   stroke color is irrelevant. This keeps the color value entirely in token space.
   ============================================================ */

/* ── Wrapper ─────────────────────────────────────────────── */

.form-select-wrapper {
  position: relative;
  display: block;
}

/* ── Select within wrapper ───────────────────────────────── */
/*
   appearance:none removes the native OS chevron.
   Extra right padding clears the custom chevron.
   All other box-model, typography, border, and focus rules
   are inherited from .form-select in form-field.css.
*/

.form-select-wrapper > .form-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--form-select-padding-right-extra);
  cursor: pointer;
}

/* ── Custom chevron ──────────────────────────────────────── */
/*
   Positioned absolute, vertically centred, right-aligned.
   pointer-events:none so it does not intercept clicks on the select.
   mask-image shape: filled triangle / chevron-down.
   background-color provides the visible color — fully token-referenced.
*/

.form-select-wrapper::after {
  content: '';
  position: absolute;
  right: var(--input-padding-x);
  top: 50%;
  transform: translateY(-50%);
  width: var(--form-select-chevron-size);
  height: var(--form-select-chevron-size);
  pointer-events: none;
  flex-shrink: 0;

  background-color: var(--form-select-chevron-color);

  /* Chevron-down shape as alpha mask.
     The SVG stroke color (#000) is irrelevant to the displayed color;
     it exists only to define the mask alpha channel. */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 5.5L8 10.5L12.5 5.5' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;

  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 5.5L8 10.5L12.5 5.5' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E");
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;

  transition: background-color var(--motion-duration-fast);
}

/* ── Error state ─────────────────────────────────────────── */

.form-select.is-error,
.form-select[aria-invalid="true"] {
  border-color: var(--input-border-error);
}

/* Chevron colour shifts on error */
.form-select-wrapper:has(.form-select.is-error)::after,
.form-select-wrapper:has(.form-select[aria-invalid="true"])::after {
  background-color: var(--form-select-chevron-color-error);
}

/* ── Disabled state ──────────────────────────────────────── */

.form-select-wrapper > .form-select:disabled {
  background-color: var(--input-bg-disabled);
  color: var(--color-text-subtle);
  cursor: not-allowed;
  opacity: 1; /* Override UA opacity so we control it fully */
  border-color: var(--input-border);
}

.form-select-wrapper:has(.form-select:disabled)::after {
  background-color: var(--form-select-chevron-color-disabled);
}

/* ── Focus ───────────────────────────────────────────────── */
/*
   Focus styles on the <select> itself: form-field.css provides
   .form-select:focus { border-color, box-shadow }.
   No override needed here; focus-within cannot be used on the wrapper
   because select focus does not propagate as :focus-within in all browsers.
*/

/* ── optgroup ────────────────────────────────────────────── */
/*
   The OS renders optgroup content. Only the label font-weight
   is slightly respected on some platforms (Chrome on macOS partially;
   Firefox mostly). This is declarative intent, not a guarantee.
*/

.form-select > optgroup {
  font-weight: var(--font-weight-ui-semibold);
  font-style: normal;
}

/* ── Compact size variant (filter bars) ──────────────────── */
/*
   Used in filter-bar rows where vertical space is tight.
   min-height: auto — let content and padding define height.
   Smaller padding; same font-size as body to avoid iOS zoom.
*/

.form-select-wrapper--sm > .form-select {
  min-height: auto;
  padding-top: var(--spacing-xs);
  padding-bottom: var(--spacing-xs);
  padding-left: var(--spacing-sm);
  padding-right: var(--form-select-padding-right-extra);
  font-size: var(--typography-size-small);
}

/* ── 375px baseline ──────────────────────────────────────── */

@media (max-width: 480px) {
  .form-select-wrapper > .form-select {
    font-size: var(--input-font-size); /* 16px — prevent iOS zoom on focus */
  }
}

/* ── Reduced motion ──────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .form-select-wrapper::after {
    transition: none;
  }
}
}

@layer components {
/* === components/split-layout/split-layout.css === */
/* ============================================================
   RESPONSIVE SPLIT LAYOUTS
   Two-column layouts that collapse to a single column on mobile.
   minmax(0, …) lets each track shrink below its content's
   intrinsic width, so long strings (IBANs, prices) never force
   horizontal page overflow before the collapse breakpoint.
   Replaces hardcoded inline `grid-template-columns` in templates.
   ============================================================ */
.split-detail {                 /* main content + sidebar (order / product review) */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 340px);
  gap: var(--spacing-xl);
  align-items: start;
}
.split-summary {                /* cart / checkout: content + order summary */
  display: grid;
  grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
  gap: var(--spacing-xl);
  align-items: start;
}
.split-even {                   /* generic two-up that should stack on mobile */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--spacing-md);
  align-items: start;
}

/* Auto-collapsing metric grid — no fixed column count, no breakpoint needed. */
.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--spacing-md);
}

/* Label/value spec list (replaces inline `140px 1fr` dl grids).
   Flexible label column + shrinkable value column so long values never
   force overflow; stacks to a single column on small phones. */
.spec-list {
  display: grid;
  grid-template-columns: minmax(80px, max-content) minmax(0, 1fr);
  gap: var(--spacing-sm) var(--spacing-lg);
}
@media (max-width: 480px) {
  .spec-list { grid-template-columns: 1fr; gap: var(--spacing-xs); }
  .spec-list dt { margin-top: var(--spacing-sm); }
  .spec-list dt:first-child { margin-top: 0; }
}
}

@layer components {
/* === components/pagination/pagination.css === */
/* ============================================================
   PAGINATION
   ============================================================ */

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-xl) 0;
}

.pagination__item {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-small);
  color: var(--color-text-primary);
  text-decoration: none;
  transition: background var(--duration-fast), border-color var(--duration-fast);
}
.pagination__item:hover { border-color: var(--color-accent-primary); color: var(--color-accent-primary); text-decoration: none; }
.pagination__item--active { background: var(--color-accent-primary); border-color: var(--color-accent-primary); color: var(--color-text-on-accent); }
}

@layer components {
/* === components/breadcrumb/breadcrumb.css === */
/* ============================================================
   BREADCRUMB
   ============================================================ */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
  padding: var(--spacing-md) 0;
}

.breadcrumb a { color: var(--color-text-muted); text-decoration: none; }
/* D-02 residual (2026-08-05): accent-as-text wants --color-text-accent, not the
   fill token (2.02:1 on dark surface-raised). Identical in light. */
.breadcrumb a:hover { color: var(--color-text-accent); text-decoration: underline; }
.breadcrumb__sep { color: var(--color-text-subtle); }
.breadcrumb__current { color: var(--color-text-primary); }

/* ------------------------------------------------------------
   COMPACT PATHWAY — DD 2026-08-20: «по-малък шрифт и по-малко разстояние от заглавието»
   The breadcrumb was body-small with generous padding, so on a 390px screen the route
   you already walked took more vertical room than the first product card's name.
   Authored in search-variants.css; moved here at the bundle split (260827) because it
   is unscoped and every panel page carries a breadcrumb too. Cascade order is unchanged
   for the pair that matters (breadcrumb base rule above → this override below).
   ------------------------------------------------------------ */
.breadcrumb {
  font-size: var(--font-size-micro);
  padding-top: var(--spacing-sm);
  padding-bottom: 0;
}
@media (max-width: 767px) {
  .breadcrumb { padding-top: var(--spacing-xs); gap: var(--spacing-xs); }
}
}

@layer components {
/* === components/bundle-selector/bundle-selector.css === */
/* ============================================================
   BUNDLE SELECTOR — pack-first evolution (S260830, ADR 0010)
   Segmented picker for wine.packs options.
   role="radiogroup" + role="radio" + aria-checked pattern.
   See README.md for the aria-pressed-to-aria-checked rationale.

   color is declared, not inherited. A class that supplies its
   own BACKGROUND must supply its own FOREGROUND — the same rule
   the 2026-08-05 filter-chip exclusion list states. Without it
   this element paints a surface and then takes whatever ink its
   context happens to hand down: on /dizayn-sistema/ that was
   pure black, giving #000000 on #1A1410 = 1.15:1 in dark.

   R1 (DD 2026-08-30): border strategy changed.
   - Container uses overflow:hidden to clip buttons to its radius.
     This fixes the stray border at 2 and 4 options where the
     container border and the per-button border-right met at the
     corners visibly.
   - UA button border reset (border:0; appearance:none; font:inherit)
     removed the double-border at every tile edge.
   - Dividers now use border-inline-start on :not(:first-child)
     so only ONE rule edge exists between any two tiles.
   - Focus ring uses outline with negative offset so it is inset
     and not clipped by the container's overflow:hidden.
   - Padding reduced to --spacing-sm vertical / --spacing-md
     horizontal. __qty dropped to --font-size-small.
   ============================================================ */

.bundle-selector {
  display: flex;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  overflow: hidden;
  /* Component-local: no outline-width token exists in the system; the
     shadow-ring token (--shadow-focus-ring) is clipped by overflow:hidden
     so we use an inset outline. Width declared here so the focus rule
     does not contain a bare literal. */
  --bundle-selector-focus-width: 2px;
}

.bundle-option {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm) var(--spacing-md);
  text-align: center;
  cursor: pointer;
  color: var(--color-text-primary);
  background: var(--color-surface-primary);
  border: 0;
  appearance: none;
  font: inherit;
  border-inline-start: 1px solid var(--color-border-default);
  transition: background var(--duration-fast), color var(--duration-fast);
}

.bundle-option:first-child { border-inline-start: none; }

.bundle-option--active { background: var(--color-accent-primary); color: var(--color-text-on-accent); }
.bundle-option:hover:not(.bundle-option--active) { background: var(--color-surface-sunk); }

.bundle-option:focus-visible {
  outline: var(--bundle-selector-focus-width) solid var(--color-accent-primary);
  outline-offset: calc(-1 * var(--bundle-selector-focus-width));
  position: relative;
  z-index: 1;
}

/* M2: active tile — on-accent outline keeps focus visible (WCAG 2.4.7) */
.bundle-option--active:focus-visible { outline-color: var(--color-text-on-accent); }

/* Glyph row — bottle silhouettes from bottleCountGlyphs(n, 12). */
.bundle-option__glyphs {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-xs);
  line-height: 1;
}

.bundle-option__qty   { font-weight: var(--font-weight-ui-semibold); font-size: var(--font-size-small); }
.bundle-option__price { font-size: var(--font-size-micro); color: inherit; opacity: 0.8; }

/* 2x2 grid at <=480px when 4 options exist (e.g. packs:[1,2,3,6]).
   Apply .bundle-selector--stack in the template when options.length == 4. */
@media (max-width: 480px) {
  .bundle-selector--stack {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }

  /* Left column: no left divider */
  .bundle-selector--stack .bundle-option:nth-child(odd) {
    border-inline-start: none;
  }

  /* Top row: horizontal divider between rows */
  .bundle-selector--stack .bundle-option:nth-child(1),
  .bundle-selector--stack .bundle-option:nth-child(2) {
    border-block-end: 1px solid var(--color-border-default);
  }
}
}

@layer components {
/* === components/tabs/tabs.css === */
/* ============================================================
   TABS
   ============================================================ */

.tabs {
  display: flex;
  border-bottom: 2px solid var(--color-border-default);
  gap: 0;
  margin-bottom: var(--spacing-xl);
  /* Horizontal scroll instead of overflow when tabs exceed viewport (mobile). */
  overflow-x: auto;
  flex-wrap: nowrap;
  scrollbar-width: none;

  /*
   * Scroll-shadow affordance — signals that more tabs exist beyond the
   * right edge. Technique: dual background-attachment (local + scroll).
   *
   * Two radial-gradient layers are stacked per edge:
   *   - The "cover" layer uses background-attachment:local so it moves with
   *     the scroll position. At the left edge it sits over the left shadow;
   *     at the right edge it sits over the right shadow. When the container
   *     is scrolled to an edge the cover matches the page surface colour and
   *     hides that side's shadow — so the fade only appears where there IS
   *     more content to scroll to.
   *   - The "shadow" layers use background-attachment:scroll so they are
   *     fixed to the element box and always rendered, but the covers mask
   *     them at the natural scroll stops.
   *
   * The fade colour is var(--color-surface-primary) — the page background
   * token — so it remaps automatically for dark mode without any extra rule.
   * The gradient renders behind the flex children (below the tab buttons)
   * and behind the border-bottom underline, so neither the tab strip line
   * nor the active-tab indicator is affected.
   *
   * At viewports wide enough that no tabs overflow, the container does not
   * scroll at all: both covers sit on top of both shadows from the start,
   * so nothing is visible. No JS, no observer, pure CSS.
   */
  background:
    /* Right-edge cover: solid surface colour at right, fades left.
       Uses local attachment so it moves with the scrollable content.
       When scrolled fully right, this layer sits over the right shadow
       and hides it — telling the user there is nothing more to the right. */
    linear-gradient(to left, var(--color-surface-primary), transparent)
      right center / 40px 100% no-repeat local,
    /* Left-edge cover: solid surface colour at left, fades right.
       Hides the left shadow when scrolled fully left (start position). */
    linear-gradient(to right, var(--color-surface-primary), transparent)
      left center / 40px 100% no-repeat local,
    /* Right overflow shadow: fixed to element box, always rendered.
       Visible only when the right cover does not reach this position
       (i.e. when there is scrollable content to the right). */
    linear-gradient(to left, var(--color-border-default), transparent)
      right center / 40px 100% no-repeat scroll,
    /* Left overflow shadow: visible only when scrolled away from left edge. */
    linear-gradient(to right, var(--color-border-default), transparent)
      left center / 40px 100% no-repeat scroll;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  flex-shrink: 0;
  white-space: nowrap;
  padding: var(--spacing-sm) var(--spacing-lg);
  border: none;
  background: none;
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-semibold);
  color: var(--color-text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  text-decoration: none;
  transition: color var(--duration-fast), border-color var(--duration-fast);
}
.tab:hover { color: var(--color-accent-primary); text-decoration: none; }
.tab--active { color: var(--color-accent-primary); border-bottom-color: var(--color-accent-primary); }

/* ── Dark theme overrides — WCAG AA contrast fix
   accent-primary (#8B2D3D) on dark surface-primary (#1A1410) = 2.21:1 — FAIL (need ≥4.5:1 text).
   accent-link (#CB807C) on dark surface-primary (#1A1410) = 6.01:1 — PASS.
   ── */
[data-theme="dark"] .tab:hover { color: var(--color-accent-link); }
[data-theme="dark"] .tab--active { color: var(--color-accent-link); border-bottom-color: var(--color-accent-link); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tab:hover { color: var(--color-accent-link); }
  :root:not([data-theme="light"]) .tab--active { color: var(--color-accent-link); border-bottom-color: var(--color-accent-link); }
}


/* ── JS-switching variant (tabs--switch) ────────────────────────────────────
   The modifier is the hook tabs.js initialises on, and it is also what keeps
   the two variants apart: .tab-nav (anchor scroll, all panels in the DOM) and
   this one must never share a tablist. It carries no visual difference from
   .tabs — the strip looks the same whichever mechanism moves it — so the only
   declaration is the one that makes the distinction real to a reader of the
   compiled CSS.

   Added in the W12 BUILD: the component shipped with its README documenting
   both class names and its CSS defining neither, which check-undefined caught
   the moment the first five pages consumed it. */
.tabs--switch { position: relative; }

/* Tighter than the base tab: ten of them share one strip, and 24px of padding
   a side is most of why the set measures 1 116px — wider than the content
   column at every width below 1 440px, even after the labels were shortened to
   one word each. */
.tabs--switch .tab { padding-inline: var(--spacing-md); }

/* ── the switch strip WRAPS on desktop, SCROLLS on mobile ───────────────────
   DD 2026-08-31: «под мобилно ги скролвам, но под десктоп с по-малка
   резолюция напрактика не мога да ги достигна». Both halves of that are true
   and they want different answers. A hidden-scrollbar strip is reachable by
   thumb and by nothing else — fine on a phone, broken with a mouse. So above
   the tablet breakpoint (breakpoint.tablet = 641px, literal because custom
   properties cannot be used in a media query) the strip wraps and every tab is
   always reachable; below it, the base scroll stands, because four rows of
   tabs would eat a third of a 390px screen before the form began.

   The row separator moves onto the tabs themselves: a wrapped strip has no
   single bottom edge for a border to sit on. */
@media (min-width: 641px) {
  .tabs--switch {
    flex-wrap: wrap;
    overflow-x: visible;
    border-bottom: none;
    -webkit-mask-image: none;
    mask-image: none;
    background: none;
  }
  .tabs--switch .tab {
    border-bottom-width: 2px;
    border-bottom-color: var(--color-border-default);
  }
  .tabs--switch .tab--active { border-bottom-color: var(--color-accent-primary); }
}

/* Panels are toggled by the `hidden` attribute, which the UA already honours —
   nothing here may set `display` on .tab-panel or it would fight that.
   tabindex="-1" makes the panel programmatically focusable; :focus-visible is
   therefore suppressed, because a panel that shows a focus ring when script
   moves focus into it rings around the whole page's worth of content. The
   focus stays visible on whatever inside the panel receives it next. */
.tab-panel { outline: none; }
}

@layer components {
/* === components/pdp/pdp.css === */
/* ============================================================
   @layer context (Phase 2 — layer-taxonomy.md §!important audit)
   This file is assigned to @layer components by css-build.

   The !important declarations below target CloudImage/lightbox elements
   that inject inline style="" attributes via JavaScript at runtime.
   Inline styles have specificity 1000 and are NOT part of any stylesheet
   layer — they are unlayered in the cascade sense. @layer !important
   rules from the components layer lose to inline styles: an unlayered
   non-!important rule beats a layered !important rule when the unlayered
   rule is an inline style. This means the !important declarations here
   continue to lose to CloudImage inline overrides exactly as they did
   before Phase 2. Behavior is unchanged.

   If CloudImage ever moves its overrides to a <style> tag (unlayered
   stylesheet), those rules would beat components-layer !important —
   the same interaction as today, since an unlayered stylesheet also
   currently beats our source-order position. No regression from layering.
   ============================================================ */

/* ============================================================
   PDP — 360° BOTTLE VIEW (CloudImage CI360 plugin host)
   ============================================================ */

.pdp-bottle-360 {
  position: relative;
  background: var(--color-surface-sunk);
  border-radius: var(--radius-card);
  border: 1px solid var(--color-border-default);
  overflow: hidden;
  aspect-ratio: 3/4;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pdp-bottle-360 .cloudimage-360 {
  width: 100% !important;
  height: 100% !important;
  padding-bottom: 0 !important;
  cursor: grab;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pdp-bottle-360 .cloudimage-360:active {
  cursor: grabbing;
}
.pdp-bottle-360 .cloudimage-360 img,
.pdp-bottle-360 .cloudimage-360 canvas {
  width: auto !important;
  height: 88% !important;
  max-width: 90% !important;
  object-fit: contain;
  position: relative !important;
  top: auto !important;
  left: auto !important;
}

.pdp-bottle-360__hint {
  position: absolute;
  bottom: var(--spacing-sm);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: var(--spacing-xs) var(--spacing-md);
  background: rgba(31, 23, 18, 0.7);
  color: var(--color-text-on-accent);
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  border-radius: var(--radius-full);
  pointer-events: none;
  white-space: nowrap;
}

/* ============================================================
   PDP — BOTTLE FLIP (front ↔ back, click to toggle)
   ============================================================ */

.pdp-bottle-flip {
  position: relative;
  width: 100%;
  aspect-ratio: 3/4;
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-card);
  overflow: hidden;
  cursor: grab;
  perspective: 1200px;
  touch-action: pan-y;
  user-select: none;
}
.pdp-bottle-flip:active { cursor: grabbing; }

.pdp-bottle-flip img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  backface-visibility: hidden;
  transition: transform 0.6s ease;
}

.pdp-bottle-flip .pdp-bottle-flip__back {
  transform: rotateY(180deg);
}

.pdp-bottle-flip.is-flipped .pdp-bottle-flip__front {
  transform: rotateY(180deg);
}

.pdp-bottle-flip.is-flipped .pdp-bottle-flip__back {
  transform: rotateY(0deg);
}

.pdp-bottle-flip__hint {
  position: absolute;
  bottom: var(--spacing-sm);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: var(--spacing-xs) var(--spacing-md);
  background: rgba(31, 23, 18, 0.7);
  color: var(--color-text-on-accent);
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  border-radius: var(--radius-full);
  pointer-events: none;
  white-space: nowrap;
  z-index: 1;
}

/* ============================================================
   PDP — BOTTLE 360 (drag-to-rotate, CloudImage CI360 host)
   The library injects inline padding-bottom for aspect ratio and
   absolute-positioned frames; the !important overrides force the
   viewer to fill our fixed 3/4 slot without library-driven sizing.
   ============================================================ */

.pdp-bottle-360 {
  position: relative;
  width: 100%;
  aspect-ratio: 3/4;
  background: var(--color-surface-sunk);
  border-radius: var(--radius-card);
  border: 1px solid var(--color-border-default);
  overflow: hidden;
}

.pdp-bottle-360 .cloudimage-360 {
  position: absolute !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  padding-bottom: 0 !important;
  cursor: grab;
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pdp-bottle-360 .cloudimage-360:active {
  cursor: grabbing;
}

.pdp-bottle-360 .cloudimage-360-inner-box {
  position: relative !important;
  width: 100% !important;
  height: 100% !important;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pdp-bottle-360 .cloudimage-360 img,
.pdp-bottle-360 .cloudimage-360 canvas {
  width: 100% !important;
  height: 100% !important;
  max-width: 100% !important;
  max-height: 100% !important;
  object-fit: contain !important;
}

.pdp-bottle-360 .cloudimage-360-hints-overlay,
.pdp-bottle-360 .cloudimage-initial-icon,
.pdp-bottle-360 .cloudimage-360-button {
  display: none !important;
}

.pdp-bottle-360__hint {
  position: absolute;
  bottom: var(--spacing-sm);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: var(--spacing-xs) var(--spacing-md);
  background: rgba(31, 23, 18, 0.7);
  color: var(--color-text-on-accent);
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  border-radius: var(--radius-full);
  pointer-events: none;
  white-space: nowrap;
  z-index: 2;
}

/* Square thumbnail variant + button trigger for lightbox */
.pdp-thumbs--square {
  grid-template-columns: repeat(3, 1fr);
}


.pdp-thumb--button {
  display: block;
  padding: 0;
  width: 100%;
  aspect-ratio: 1 / 1;
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.2s ease, transform 0.2s ease;
}

.pdp-thumb--button:hover,
.pdp-thumb--button:focus-visible {
  border-color: var(--color-accent-primary);
  transform: translateY(-1px);
}

.pdp-thumb--button img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Lightbox overlay — three-column layout: thumbs | stage | info.
   Background matches the site's primary surface (parchment in light theme,
   deep brown in dark theme) so the gallery feels integrated, not a black void. */
.pdp-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--color-surface-primary);
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: var(--spacing-xl);
}

.pdp-lightbox[hidden] { display: none; }

.pdp-lightbox__layout {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) 320px;
  gap: var(--spacing-xl);
  align-items: center;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
}

/* LEFT — vertical thumbnails */
.pdp-lightbox__thumbs {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  max-height: 100%;
  overflow-y: auto;
  padding: var(--spacing-xs);
}
.pdp-lightbox__thumb {
  appearance: none;
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  padding: var(--spacing-xs);
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease;
  width: 100%;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pdp-lightbox__thumb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
}
.pdp-lightbox__thumb:hover,
.pdp-lightbox__thumb:focus-visible {
  border-color: var(--color-border-strong);
  background: var(--color-surface-alt);
}
.pdp-lightbox__thumb.is-active {
  border-color: var(--color-accent-gold, var(--color-accent-primary));
  background: var(--color-surface-alt);
}

/* CENTER — main image stage with prev/next */
.pdp-lightbox__stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 0;
}
.pdp-lightbox__img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
  user-select: none;
}

/* RIGHT — product info panel */
.pdp-lightbox__info {
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-card);
  padding: var(--spacing-xl);
  color: var(--color-text-primary);
  align-self: center;
  max-height: 100%;
  overflow-y: auto;
}
.pdp-lightbox__info-eyebrow {
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0 0 var(--spacing-xs);
}
.pdp-lightbox__info-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--font-size-h3);
  font-weight: var(--font-weight-display-regular);
  margin: 0 0 var(--spacing-xs);
  color: var(--color-text-primary);
}
.pdp-lightbox__info-region {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
  margin: 0 0 var(--spacing-lg);
}
.pdp-lightbox__info-facts {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin: 0 0 var(--spacing-lg);
  padding: var(--spacing-md) 0;
  border-top: 1px solid var(--color-border-default);
  border-bottom: 1px solid var(--color-border-default);
}
.pdp-lightbox__info-facts > div {
  display: flex;
  justify-content: space-between;
  gap: var(--spacing-md);
  font-size: var(--font-size-small);
}
.pdp-lightbox__info-facts dt {
  color: var(--color-text-subtle);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  font-size: var(--font-size-micro);
}
.pdp-lightbox__info-facts dd {
  margin: 0;
  font-weight: var(--font-weight-ui-semibold);
  color: var(--color-text-primary);
}
.pdp-lightbox__info-price {
  font-size: var(--font-size-h3);
  font-weight: var(--font-weight-ui-semibold);
  color: var(--color-text-primary);
  margin-bottom: var(--spacing-md);
}

/* Close + nav buttons */
.pdp-lightbox__close,
.pdp-lightbox__nav {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-full);
  cursor: pointer;
  font-family: inherit;
  line-height: 1;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.pdp-lightbox__close:hover,
.pdp-lightbox__nav:hover,
.pdp-lightbox__close:focus-visible,
.pdp-lightbox__nav:focus-visible {
  background: var(--color-surface-sunk);
  border-color: var(--color-border-strong);
}
.pdp-lightbox__close {
  position: absolute;
  top: var(--spacing-lg);
  right: var(--spacing-lg);
  width: 44px;
  height: 44px;
  font-size: 20px;
  z-index: 2;
}
.pdp-lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  font-size: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 0 4px;
  z-index: 1;
}
.pdp-lightbox__nav--prev { left: var(--spacing-sm); }
.pdp-lightbox__nav--next { right: var(--spacing-sm); }

@media (max-width: 960px) {
  /* The whole overlay scrolls as one column; the info panel sits statically
     in normal flow (no separate inner scroll window). */
  .pdp-lightbox {
    align-items: flex-start;
    overflow-y: auto;
  }
  .pdp-lightbox__layout {
    grid-template-columns: 72px minmax(0, 1fr);
    grid-template-areas: "thumbs stage" "info info";
    gap: var(--spacing-md);
    align-items: start;
  }
  .pdp-lightbox__thumbs { grid-area: thumbs; max-height: none; }
  .pdp-lightbox__stage { grid-area: stage; height: auto; }
  .pdp-lightbox__img { max-height: 55vh; }
  /* Static description — no inner scrollbar, full content always visible. */
  .pdp-lightbox__info {
    grid-area: info;
    max-height: none;
    overflow: visible;
  }
  /* Keep the close button pinned to the viewport while the overlay scrolls. */
  .pdp-lightbox__close { position: fixed; }
}

@media (max-width: 600px) {
  .pdp-lightbox { padding: var(--spacing-md); }
  .pdp-lightbox__layout { gap: var(--spacing-sm); }
  .pdp-lightbox__close { top: var(--spacing-md); right: var(--spacing-md); }
  .pdp-lightbox__nav { width: 40px; height: 40px; font-size: 22px; }
  .pdp-lightbox__nav--prev { left: var(--spacing-xs); }
  .pdp-lightbox__nav--next { right: var(--spacing-xs); }
}

/* ============================================================
   PDP — HERO LAYOUT + GALLERY + SECONDARY SECTIONS
   Responsive: stacks single-column under 768px.
   ============================================================ */

.pdp-hero-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-editorial-md);
  align-items: start;
}

.pdp-thumbs {
  list-style: none;
  margin: var(--spacing-md) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-sm);
}

.pdp-thumb {
  aspect-ratio: 1 / 1;
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  overflow: hidden;
  cursor: pointer;
}

.pdp-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* --contents: the same frame, a different job. On the wine's page a thumb is
   another view of ONE bottle, so a square crop that fills the tile is right. On
   the pack's page the row is the pack's CONTENTS — three to six different
   bottles, each a link to its own wine — and there a crop hides which bottle it
   is. Whole bottle, portrait frame, and small enough that six of them read as a
   list rather than as a gallery: three across a 530px column at the base size
   render 170px tiles, which is bigger than the hero deserves to compete with. */
.pdp-thumbs--contents {
  grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));
}
.pdp-thumbs--contents .pdp-thumb {
  /* display, because here the thumb is an <a> and not the wine page's <button>.
     aspect-ratio does not apply to a non-replaced INLINE box, so without this the
     tile took its height from the image and rendered 75×194 instead of 75×100. */
  display: block;
  aspect-ratio: 3 / 4;
}
.pdp-thumbs--contents .pdp-thumb img {
  object-fit: contain;
}

/* Vertical thumbnail rail — placed to the LEFT of the main flip image.
   Declared after .pdp-thumbs so the flex override beats the base grid. */
.pdp-bottle-flip-layout {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: var(--spacing-md);
  align-items: start;
}

.pdp-bottle-flip-layout__main {
  min-width: 0;
}

.pdp-thumbs.pdp-thumbs--vertical {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin: 0;
  padding: 0;
  list-style: none;
  width: 100%;
  grid-template-columns: none;
}

.pdp-thumbs--vertical > li {
  width: 100%;
}

.pdp-thumb--mini {
  width: 120px;
  aspect-ratio: 1 / 1;
}

@media (max-width: 600px) {
  .pdp-bottle-flip-layout {
    grid-template-columns: 88px 1fr;
    gap: var(--spacing-sm);
  }
  .pdp-thumb--mini { width: 88px; }
}

.pdp-author {
  display: flex;
  gap: var(--spacing-xl);
  align-items: flex-start;
}

.pdp-author__portrait {
  width: var(--card-author-portrait-lg);
  height: var(--card-author-portrait-lg);
  border-radius: var(--radius-avatar);
  border: 3px solid var(--card-author-ring-premium);
  background: var(--color-surface-sunk);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
  flex-shrink: 0;
}

.pdp-winery-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-xl);
  align-items: start;
}

.pdp-winery-grid__media {
  background: var(--color-surface-primary);
  border-radius: var(--radius-card);
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
  border: 1px solid var(--color-border-default);
}

.pdp-secondary-cta {
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: var(--card-radius);
  padding: var(--spacing-xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-xl);
}

.pdp-secondary-cta__price {
  text-align: right;
  flex-shrink: 0;
}

/* PDP — paired sections (two editorial blocks side-by-side).
   Used for История | Какво е това вино, Тех. данни | Логистика, Автор | Изба. */
.pdp-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-2xl, var(--spacing-xl));
  align-items: start;
}
.pdp-pair > * { min-width: 0; }

/* .trust-strip--stacked (the vertical variant used in the half-width column)
   moved to trust-strip.css at the bundle split (260828): pdp is core and
   trust-strip is storefront, so this file now loads BEFORE the owner and the
   owner's base rule was winning back align-items and gap — 11 renders differed
   in split-verify. A component's modifier belongs in the component. */

/* Compact author block — fits within a half-width column. */
.pdp-author--compact { gap: var(--spacing-md); align-items: flex-start; }
.pdp-author--compact .pdp-author__portrait {
  width: calc(var(--card-author-portrait-lg) * 0.65);
  height: calc(var(--card-author-portrait-lg) * 0.65);
  font-size: 2rem;
}

/* Winery profile — vertical stack (media on top, text below).
   Used inside a half-width column where horizontal split would be too cramped. */
.pdp-winery-stack { display: flex; flex-direction: column; gap: var(--spacing-md); }
.pdp-winery-stack__media {
  background: var(--color-surface-primary);
  border-radius: var(--radius-card);
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
  border: 1px solid var(--color-border-default);
}

/* PDP context notes — the «Стил» block above the facts list.
   Production structure: demoto.enotale.eu crawl 2026-08-31. Lifted out of an
   inline style attribute so the undefined-class gate can see it; declarations
   are unchanged and token-only. */
.pdp-context-notes {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-lg);
}
.pdp-context-notes p {
  margin: 0;
}

/* PDP facts — tight vertical list with pictogram + label + value per row.
   Used inside the "За виното" column to surface technical data without a heading. */
.pdp-facts {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--color-border-default);
}
.pdp-facts__row {
  display: grid;
  grid-template-columns: 1.75rem 8rem 1fr;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-sm) 0;
  border-bottom: 1px solid var(--color-border-default);
  font-size: var(--font-size-small);
  /* Warm taupe cascades to icon + label (and via currentColor to SVG strokes).
     Value cell overrides this with the darker primary text color.
     D-07 (2026-08-04): was the raw hex #6b5d4e. That is walnut, i.e. the LIGHT
     value of --color-text-muted, frozen — so it could not follow the theme and
     rendered 2.86:1 on the dark page across 22 wine pages (268 instances). */
  color: var(--color-text-muted);
}
.pdp-facts__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  color: inherit;
}
.pdp-facts__icon svg {
  width: 1.25rem;
  height: 1.25rem;
  display: block;
}
.pdp-facts__label {
  font-size: var(--font-size-micro);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  color: inherit;
}
.pdp-facts__value {
  font-weight: var(--font-weight-ui-semibold);
  /* D-07: was the raw hex #1f1712 — ink, the LIGHT value of
     --color-text-primary. Frozen against a dark page that is #1A1410, it gave
     1.03:1: the fact values on every wine page were invisible in dark mode.
     335 instances over 22 pages, the largest single defect in the whole audit. */
  color: var(--color-text-primary);
}
.pdp-facts__value a { color: inherit; }

/* Food pairing — circle pictograms in a single row.
   Used inside "За виното" → "С какво да пиете" subsection. */
.food-pairing-row {
  display: flex;
  gap: var(--spacing-md);
  margin: var(--spacing-sm) 0 var(--spacing-md);
  flex-wrap: wrap;
}
.food-pairing-card {
  flex: 1 1 0;
  min-width: 90px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-sm);
  text-align: center;
}
/* --compact: the same cards at a step's scale. A tasting step is a row inside a
   column, not a full-width section, so the pictograms drop from 56px to 40 and the
   row stops stretching its cards to fill. Declared here, beside the row it modifies,
   rather than in pack-detail.css — the modifier belongs to the component. */
.food-pairing-row--compact {
  gap: var(--spacing-sm);
  margin: var(--spacing-sm) 0 0;
}
.food-pairing-row--compact .food-pairing-card {
  flex: 0 0 auto;
  min-width: 0;
  max-width: 5.5rem;
  gap: var(--spacing-xs);
}
.food-pairing-row--compact .food-pairing-card__icon {
  width: 40px;
  height: 40px;
}
.food-pairing-row--compact .food-pairing-card__label {
  font-size: var(--font-size-micro);
  line-height: 1.25;
}

.food-pairing-card__icon {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-border-default);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.75rem;
  line-height: 1;
}
.food-pairing-card__icon:has(.food-pairing-card__img) {
  padding: 0;
  overflow: hidden;
}
.food-pairing-card__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Source pictograms carry built-in whitespace padding; scale the illustration
     up inside the circular icon (which clips via overflow:hidden) so the subject
     reads larger. Applies to SYS_03 reference page, PDP and WIN_13. */
  transform: scale(1.3);
  transform-origin: center;
}
.food-pairing-card__label {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
  margin: 0;
  line-height: var(--line-height-tight, 1.3);
}

.hero__image-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 4/3;
  border-radius: var(--card-radius);
  overflow: hidden;
  border: 1px solid var(--color-border-default);
}
.hero__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.hero__author-overlay {
  position: absolute;
  left: var(--spacing-lg);
  right: var(--spacing-lg);
  bottom: var(--spacing-lg);
  padding: var(--spacing-md) var(--spacing-lg);
  background: linear-gradient(180deg, rgba(31,23,18,0) 0%, rgba(31,23,18,0.55) 60%, rgba(31,23,18,0.75) 100%);
  border-radius: var(--card-radius);
  text-align: center;
}
.author-badge--on-image {
  justify-content: center;
}
.author-badge--on-image .author-badge__name,
.author-badge--on-image .author-badge__role {
  color: var(--color-text-on-accent);
  text-shadow: 0 1px 2px rgba(0,0,0,0.4);
}


/* ============================================================
   PDP BUY BLOCK — trust list (S260830, §5-W7)
   Replaces three inline-style="" rows. Token-only.
   ============================================================ */
.pdp-buy__trust {
  list-style: none;
  margin: var(--spacing-lg) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}

/* .pdp-buy__trust-item RETIRED S260831 — it was a character-for-character copy of
   .trust-item (trust-strip/trust-strip.css:27): same five declarations, same tokens.
   The buy block now consumes the component. The check-duplication gate caught it.
   The wrapper .pdp-buy__trust stays local: it is a plain vertical list, while
   .trust-strip carries a border-top and page-level padding this context must not have. */

/* ============================================================
   PDP IDENTITY — the product's name and its attribution line.

   PROMOTED FROM INLINE STYLE, 2026-09-01. These five declarations stood as a
   `style=""` attribute on BUY_04_PDP and, character for character, on
   WIN_13_PRODUCT_PREVIEW — two hand-copies of one treatment, invisible to
   check-duplication because a gate that reads CSS files cannot see a rule that
   lives in an attribute. The pack's page needed the same treatment and would have
   made a third; that is what forced the promotion.

   The display serif at pdp-wine size is the identity anchor of every product page
   in the storefront, and it is what makes a pack read as a product of the same
   kind rather than as a page from another site. Italic, because the whole
   editorial voice of the storefront is italic display; the UI face carries the
   money, immediately below.
   ============================================================ */
.pdp-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--font-size-pdp-wine);
  font-weight: var(--font-weight-display-regular);
  line-height: var(--line-height-display);
  margin-bottom: var(--spacing-sm);
}

/* Изба · регион. One size down, light weight, muted — attribution, not identity.
   The link inherits the line's colour rather than taking link blue: the whole line
   is one editorial statement and a coloured half would break it into two. */
.pdp-attrib {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--font-size-pdp-winery);
  font-weight: var(--font-weight-display-light);
  color: var(--color-text-muted);
  margin-bottom: var(--spacing-lg);
}
.pdp-attrib a {
  color: inherit;
  text-decoration: underline;
}
.pdp-attrib__sep {
  color: var(--color-border-strong);
}

/* ============================================================
   PDP VINTAGE SELECTOR ROW — S260831, F1(b)
   Label inline-start, buttons after, align-items center.
   Replaces inline style="" soup on the vintage div/buttons.
   ============================================================ */
.pdp-vintage {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
  margin-bottom: var(--spacing-lg);
}

.pdp-vintage__label {
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-semibold);
  color: var(--color-text-muted);
  letter-spacing: var(--letter-spacing-allcaps);
  text-transform: uppercase;
  white-space: nowrap;
}

.pdp-vintage__opt {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-primary);
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.pdp-vintage__opt--active {
  border-color: var(--color-accent-primary);
  background: var(--color-accent-primary);
  color: var(--color-text-on-accent);
}

.pdp-vintage__opt:not(.pdp-vintage__opt--active):hover {
  border-color: var(--color-border-strong);
}

/* ============================================================
   PDP HERO QUOTE — the winemaker's voice in the buy column.

   DD 2026-08-31: «да върнем думичките на винаря, но да са малко по-компактни
   (наруши се баланса в горната част)» and then «да мине под реколта».

   Position is load-bearing: it sits BELOW the vintage selector, closing the identity
   block (name → winery · region → vintage → voice) before the buy block takes over.
   Placed above the selector it split one identity group in two.

   Deliberately NOT styled as a pull quote — no gold rule, no indent. The formal
   blockquote treatment belongs to «Профил на автора» further down the page; two pull
   quotes on one page compete instead of building. This is a caption in the display face.
   ============================================================ */
.pdp-hero-quote {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--font-size-body);
  line-height: var(--line-height-display);
  color: var(--color-text-muted);
  margin-top: var(--spacing-md);
  margin-bottom: var(--spacing-md);
}

/* The shipping fee sits beside the delivery badge, in the muted trust tone rather than
   the badge's tier colour — the tier colour means «how fast», and a price is not fast
   or slow. Same size as the surrounding trust rows so the pair reads as one statement. */
.pdp-buy__fee {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
}
}

@layer components {
/* === components/progress-bar/progress-bar.css === */
/* ============================================================
   PROGRESS BAR (order status)
   ============================================================ */

.progress-bar-wrap {
  margin: var(--spacing-sm) 0;
}

.progress-bar {
  height: 4px;
  background: var(--color-border-default);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.progress-bar__fill {
  height: 100%;
  background: var(--color-accent-primary);
  border-radius: var(--radius-full);
  transition: width var(--duration-moderate) var(--easing-standard);
}

.progress-steps {
  display: flex;
  justify-content: space-between;
  font-size: var(--font-size-micro);
  /* D5-sweep: was --color-text-subtle (12px micro, fails AA); muted passes 4.5:1. */
  color: var(--color-text-muted);
  margin-top: var(--spacing-xs);
}


/* ------------------------------------------------------------
   SHIPMENT ROWS — one bar per пратка, not one per order
   ------------------------------------------------------------
   Added 2026-08-25 for BUY_09_ORDER_LIST. The listing drew a single
   five-step bar from `order.status`, on orders that split across up
   to three wineries: order 100051 showed one 20 %-filled bar for
   пратки that were Изпратена, Готова пратка and Обработва се. The
   bar could not be right for any of them, and the five step labels
   under it belonged to an order-level story the data does not tell.

   A row per winery instead: name, a slim bar, and the state with
   its own date. The bar is decoration over text that already says
   the state, so it carries role="img" and an aria-label rather than
   asking a screen reader to interpret a width.
   ------------------------------------------------------------ */
.order-shipments {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-md);
}

.order-shipment {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(4rem, 1fr) minmax(0, auto);
  align-items: center;
  gap: var(--spacing-md);
  font-size: var(--font-size-small);
}

.order-shipment__winery {
  font-family: var(--font-ui);
  font-weight: var(--font-weight-ui-medium);
  color: var(--color-text-primary);
  min-width: 0;
}

.order-shipment__bar {
  margin: 0;
}

.order-shipment__state {
  color: var(--color-text-muted);
  text-align: right;
  white-space: nowrap;
}

.order-shipment__state .tcell-flag {
  display: inline-block;
  margin-top: 0;
}

/* At narrow widths three columns cannot hold a winery name, a bar and a
   date. The bar is the first thing to go — it is the only one of the three
   that says nothing the text does not. */
@media (max-width: 640px) {
  .order-shipment {
    grid-template-columns: 1fr auto;
  }
  .order-shipment__bar {
    display: none;
  }
  .order-shipment__state {
    white-space: normal;
  }
}


/* Wines belong to the winery sending them (DD 2026-08-25). The group is the
   пратка: its row, then its bottles indented under it. */
.order-shipment-group + .order-shipment-group {
  margin-top: var(--spacing-sm);
  padding-top: var(--spacing-sm);
  border-top: 1px solid var(--color-border-default);
}

.order-shipment__wines {
  list-style: none;
  margin: var(--spacing-xs) 0 0;
  padding: 0 0 0 var(--spacing-md);
  color: var(--color-text-muted);
  font-size: var(--font-size-small);
}

.order-shipment__wines li {
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: 2px 0;
}

.order-shipment__wines svg {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}

/* «възможно забавяне» — the buyer-facing wording for sla_risk. A caution, not
   an alarm: the date may move, which is a fact, not a fault the reader can act
   on. Amber ink on the page ground, no fill — a fill here would out-shout the
   status pill beside it, which is the thing that actually changed. */
.order-shipment__delay,
.sub-order-card__delay {
  color: var(--color-status-warning-text);
  font-style: italic;
}
}

@layer components {
/* === components/card/card.css === */
/* ============================================================
   CARD — shared base chrome
   Layer: components (assigned at concat time by css-build)

   CHROME ONLY. Padding, margin, and width are composed via
   utility classes in templates — they are intentionally NOT
   set here. Inline pattern observed:
     background + border + radius constant (12× alt, 8× sunk)
     padding and margin-bottom varied per use
   ============================================================ */

.card {
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: var(--card-radius);
}

/* ----------------------------------------------------------
   MODIFIER: sunk surface
   Use for inset/recessed content areas (form sections,
   profile info panels, account settings blocks).
   8× observed inline: surface-sunk + card-radius + no border.
   ---------------------------------------------------------- */

.card--sunk {
  background: var(--color-surface-sunk);
  /* Truly borderless (not transparent 1px) — matches the observed inline sunk
     pattern (surface-sunk + radius + padding, no border). Using border:none
     keeps the box model identical to the borderless inline it replaces. */
  border: none;
}
}

@layer components {
/* === components/section-heading/section-heading.css === */
/* ============================================================
   SECTION-HEADING — display typography for section/page headings
   Layer: components (assigned at concat time by css-build)

   Sets font-family only — the single property invariant across
   all inline combos. font-weight, line-height, and margin are
   NOT set; they inherit from the heading element or are
   composed via utilities at migration time.

   Distinct from .section-header (the flex title+link row in
   components/section-header/section-header.css).

   Observed inline combos:
     font-display + h3 + mb-xs   (7×)
     font-display + h3 + mb-xl   (7×)
     font-display + h4 + mb-md   (7×)
     font-display + h4 + mb-lg   (9×)
     font-display + h2 + mb-xl   (~14×)
     font-display + h1 + various (7×)
   No dominant default pairing — size+margin compose via utils.
   ============================================================ */

.section-heading {
  font-family: var(--font-display);
}
}

@layer components {
/* === components/eyebrow/eyebrow.css === */
/* ============================================================
   EYEBROW — kicker / category label
   Layer: components (assigned at concat time by css-build)

   Declares exactly the 4 properties set by all 7 inline uses.
   font-family, font-weight, and margin are NOT set — they
   inherit from context, matching the inline behaviour.

   Observed inline pattern:
     <p style="font-size: var(--font-size-micro); text-transform: uppercase;
               letter-spacing: var(--letter-spacing-allcaps);
               color: var(--color-text-muted);">…</p>
   ============================================================ */

.eyebrow {
  font-size: var(--font-size-micro);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-allcaps);
  color: var(--color-text-muted);
}
}

@layer components {
/* === base/layout-helpers.css === */
/* ============================================================
   PAGE LAYOUT HELPERS
   ============================================================ */

.page-header {
  padding: var(--spacing-xl) 0 var(--spacing-lg);
  border-bottom: 1px solid var(--color-border-default);
  margin-bottom: var(--spacing-xl);
}

.page-header__title {
  font-family: var(--font-display);
  font-size: var(--font-size-h2);
  font-weight: var(--font-weight-display-regular);
  margin: 0 0 var(--spacing-sm);
}

.page-header__meta {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
}

/* Centered-compact variant — BUY_02_WINE_CATALOG only.
   Does not affect BUY_06_CART, SYS_03_FOOD_PAIRING, WIN_20_SOCIAL_WALK
   which use their own page-header subclasses.
   Item C: padding-top reduced from var(--spacing-lg) (24px) to var(--spacing-sm) (8px)
   per DD. .breadcrumb above contributes 16px bottom margin; total gap 8+16=24px.

   2026-08-10: bottom padding and margin cut (16→8, 24→16). See the band-height
   override further down for why this header had grown loose at both ends. */
.page-header--centered-compact {
  padding: var(--spacing-sm) 0 var(--spacing-sm);
  margin-bottom: var(--spacing-md);
}
.page-header--centered-compact .page-header__title {
  font-size: var(--font-size-h3);
  text-align: center;
}
.page-header--centered-compact .page-header__meta {
  text-align: center;
}

/* ============================================================
   HEADER MEDIA — .header-media
   Reusable two-column layout modifier: text left / illustration band right.
   Applied ALONGSIDE the page's own header class on the same element.

   Class contract (consumed by template agents — do not rename):
     .header-media        — modifier on the header element
     .header-media__text  — wraps the existing heading markup
     .header-media__media — wraps EITHER an inline <svg aria-hidden="true">
                            OR a <picture> holding a decorative photograph

   Consumed by:
     BUY_02 /vino/    — .page-header.page-header--centered-compact.header-media
     PUB_07 /novini/  — .news-archive__header.header-media
     PUB_08 /blog/    — .blog-archive__header.header-media

   MEDIA KIND — DD-ratified 2026-08-10: all three consumers now carry a
   photograph, not line art. The SVG path below is retained because the class
   contract still supports it (a future band may be drawn), but no page uses it.
   Photographic bands are decorative: alt="" + aria-hidden, never a caption.

   HEIGHT CONTRACT — the band must not turn a small header into a hero.
   Measured from token values at the 900px breakpoint entry:

     Archive headers (.news-archive__header / .blog-archive__header) text content,
     since the DD removed both standfirsts on 2026-08-10:
       eyebrow 20px + its 16px margin
       + h1 at desktop capped 4.5rem = 72px; ×1.2 = 86.4px, margins 16+16
       ≈ 116px
     → band height var(--spacing-section-hero) = 128px. The band is now the
       TALLER of the two and therefore sets the row, which is what makes it
       read as vertically centred against the heading — the 6px of slack lands
       on the text column instead. Before the standfirsts went the text column
       was ~185px and the band floated inside it.

     .page-header--centered-compact (/vino/) text content:
       h3 at 900px  = 12px + 0.015×900 = 25.5px; ×1.2 line-height = 30.6px
       + margin-bottom spacing-sm (8px) = 38.6px ≈ 39px
     → the 128px band DOES set this header's height, growing it by ~89px.
       Accepted deliberately when the bands became photographic: at the 64px
       the line art used, a 3:1 photograph crops to a ~11:1 letterbox slice in
       which nothing is legible. A photograph needs vertical room to read as a
       photograph. All three bands are therefore one height — the previous
       64/128 split was a property of the drawings, not of the pages.

   SCALING CONTRACT — the wrapper OWNS the band height; the media fills it
   (width:100%; height:100%). Cropping belongs to the media, never to a CSS clip
   on the wrapper:
     - <svg>     → set preserveAspectRatio explicitly. `xMidYMid slice` for a
                   frieze that runs past the frame; `xMidYMid meet` for a
                   self-contained scene.
     - <picture> → object-fit:cover on the <img>. The wrapper's overflow:hidden
                   here exists only to clip the border-radius, not to size.
   Do NOT size the band with max-height on the media itself. A viewBox at
   `width:100%; height:auto` renders as tall as the column is wide divided by
   its ratio — in a ~600px column a 5:1 drawing comes out 120px tall and a
   40px clip decapitates it.

   No padding or border-bottom added here — existing header classes supply them.
   ============================================================ */

/* Band hidden on mobile; no grid gap leaks below 900px. */
.header-media__media {
  display: none;
}

/* BREADCRUMB INSIDE THE HEADER — DD 2026-08-10.

   The breadcrumb used to sit above .header-media as a full-width sibling. That
   left the band unable to start any higher than the breadcrumb's bottom edge,
   so the header carried a strip of empty parchment across its whole top-right
   corner — the band's column simply had nothing in it at that height.

   Moving the breadcrumb into .header-media__text makes it the first line of the
   left column instead, and the band grows into the space it vacated. The
   breadcrumb still renders first and still reads first; only its containing
   block changed.

   Its own 16px top padding has to go, at every width: inside the header that
   padding stacks on the header's own padding-top and reopens a smaller version
   of the gap this move exists to close. The bottom half is kept — it is the
   breadcrumb's separation from the heading below it, which is still needed. */
.header-media__text > .breadcrumb {
  padding-top: 0;
}

/* ≥900px: two-column grid with band. */
@media (min-width: 900px) {
  .header-media {
    display: grid;
    grid-template-columns: 2fr 3fr;   /* band column slightly wider than text */
    gap: var(--spacing-xl);           /* 32px column gap */
    align-items: center;
  }

  /* Text left, naturally fills the 2fr column. */
  .header-media__text {
    min-width: 0;  /* allow shrink below intrinsic width */
  }

  /* .header-media__side existed here for one afternoon on 2026-08-10: it stacked
     the archive standfirst under the band, and went when the DD removed the
     standfirst from both archives entirely. Not kept — with no consumer it is
     dead weight, and with it went the :has() start-alignment override, so every
     header is centred again. Recorded because the shape may be wanted back:
     wrap band + caption in one element so the pair stays a single grid item;
     a bare sibling would become a third column. */

  /* Override centring from .page-header--centered-compact.
     Specificity: (0,2,0) — ties with centered-compact rule; wins by source
     order since this block appears later in the same file. */
  .header-media .page-header__title,
  .header-media .page-header__meta {
    text-align: left;
  }

  /* Media band: the wrapper owns the height; the media fills it exactly. */
  .header-media__media {
    display: block;
    height: var(--spacing-section-hero);   /* 128px — see PHOTOGRAPHIC BANDS */
    min-width: 0;
    overflow: hidden;
    border-radius: var(--card-radius);
  }

  /* Compact header (/vino/) takes a shorter band — DD 2026-08-10.

     The grid row is as tall as the band, and align-items:center splits the
     difference between the row and the text column above and below the text.
     At 128px against an 82px text column that was 23px of dead parchment on
     each side, which the DD read — correctly — as a gap under the breadcrumb
     and a second gap under the meta line. Neither is a padding value; both are
     the band's height showing through.

     96px leaves 7px of slack per side. The band goes from ~5.6:1 to ~7.5:1,
     which the cellar photograph survives because its subject is a horizontal
     row of bottles; a photo with a tall subject would not, and would need the
     column narrowed instead of the band shortened.

     The archive headers keep 128px: their text column is ~150px tall, so the
     band is the shorter of the two there and sets nothing. */
  .page-header--centered-compact.header-media .header-media__media {
    height: var(--spacing-editorial-lg);   /* 96px */
  }

  /* SVG fills the wrapper box in both axes. Fit and cropping are decided by
     the band's own preserveAspectRatio, never by a CSS clip. */
  .header-media__media svg {
    display: block;
    width: 100%;
    height: 100%;
  }

  /* PHOTOGRAPHIC BANDS (DD-ratified 2026-08-10) — the three consumers now carry
     a photograph instead of line art. <picture> is display:inline by default and
     would add a baseline gap under the image; it must be a block that fills the
     wrapper so the <img> has a percentage height to resolve against.
     object-fit:cover is the raster equivalent of the SVG's `slice` — the band
     crops, it never letterboxes. object-position keeps the horizon/subject line
     centred when a 3:1 source is cropped to the band's own ratio. */
  .header-media__media picture {
    display: block;
    width: 100%;
    height: 100%;
  }

  .header-media__media img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--band-focus, center);
  }

  /* Crop focus. The band is 5.6:1 (7.5:1 on the compact header) against 3:1
     sources, so roughly half the frame height is discarded — WHICH half is a
     composition decision per photo, not a default. `center` keeps a landscape;
     a still life whose subject sits low needs the window moved down or the
     subject is decapitated. Percentages are geometry, not design values, so
     they are literals by intent — there is no token for "where the horizon
     sits in this photograph".

     Only --focus-lower is in use, on /blog/ (the notebook sits along the
     bottom edge of its frame). --focus-low was applied to /vino/ and withdrawn
     by the DD on 2026-08-10: at the 96px band the offset crop cut the tops of
     the bottles, and centred reads better than the reason the offset existed.
     Kept as a declared option rather than deleted — it is the other half of a
     two-value scale and the next photograph will want one of them. */
  .header-media__media--focus-low  { --band-focus: center 62%; }
  .header-media__media--focus-lower { --band-focus: center 78%; }
}

/* .measure-editorial was introduced here on 2026-08-10 to cap the BUY_05
   philosophy prose at 720px, and removed the same day when the DD ruled that
   paragraph should run the full container width. It is not kept "in case":
   a helper with no consumer is dead weight, and the pattern is one line to
   re-add if a future section wants it. */

.sidebar-layout {
  display: flex;
  gap: var(--spacing-xl);
  align-items: flex-start;
}
/* Let the main column shrink below its content's intrinsic width
   (flex children default to min-width:auto, which forces overflow). */
.sidebar-layout > * { min-width: 0; }

.main-content { flex: 1; min-width: 0; }
}

@layer components {
/* === components/empty-state/empty-state.css === */
/* ============================================================
   EMPTY STATES
   ============================================================ */

.empty-state {
  text-align: center;
  padding: var(--spacing-editorial-md) var(--spacing-lg);
  color: var(--color-text-muted);
}

.empty-state__icon { font-size: 3rem; margin-bottom: var(--spacing-lg); }

.empty-state__title {
  font-family: var(--font-display);
  font-size: var(--font-size-h3);
  font-weight: var(--font-weight-display-regular);
  color: var(--color-text-primary);
  margin-bottom: var(--spacing-md);
}

.empty-state__copy { font-size: var(--font-size-body); margin-bottom: var(--spacing-xl); }
}

@layer components {
/* === components/section-header/section-header.css === */
/* ============================================================
   SECTION HEADERS
   ============================================================ */

.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--spacing-sm) var(--spacing-md);
  flex-wrap: wrap;
  margin-bottom: var(--spacing-xl);
}

.section-header__title {
  font-family: var(--font-display);
  font-size: var(--font-size-h2);
  font-weight: var(--font-weight-display-regular);
  margin: 0;
}

.section-header__link {
  font-size: var(--font-size-small);
  color: var(--color-text-link);
}

/* ── Actions cluster (carousel controls + link, right side) ── */

/* Groups carousel arrow controls + "Виж всички →" link as one right-hand
   cluster in the section-header row. Introduced for the carousel home section.

   Backward compatibility: pages without __actions are unaffected —
   .section-header__link as a direct child of .section-header still
   pushes right via justify-content:space-between. __actions is purely additive.

   Gap choice: --spacing-sm (8px). This is half the outer section-header
   column-gap (16px), so the arrows and the link read as one cluster — the
   visual separation between title and cluster is wider than the internal
   controls-to-link gap, which is the intended spatial hierarchy.

   align-self:center overrides the parent's align-items:baseline for this
   element. The mix of large button boxes (44px) and small link text would
   produce an awkward computed baseline if baseline-aligned; centering the
   cluster as a whole against the heading row is the correct reading.

   Overflow check at narrowest visible-controls width (641px, where carousel
   arrows become visible): H2 ≈ 160px + actions ≈ 44+8+44+8+80 = 184px +
   space-between gap = ~344px total. No wrap. safe to 641px. */
.section-header__actions {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  align-self: center;
}
}

@layer components {
/* === components/sticky-cta/sticky-cta.css === */
/* ============================================================
   STICKY CTA BAR (mobile)
   ============================================================ */

.sticky-cta-bar {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: var(--color-surface-raised);
  border-top: 1px solid var(--color-border-default);
  padding: var(--spacing-md) var(--spacing-lg);
  z-index: var(--z-modal);
  /* C5 fix: consume --sticky-bar-shadow component-layer token (global→semantic→component chain). */
  box-shadow: var(--sticky-bar-shadow);
}
}

@layer utilities {
/* === base/utilities.css === */
/* GENERATED — do not edit; source: build/utility-build.js */
/* Spec: design-system/utilities.md */
/* Layer: utilities — wrapped by css-build at concat time */
/* Class counts: 169 spacing + 8 font-size + 6 font-weight + 6 text-color + 4 supplemental = 193 total */

/* ============================================================
   SPACING UTILITIES
   169 classes: 13 directions × 13 steps
   Source: --spacing-{step} semantic aliases
   ============================================================ */

/* mb-* */
.mb-xs {
  margin-bottom: var(--spacing-xs);
}
.mb-sm {
  margin-bottom: var(--spacing-sm);
}
.mb-md {
  margin-bottom: var(--spacing-md);
}
.mb-lg {
  margin-bottom: var(--spacing-lg);
}
.mb-xl {
  margin-bottom: var(--spacing-xl);
}
.mb-2xl {
  margin-bottom: var(--spacing-2xl);
}
.mb-2xl-plus {
  margin-bottom: var(--spacing-2xl-plus);
}
.mb-3xl {
  margin-bottom: var(--spacing-3xl);
}
.mb-editorial-sm {
  margin-bottom: var(--spacing-editorial-sm);
}
.mb-editorial-md {
  margin-bottom: var(--spacing-editorial-md);
}
.mb-editorial-lg {
  margin-bottom: var(--spacing-editorial-lg);
}
.mb-editorial-xl {
  margin-bottom: var(--spacing-editorial-xl);
}
.mb-section-hero {
  margin-bottom: var(--spacing-section-hero);
}

/* mt-* */
.mt-xs {
  margin-top: var(--spacing-xs);
}
.mt-sm {
  margin-top: var(--spacing-sm);
}
.mt-md {
  margin-top: var(--spacing-md);
}
.mt-lg {
  margin-top: var(--spacing-lg);
}
.mt-xl {
  margin-top: var(--spacing-xl);
}
.mt-2xl {
  margin-top: var(--spacing-2xl);
}
.mt-2xl-plus {
  margin-top: var(--spacing-2xl-plus);
}
.mt-3xl {
  margin-top: var(--spacing-3xl);
}
.mt-editorial-sm {
  margin-top: var(--spacing-editorial-sm);
}
.mt-editorial-md {
  margin-top: var(--spacing-editorial-md);
}
.mt-editorial-lg {
  margin-top: var(--spacing-editorial-lg);
}
.mt-editorial-xl {
  margin-top: var(--spacing-editorial-xl);
}
.mt-section-hero {
  margin-top: var(--spacing-section-hero);
}

/* ml-* */
.ml-xs {
  margin-left: var(--spacing-xs);
}
.ml-sm {
  margin-left: var(--spacing-sm);
}
.ml-md {
  margin-left: var(--spacing-md);
}
.ml-lg {
  margin-left: var(--spacing-lg);
}
.ml-xl {
  margin-left: var(--spacing-xl);
}
.ml-2xl {
  margin-left: var(--spacing-2xl);
}
.ml-2xl-plus {
  margin-left: var(--spacing-2xl-plus);
}
.ml-3xl {
  margin-left: var(--spacing-3xl);
}
.ml-editorial-sm {
  margin-left: var(--spacing-editorial-sm);
}
.ml-editorial-md {
  margin-left: var(--spacing-editorial-md);
}
.ml-editorial-lg {
  margin-left: var(--spacing-editorial-lg);
}
.ml-editorial-xl {
  margin-left: var(--spacing-editorial-xl);
}
.ml-section-hero {
  margin-left: var(--spacing-section-hero);
}

/* mr-* */
.mr-xs {
  margin-right: var(--spacing-xs);
}
.mr-sm {
  margin-right: var(--spacing-sm);
}
.mr-md {
  margin-right: var(--spacing-md);
}
.mr-lg {
  margin-right: var(--spacing-lg);
}
.mr-xl {
  margin-right: var(--spacing-xl);
}
.mr-2xl {
  margin-right: var(--spacing-2xl);
}
.mr-2xl-plus {
  margin-right: var(--spacing-2xl-plus);
}
.mr-3xl {
  margin-right: var(--spacing-3xl);
}
.mr-editorial-sm {
  margin-right: var(--spacing-editorial-sm);
}
.mr-editorial-md {
  margin-right: var(--spacing-editorial-md);
}
.mr-editorial-lg {
  margin-right: var(--spacing-editorial-lg);
}
.mr-editorial-xl {
  margin-right: var(--spacing-editorial-xl);
}
.mr-section-hero {
  margin-right: var(--spacing-section-hero);
}

/* mx-* */
.mx-xs {
  margin-left: var(--spacing-xs);
  margin-right: var(--spacing-xs);
}
.mx-sm {
  margin-left: var(--spacing-sm);
  margin-right: var(--spacing-sm);
}
.mx-md {
  margin-left: var(--spacing-md);
  margin-right: var(--spacing-md);
}
.mx-lg {
  margin-left: var(--spacing-lg);
  margin-right: var(--spacing-lg);
}
.mx-xl {
  margin-left: var(--spacing-xl);
  margin-right: var(--spacing-xl);
}
.mx-2xl {
  margin-left: var(--spacing-2xl);
  margin-right: var(--spacing-2xl);
}
.mx-2xl-plus {
  margin-left: var(--spacing-2xl-plus);
  margin-right: var(--spacing-2xl-plus);
}
.mx-3xl {
  margin-left: var(--spacing-3xl);
  margin-right: var(--spacing-3xl);
}
.mx-editorial-sm {
  margin-left: var(--spacing-editorial-sm);
  margin-right: var(--spacing-editorial-sm);
}
.mx-editorial-md {
  margin-left: var(--spacing-editorial-md);
  margin-right: var(--spacing-editorial-md);
}
.mx-editorial-lg {
  margin-left: var(--spacing-editorial-lg);
  margin-right: var(--spacing-editorial-lg);
}
.mx-editorial-xl {
  margin-left: var(--spacing-editorial-xl);
  margin-right: var(--spacing-editorial-xl);
}
.mx-section-hero {
  margin-left: var(--spacing-section-hero);
  margin-right: var(--spacing-section-hero);
}

/* my-* */
.my-xs {
  margin-top: var(--spacing-xs);
  margin-bottom: var(--spacing-xs);
}
.my-sm {
  margin-top: var(--spacing-sm);
  margin-bottom: var(--spacing-sm);
}
.my-md {
  margin-top: var(--spacing-md);
  margin-bottom: var(--spacing-md);
}
.my-lg {
  margin-top: var(--spacing-lg);
  margin-bottom: var(--spacing-lg);
}
.my-xl {
  margin-top: var(--spacing-xl);
  margin-bottom: var(--spacing-xl);
}
.my-2xl {
  margin-top: var(--spacing-2xl);
  margin-bottom: var(--spacing-2xl);
}
.my-2xl-plus {
  margin-top: var(--spacing-2xl-plus);
  margin-bottom: var(--spacing-2xl-plus);
}
.my-3xl {
  margin-top: var(--spacing-3xl);
  margin-bottom: var(--spacing-3xl);
}
.my-editorial-sm {
  margin-top: var(--spacing-editorial-sm);
  margin-bottom: var(--spacing-editorial-sm);
}
.my-editorial-md {
  margin-top: var(--spacing-editorial-md);
  margin-bottom: var(--spacing-editorial-md);
}
.my-editorial-lg {
  margin-top: var(--spacing-editorial-lg);
  margin-bottom: var(--spacing-editorial-lg);
}
.my-editorial-xl {
  margin-top: var(--spacing-editorial-xl);
  margin-bottom: var(--spacing-editorial-xl);
}
.my-section-hero {
  margin-top: var(--spacing-section-hero);
  margin-bottom: var(--spacing-section-hero);
}

/* p-* */
.p-xs {
  padding: var(--spacing-xs);
}
.p-sm {
  padding: var(--spacing-sm);
}
.p-md {
  padding: var(--spacing-md);
}
.p-lg {
  padding: var(--spacing-lg);
}
.p-xl {
  padding: var(--spacing-xl);
}
.p-2xl {
  padding: var(--spacing-2xl);
}
.p-2xl-plus {
  padding: var(--spacing-2xl-plus);
}
.p-3xl {
  padding: var(--spacing-3xl);
}
.p-editorial-sm {
  padding: var(--spacing-editorial-sm);
}
.p-editorial-md {
  padding: var(--spacing-editorial-md);
}
.p-editorial-lg {
  padding: var(--spacing-editorial-lg);
}
.p-editorial-xl {
  padding: var(--spacing-editorial-xl);
}
.p-section-hero {
  padding: var(--spacing-section-hero);
}

/* pb-* */
.pb-xs {
  padding-bottom: var(--spacing-xs);
}
.pb-sm {
  padding-bottom: var(--spacing-sm);
}
.pb-md {
  padding-bottom: var(--spacing-md);
}
.pb-lg {
  padding-bottom: var(--spacing-lg);
}
.pb-xl {
  padding-bottom: var(--spacing-xl);
}
.pb-2xl {
  padding-bottom: var(--spacing-2xl);
}
.pb-2xl-plus {
  padding-bottom: var(--spacing-2xl-plus);
}
.pb-3xl {
  padding-bottom: var(--spacing-3xl);
}
.pb-editorial-sm {
  padding-bottom: var(--spacing-editorial-sm);
}
.pb-editorial-md {
  padding-bottom: var(--spacing-editorial-md);
}
.pb-editorial-lg {
  padding-bottom: var(--spacing-editorial-lg);
}
.pb-editorial-xl {
  padding-bottom: var(--spacing-editorial-xl);
}
.pb-section-hero {
  padding-bottom: var(--spacing-section-hero);
}

/* pt-* */
.pt-xs {
  padding-top: var(--spacing-xs);
}
.pt-sm {
  padding-top: var(--spacing-sm);
}
.pt-md {
  padding-top: var(--spacing-md);
}
.pt-lg {
  padding-top: var(--spacing-lg);
}
.pt-xl {
  padding-top: var(--spacing-xl);
}
.pt-2xl {
  padding-top: var(--spacing-2xl);
}
.pt-2xl-plus {
  padding-top: var(--spacing-2xl-plus);
}
.pt-3xl {
  padding-top: var(--spacing-3xl);
}
.pt-editorial-sm {
  padding-top: var(--spacing-editorial-sm);
}
.pt-editorial-md {
  padding-top: var(--spacing-editorial-md);
}
.pt-editorial-lg {
  padding-top: var(--spacing-editorial-lg);
}
.pt-editorial-xl {
  padding-top: var(--spacing-editorial-xl);
}
.pt-section-hero {
  padding-top: var(--spacing-section-hero);
}

/* pl-* */
.pl-xs {
  padding-left: var(--spacing-xs);
}
.pl-sm {
  padding-left: var(--spacing-sm);
}
.pl-md {
  padding-left: var(--spacing-md);
}
.pl-lg {
  padding-left: var(--spacing-lg);
}
.pl-xl {
  padding-left: var(--spacing-xl);
}
.pl-2xl {
  padding-left: var(--spacing-2xl);
}
.pl-2xl-plus {
  padding-left: var(--spacing-2xl-plus);
}
.pl-3xl {
  padding-left: var(--spacing-3xl);
}
.pl-editorial-sm {
  padding-left: var(--spacing-editorial-sm);
}
.pl-editorial-md {
  padding-left: var(--spacing-editorial-md);
}
.pl-editorial-lg {
  padding-left: var(--spacing-editorial-lg);
}
.pl-editorial-xl {
  padding-left: var(--spacing-editorial-xl);
}
.pl-section-hero {
  padding-left: var(--spacing-section-hero);
}

/* pr-* */
.pr-xs {
  padding-right: var(--spacing-xs);
}
.pr-sm {
  padding-right: var(--spacing-sm);
}
.pr-md {
  padding-right: var(--spacing-md);
}
.pr-lg {
  padding-right: var(--spacing-lg);
}
.pr-xl {
  padding-right: var(--spacing-xl);
}
.pr-2xl {
  padding-right: var(--spacing-2xl);
}
.pr-2xl-plus {
  padding-right: var(--spacing-2xl-plus);
}
.pr-3xl {
  padding-right: var(--spacing-3xl);
}
.pr-editorial-sm {
  padding-right: var(--spacing-editorial-sm);
}
.pr-editorial-md {
  padding-right: var(--spacing-editorial-md);
}
.pr-editorial-lg {
  padding-right: var(--spacing-editorial-lg);
}
.pr-editorial-xl {
  padding-right: var(--spacing-editorial-xl);
}
.pr-section-hero {
  padding-right: var(--spacing-section-hero);
}

/* px-* */
.px-xs {
  padding-left: var(--spacing-xs);
  padding-right: var(--spacing-xs);
}
.px-sm {
  padding-left: var(--spacing-sm);
  padding-right: var(--spacing-sm);
}
.px-md {
  padding-left: var(--spacing-md);
  padding-right: var(--spacing-md);
}
.px-lg {
  padding-left: var(--spacing-lg);
  padding-right: var(--spacing-lg);
}
.px-xl {
  padding-left: var(--spacing-xl);
  padding-right: var(--spacing-xl);
}
.px-2xl {
  padding-left: var(--spacing-2xl);
  padding-right: var(--spacing-2xl);
}
.px-2xl-plus {
  padding-left: var(--spacing-2xl-plus);
  padding-right: var(--spacing-2xl-plus);
}
.px-3xl {
  padding-left: var(--spacing-3xl);
  padding-right: var(--spacing-3xl);
}
.px-editorial-sm {
  padding-left: var(--spacing-editorial-sm);
  padding-right: var(--spacing-editorial-sm);
}
.px-editorial-md {
  padding-left: var(--spacing-editorial-md);
  padding-right: var(--spacing-editorial-md);
}
.px-editorial-lg {
  padding-left: var(--spacing-editorial-lg);
  padding-right: var(--spacing-editorial-lg);
}
.px-editorial-xl {
  padding-left: var(--spacing-editorial-xl);
  padding-right: var(--spacing-editorial-xl);
}
.px-section-hero {
  padding-left: var(--spacing-section-hero);
  padding-right: var(--spacing-section-hero);
}

/* py-* */
.py-xs {
  padding-top: var(--spacing-xs);
  padding-bottom: var(--spacing-xs);
}
.py-sm {
  padding-top: var(--spacing-sm);
  padding-bottom: var(--spacing-sm);
}
.py-md {
  padding-top: var(--spacing-md);
  padding-bottom: var(--spacing-md);
}
.py-lg {
  padding-top: var(--spacing-lg);
  padding-bottom: var(--spacing-lg);
}
.py-xl {
  padding-top: var(--spacing-xl);
  padding-bottom: var(--spacing-xl);
}
.py-2xl {
  padding-top: var(--spacing-2xl);
  padding-bottom: var(--spacing-2xl);
}
.py-2xl-plus {
  padding-top: var(--spacing-2xl-plus);
  padding-bottom: var(--spacing-2xl-plus);
}
.py-3xl {
  padding-top: var(--spacing-3xl);
  padding-bottom: var(--spacing-3xl);
}
.py-editorial-sm {
  padding-top: var(--spacing-editorial-sm);
  padding-bottom: var(--spacing-editorial-sm);
}
.py-editorial-md {
  padding-top: var(--spacing-editorial-md);
  padding-bottom: var(--spacing-editorial-md);
}
.py-editorial-lg {
  padding-top: var(--spacing-editorial-lg);
  padding-bottom: var(--spacing-editorial-lg);
}
.py-editorial-xl {
  padding-top: var(--spacing-editorial-xl);
  padding-bottom: var(--spacing-editorial-xl);
}
.py-section-hero {
  padding-top: var(--spacing-section-hero);
  padding-bottom: var(--spacing-section-hero);
}

/* ============================================================
   FONT-SIZE UTILITIES
   8 classes: .text-{scale-name}
   Source: --font-size-{name}
   Note: .text-{size} and .text-{color} share prefix but differ in suffix pattern.
   ============================================================ */

.text-h1 {
  font-size: var(--font-size-h1);
}
.text-h2 {
  font-size: var(--font-size-h2);
}
.text-h3 {
  font-size: var(--font-size-h3);
}
.text-h4 {
  font-size: var(--font-size-h4);
}
.text-body {
  font-size: var(--font-size-body);
}
.text-small {
  font-size: var(--font-size-small);
}
.text-micro {
  font-size: var(--font-size-micro);
}
.text-compact {
  font-size: var(--font-size-compact);
}

/* ============================================================
   FONT-WEIGHT UTILITIES
   6 classes: .fw-{weight-name}
   Source: --font-weight-{name}
   Omitted (0 inline uses): .fw-display-bold, .fw-ui-light
   ============================================================ */

.fw-ui-medium {
  font-weight: var(--font-weight-ui-medium);
}
.fw-ui-semibold {
  font-weight: var(--font-weight-ui-semibold);
}
.fw-display-light {
  font-weight: var(--font-weight-display-light);
}
.fw-display-medium {
  font-weight: var(--font-weight-display-medium);
}
.fw-display-regular {
  font-weight: var(--font-weight-display-regular);
}
.fw-ui-regular {
  font-weight: var(--font-weight-ui-regular);
}

/* ============================================================
   TEXT-COLOR UTILITIES
   6 classes: .text-{color-name}
   Source: semantic color tokens (mixed: text-*, accent-*, status-*)
   Note: .text-error → --color-status-error per spec (no text-error alias exists)
   ============================================================ */

.text-muted {
  color: var(--color-text-muted);
}
.text-subtle {
  color: var(--color-text-subtle);
}
.text-primary {
  color: var(--color-text-primary);
}
.text-error {
  color: var(--color-status-error);
}
.text-accent {
  color: var(--color-text-accent);
}
.text-gold {
  color: var(--color-accent-gold);
}

/* ============================================================
   SUPPLEMENTAL UTILITIES — W2 phantom rider (spec §1.7)
   4 classes: .text-link (×92 uses), .text-h5 (×57 uses),
              .text-h6 (×4 uses), .mb-0 (×17 uses).
   Token source: component.json → utility.* (with fallback values).
   ============================================================ */

/* .text-link — link colour, underline, hover/focus/visited states */
.text-link {
  color: var(--color-text-link);
  text-decoration: underline;
  text-underline-offset: var(--utility-text-link-underline-offset);
  cursor: pointer;
}
.text-link:hover {
  color: var(--color-accent-primary);
  text-decoration-thickness: var(--radius-xs);
}
.text-link:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
  border-radius: var(--radius-xs);
}
.text-link:visited {
  color: var(--color-text-link-visited);
}

/* .text-h5 — h5 scale (×57 uses; headings render at body size without this class) */
.text-h5 {
  font-size: var(--utility-font-size-h5, 0.9375rem);
}

/* .text-h6 — h6 scale (×4 uses) */
.text-h6 {
  font-size: var(--utility-font-size-h6, 0.8125rem);
}

/* .mb-0 — margin-bottom reset to zero (×17 uses) */
.mb-0 {
  margin-bottom: 0;
}

/* Total: 169 spacing + 8 font-size + 6 font-weight + 6 text-color + 4 supplemental = 193 classes */
}

@layer utilities {
/* === components/row-between/row-between.css === */
/* ============================================================
   ROW-BETWEEN — flex row with space-between alignment
   Layer: utilities (assigned at concat time by css-build)

   Layout primitive for header rows: title on the left,
   action/meta on the right. Margin-bottom composes via
   utility classes.

   Placed in utilities layer (not components) because this
   expresses pure layout intent with no semantic component
   identity, analogous to .container. The utilities layer
   ensures it wins over component defaults without !important.

   Observed inline pattern (28× total, 13× with mb-xl):
     display:flex; align-items:center;
     justify-content:space-between; margin-bottom: var(--spacing-xl)
   ============================================================ */

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
}

@layer components {
/* === components/back-to-top/back-to-top.css === */
/* ============================================================
   BACK-TO-TOP — wine bottle, fixed bottom-right
   Hidden until user scrolls past 400px (toggled via .is-visible).
   ============================================================ */
.back-to-top {
  position: fixed;
  bottom: var(--spacing-lg);
  right: var(--spacing-lg);
  z-index: 90;
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-xs) var(--spacing-md) var(--spacing-xs) var(--spacing-sm);
  background: var(--color-accent-primary);
  color: var(--color-accent-primary-text);
  border: 1px solid var(--color-accent-primary);
  border-radius: var(--radius-full, 999px);
  font-family: var(--font-ui);
  font-size: var(--font-size-sm, 0.875rem);
  font-weight: var(--font-weight-ui-semibold, 600);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: var(--shadow-floating, var(--shadow-modal));
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity var(--duration-base) var(--easing-standard),
              transform var(--duration-base) var(--easing-standard),
              background var(--duration-fast) var(--easing-standard);
}
.back-to-top[hidden] { display: none; }
.back-to-top.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.back-to-top:hover,
.back-to-top:focus-visible {
  background: var(--color-accent-primary-hover);
  border-color: var(--color-accent-primary-hover);
}
.back-to-top:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring), var(--shadow-floating, var(--shadow-modal));
}
.back-to-top__bottle {
  width: 22px;
  height: 30px;
  display: block;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.back-to-top__label {
  white-space: nowrap;
}
@media (max-width: 480px) {
  .back-to-top {
    padding: var(--spacing-xs);
    gap: 0;
    bottom: var(--spacing-md);
    right: var(--spacing-md);
  }
  .back-to-top__label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .back-to-top__bottle {
    width: 24px;
    height: 32px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: none;
    transform: none;
  }
}
}

@layer components {
/* === components/visitka/visitka-palettes.css === */
/* ============================================================
   Visitka site palette token-packs — --vsite-* namespace
   Source of truth: core-data/drafts/visitka-palettes.md
   Also transcribed from tokens.css visitka blocks (AA-verified 2026-06-12).

   This file is palette-INDEPENDENT from the platform token system.
   Components inside [data-theme^="visitka-"] reference --vsite-*
   only; platform --color-* tokens are NOT overridden here.

   Loaded via css-manifest.txt (layer:components) → prototype.css.
   The visitka site article element carries data-theme="visitka-{name}";
   this scopes all --vsite-* resolutions within each winery site render.

   Architecture:
     --vsite-bg             page background (paper)
     --vsite-surface        cards / content panels
     --vsite-surface-sunk   inset sections / footer
     --vsite-text           headlines and body copy
     --vsite-text-muted     metadata, addresses, captions
     --vsite-accent         CTA fills, hero band bg, buttons, accent marks
     --vsite-on-accent      text on filled accent backgrounds
     --vsite-accent-text    accent used AS TEXT on light surfaces (AA-safe)
     --vsite-border         card edges, dividers, input strokes
     --vsite-link           consent/footer links (AA on palette surface)
     --vsite-input-bg       contact-form input background
     --vsite-input-text     contact-form input text
     --vsite-input-label    contact-form label text
     --vsite-input-border   contact-form input border
     --vsite-input-placeholder contact-form placeholder text
     --vsite-error          validation error (text + icon + border)
     --vsite-success        validation success icon
     --vsite-warning        validation warning icon

   Hero scrim is palette-INDEPENDENT — components keep using
   platform --color-scrim / --color-on-scrim (intentionally fixed
   dark/white for hero legibility across all 8 palettes).
   ============================================================ */

/* ── DEFAULT block ──────────────────────────────────────────────────
   Ensures --vsite-* tokens always resolve under any visitka wrapper,
   even before a palette-specific block is matched. Uses Мавруд values
   as the Enotale-platform-family default (palette 1).
   ─────────────────────────────────────────────────────────────────── */
[data-theme^="visitka-"] {
  /* Мавруд defaults — warm parchment + wine burgundy */
  --vsite-bg:                #F5EDD8;
  --vsite-surface:           #FAF4E3;
  --vsite-surface-sunk:      #EDE0C4;
  --vsite-text:              #1C1410;
  --vsite-text-muted:        #6A5B4B;
  --vsite-accent:            #6B1F2A;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #6B1F2A;
  --vsite-border:            #D6C9A8;
  --vsite-link:              #9B3D4A;
  /* Input tokens — default to white field with palette text/border */
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1C1410;
  --vsite-input-label:       #1C1410;
  --vsite-input-border:      #D6C9A8;
  --vsite-input-placeholder: #6A5B4B;
  /* Status tokens — platform values (pass on all light palettes) */
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 1 — Мавруд ─────────────────────────────────────────────
   Warm parchment + wine burgundy. Platform-family default.
   accent-text = accent (burgundy passes AA on all Мавруд surfaces).
   link = rose-ink #9B3D4A (5.08:1 min on sunk — PASS AA).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-mavrud"] {
  --vsite-bg:                #F5EDD8;
  --vsite-surface:           #FAF4E3;
  --vsite-surface-sunk:      #EDE0C4;
  --vsite-text:              #1C1410;
  --vsite-text-muted:        #6A5B4B;
  --vsite-accent:            #6B1F2A;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #6B1F2A;
  --vsite-border:            #D6C9A8;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1C1410;
  --vsite-input-label:       #1C1410;
  --vsite-input-border:      #D6C9A8;
  --vsite-input-placeholder: #6A5B4B;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 2 — Варовик ────────────────────────────────────────────
   Chalky cool white + Aegean teal. Coastal mineral character.
   accent-text = accent (teal #2B6472 passes AA on all Варовик surfaces).
   link = rose-ink #9B3D4A (5.29:1 min on sunk — PASS AA).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-varovik"] {
  --vsite-bg:                #F2F0EC;
  --vsite-surface:           #FAFAF8;
  --vsite-surface-sunk:      #E8E5DF;
  --vsite-text:              #1A1C1E;
  --vsite-text-muted:        #5A6068;
  --vsite-accent:            #2B6472;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #2B6472;
  --vsite-border:            #CFCBC2;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1A1C1E;
  --vsite-input-label:       #1A1C1E;
  --vsite-input-border:      #CFCBC2;
  --vsite-input-placeholder: #5A6068;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 3 — Керемида ───────────────────────────────────────────
   Warm off-white + fired terracotta. Urban / contemporary.
   accent-text override: #B04C2A fails on surface-sunk at 4.02:1.
   #924125 = 5.23:1 on sunk / 6.49:1 on alt / 6.02:1 on bg. AA.
   link = rose-ink #9B3D4A (4.97:1 on sunk — PASS AA 4.5:1).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-keremida"] {
  --vsite-bg:                #F4EDE5;
  --vsite-surface:           #FAF6F1;
  --vsite-surface-sunk:      #E8DDD3;
  --vsite-text:              #1A1410;
  --vsite-text-muted:        #6B5A50;
  --vsite-accent:            #B04C2A;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #924125;
  --vsite-border:            #D4C4B8;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1A1410;
  --vsite-input-label:       #1A1410;
  --vsite-input-border:      #D4C4B8;
  --vsite-input-placeholder: #6B5A50;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 4 — Трева ──────────────────────────────────────────────
   Unbleached linen + dark forest green. Natural / organic.
   accent-text = accent (forest green passes AA on all Трева surfaces).
   link = rose-ink #9B3D4A (4.91:1 on sunk — PASS AA 4.5:1).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-treva"] {
  --vsite-bg:                #EFECE4;
  --vsite-surface:           #F7F5EE;
  --vsite-surface-sunk:      #E2DDD2;
  --vsite-text:              #1A1C16;
  --vsite-text-muted:        #5A6050;
  --vsite-accent:            #3D6130;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #3D6130;
  --vsite-border:            #CCCABF;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1A1C16;
  --vsite-input-label:       #1A1C16;
  --vsite-input-border:      #CCCABF;
  --vsite-input-placeholder: #5A6050;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 5 — Розé ───────────────────────────────────────────────
   Bleached shell + dried rose. Provençal restraint.
   accent-text = accent (dried rose passes AA on all Розé surfaces).
   link = rose-ink #9B3D4A (5.14:1 on sunk — PASS AA).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-roze"] {
  --vsite-bg:                #F8F0EC;
  --vsite-surface:           #FEF8F5;
  --vsite-surface-sunk:      #EDE0D8;
  --vsite-text:              #1C1410;
  --vsite-text-muted:        #70554E;
  --vsite-accent:            #943848;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #943848;
  --vsite-border:            #D8C8C0;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1C1410;
  --vsite-input-label:       #1C1410;
  --vsite-input-border:      #D8C8C0;
  --vsite-input-placeholder: #70554E;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 6 — Хума ───────────────────────────────────────────────
   Near-black warm clay + aged amber. Inverted dark palette.
   Dark/inverted: all input + link tokens need dark-surface equivalents.
   AA verified: cream label on dark ~13:1; cream input text on sunk ~12:1;
   amber link on surface ~9:1 (verified in tokens.css before removal).
   Status overrides for dark surfaces:
     error #E06B6B: 4.71:1 on sunk / 5.57:1 on bg — PASS AA
     success #6BA35A: 5.09:1 on sunk / 6.02:1 on bg — PASS AA
     warning #C9902A: 5.45:1 on sunk / 6.45:1 on bg — PASS AA
   Platform error/success/success would fail on dark clay (1.72–2.97:1).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-huma"] {
  --vsite-bg:                #1C1510;
  --vsite-surface:           #251C15;
  --vsite-surface-sunk:      #2F231A;
  --vsite-text:              #F2E8D8;
  --vsite-text-muted:        #A8917C;
  --vsite-accent:            #C49050;
  --vsite-on-accent:         #1C1510;
  --vsite-accent-text:       #C49050;
  --vsite-border:            #3D3028;
  --vsite-link:              #C49050;
  /* Dark input treatment: lifted dark field, cream text */
  --vsite-input-bg:          #2F231A;
  --vsite-input-text:        #F2E8D8;
  --vsite-input-label:       #F2E8D8;
  --vsite-input-border:      #A8917C;
  --vsite-input-placeholder: #A8917C;
  /* Dark-surface status overrides — platform values fail on clay */
  --vsite-error:             #E06B6B;
  --vsite-success:           #6BA35A;
  --vsite-warning:           #C9902A;
}

/* ── Palette 7 — Праг ───────────────────────────────────────────────
   Near-white warm + deep warm charcoal. Business-clean warm-neutral.
   accent-text = accent (charcoal #2C2824 passes AA on all Праг surfaces).
   link = rose-ink #9B3D4A (5.39:1 on sunk — PASS AA).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-prag"] {
  --vsite-bg:                #F7F5F2;
  --vsite-surface:           #FDFCFB;
  --vsite-surface-sunk:      #EAE7E2;
  --vsite-text:              #1A1917;
  --vsite-text-muted:        #6B6560;
  --vsite-accent:            #2C2824;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #2C2824;
  --vsite-border:            #D8D4CE;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #1A1917;
  --vsite-input-label:       #1A1917;
  --vsite-input-border:      #D8D4CE;
  --vsite-input-placeholder: #6B6560;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}

/* ── Palette 8 — Туш ────────────────────────────────────────────────
   Cool neutral near-white + deep navy-ink. Business-clean cool-neutral.
   accent-text = accent (navy #1E3A52 passes AA on all Туш surfaces).
   link = rose-ink #9B3D4A (5.36:1 on sunk — PASS AA).
   ─────────────────────────────────────────────────────────────────── */
[data-theme="visitka-tush"] {
  --vsite-bg:                #F3F4F5;
  --vsite-surface:           #FAFBFB;
  --vsite-surface-sunk:      #E5E7E9;
  --vsite-text:              #181A1C;
  --vsite-text-muted:        #60686F;
  --vsite-accent:            #1E3A52;
  --vsite-on-accent:         #FFFFFF;
  --vsite-accent-text:       #1E3A52;
  --vsite-border:            #D0D4D7;
  --vsite-link:              #9B3D4A;
  --vsite-input-bg:          #FFFFFF;
  --vsite-input-text:        #181A1C;
  --vsite-input-label:       #181A1C;
  --vsite-input-border:      #D0D4D7;
  --vsite-input-placeholder: #60686F;
  --vsite-error:             #8B2323;
  --vsite-success:           #4A6B3C;
  --vsite-warning:           #B8782A;
}
}

@layer components {
/* === components/identity-header/identity-header.css === */
/* ============================================================
   IDENTITY HEADER — core identity strip above a sidebar layout
   Layer: components   Bundle: core

   PROMOTED from profile-hub.css (W8-2, 2026-08-30). The buyer's
   /profil/ identity row (monogram · name · meta · stats rail) is
   the same shape the DD asked for on WIN_01 — «защо и избата да
   не си вижда лесно тези неща». One component, three consumers:
     partials/profile-header.njk  — every /profil/* buyer page
     WIN_01 winery identity row   — via partial with winery data
     (Admin does NOT get identity-header this wave)

   Optional __actions slot (right of the stats) carries CTAs such
   as «+ Добави вино» and «Добави с AI» on WIN_01.

   Color rule: semantic layer tokens only. Primitives do not remap
   in dark mode (the D-02 trap).

   CLASS RENAME from profile-header* → identity-header*. The partial
   (profile-header.njk) emits the new classes; 14 pages follow without
   edits because they include the partial, not the class names directly.
   ============================================================ */

/* A THIN IDENTITY STRIP, not a card. See profile-hub.css deleted block
   for the DD rationale (2026-08-24): 112px → 56px. */
.identity-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) var(--spacing-md);
  margin-bottom: var(--spacing-md);
  background: var(--color-surface-sunk);
  border-radius: var(--card-radius);
  min-height: var(--profile-header-height);
}

.identity-header__monogram {
  flex-shrink: 0;
  width: var(--profile-header-monogram);
  height: var(--profile-header-monogram);
  border-radius: var(--radius-avatar);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-semibold);
  color: var(--color-text-muted);
  letter-spacing: var(--letter-spacing-allcaps);
}

/* Identity block: column stack — title-row (name + plan pill) then meta under.
   min-width: 0 is mandatory: without it a flex child cannot shrink below its
   content width, so the name never truncates and the meta never receives space. */
.identity-header__identity {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

/* Title row: name + plan pill on ONE line.
   min-width: 0 propagates the shrink budget into the name child. */
.identity-header__title-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  min-width: 0;
  flex-wrap: nowrap;
}

.identity-header__name {
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-display-medium);
  line-height: var(--line-height-tight);
  color: var(--color-text-primary);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.identity-header__meta {
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- Stats rail — desktop only ----------------------------- */
.identity-header__stats {
  display: flex;
  gap: var(--spacing-lg);
  margin: 0;
  flex-shrink: 0;
}

.identity-header__stat {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-xs);
}

.identity-header__stat dt {
  font-family: var(--font-ui);
  font-size: var(--font-size-micro);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-allcaps);
  color: var(--color-text-muted);
  margin: 0;
}

.identity-header__stat dd {
  font-family: var(--font-display);
  font-size: var(--font-size-body);
  line-height: var(--line-height-tight);
  color: var(--color-text-primary);
  margin: 0;
}

/* ---- Plan pill slot — winery variant (WIN_01); lives inside __title-row.
   Wraps a .status-pill --plan-{tier} badge. Buyer variant omits this element. */
.identity-header__plan {
  flex-shrink: 0;
}

/* ---- Actions slot (optional) — e.g. «+ Добави вино» on WIN_01 */
.identity-header__actions {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  flex-shrink: 0;
  margin-left: var(--spacing-sm);
}

/* ---- Responsive -------------------------------------------- */
@media (max-width: 768px) {
  .identity-header {
    padding: var(--spacing-sm) var(--spacing-md);
    margin-bottom: var(--spacing-sm);
  }
  .identity-header__identity { display: block; }
  .identity-header__stats { display: none; }
  .identity-header__meta-extra { display: none; }
  .identity-header__actions { display: none; }

  /* On a phone, identity belongs to the hub, not to every leaf.
     Selector reads structure: identity header immediately before
     .sidebar-layout is a drill-down leaf — hide on phone. */
  .identity-header:has(+ .sidebar-layout) { display: none; }
}
}

@layer components {
/* === components/checkbox-line/checkbox-line.css === */
/* checkbox-line — a flex label row that wraps a native checkbox or radio + text
   ------------------------------------------------------------------------------
   REVIEW 260828, promotion 7 of 8.
   Six page-local names across eight page files, all sharing the same shape:
   «flex row, gap, min-height: 44px, accent-color on the native control».

   SOURCE NAMES AND SCREENS:
     .inline-checkbox-label       WIN_03_PRODUCT_SUBMIT (×1 natural-wine,
                                  ×5 pkg variants) + ADM_24_PRODUCT_FORM (×1 + ×4)
                                  + ADM_24_PRODUCT_FORM_edit (×1 + ×3)
     .wine-picker-row__check      WIN_19_PACK_FORM (×3 rows, disabled state)
     .vs-wine-check-label         WIN_23_VISITKA (×4, disabled state via JS)
     .vs-subtoggle-label          WIN_23_VISITKA (×1, muted text — sub-item)
     .win17-package-option        WIN_17_PRODUCT_DETAIL + WIN_17_PRODUCT_DETAIL_team
                                  (×4 each)
     .radio-label / .radio-input  ADM_07_CLAIM_DETAIL (×3 radio buttons)

   CONTROL SIZE: 16px.
   Split across sources: 18px — WIN_03, WIN_19, WIN_23 (wine-check); 16px — WIN_17,
   WIN_23 (subtoggle), ADM_07, and the app's own checkbox-line (no explicit size →
   browser default, ~16px on all tested platforms). 16px wins: majority (4 sources
   including the app's live behaviour) and consistency with checkbox-checklist (16px,
   same design-system session). The 18px sources are also the ones carrying redundant
   inline style= attributes that are removed in this migration.

   MODIFIERS:
   --disabled: opacity 0.5 + not-allowed cursor. Consumed by WIN_19 (static HTML
               disabled state) and WIN_23 (JS-driven max-selection guard).
   --muted:    color becomes --color-text-muted. Consumed by WIN_23 .vs-subtoggle-label,
               which is a secondary sub-item indented below its parent toggle. The muted
               text is intentional visual hierarchy — it de-emphasises the secondary
               option relative to the parent row. One consumer is enough: the semantic
               distinction (primary vs sub-item) is clear and specific.

   RADIO: The app uses checkbox-line for both type="checkbox" and type="radio" (245
   checkbox + radio instances across 10 screens). The .radio-label / .radio-input
   pair on ADM_07_CLAIM_DETAIL was the only radio consumer. It also used
   `accent-color: var(--color-border-focus)` for the radio — a defect (wrong token,
   focus-blue accent on a data choice). Canonical: --color-accent-primary.

   NOT IN SCOPE (listed as future adopters in README):
     .fcol__opt       search-variants, storefront lane
     .auth-checkbox-label   PUB_50

   APP VOCABULARY IS BINDING.
   The live app ships `<label class="checkbox-line"><input …> Текст</label>`.
   Our selector matches that shape exactly. */

.checkbox-line {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  min-height: var(--touch-target-min);
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  color: var(--color-text-primary);
  cursor: pointer;
}

/* Native control — checkbox and radio treated identically */
.checkbox-line input[type="checkbox"],
.checkbox-line input[type="radio"] {
  accent-color: var(--color-accent-primary);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  cursor: pointer;
  margin: 0;
}

/* Disabled state — applied by HTML attribute or JS classList */
.checkbox-line--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.checkbox-line--disabled input[type="checkbox"],
.checkbox-line--disabled input[type="radio"] {
  cursor: not-allowed;
}

/* Muted text — secondary / sub-item rows (WIN_23 subtoggle) */
.checkbox-line--muted {
  color: var(--color-text-muted);
}
}

@layer components {
/* === components/chip/chip.css === */
/* chip — selectable toggle chip and removable selected tag
   ----------------------------------------------------------
   REVIEW 260828, promotion 8 of 8.
   Two names, two forms on our pages; 75 app instances across two crawl screens.

   SOURCE NAMES AND SCREENS (our pages):
     .taste-chips-list / .taste-chip / .taste-chip__remove
       WIN_03_PRODUCT_SUBMIT (2 demo chips, JS-driven),
       ADM_24_PRODUCT_FORM (3 demo chips, JS-driven),
       ADM_24_PRODUCT_FORM_edit (3 demo chips, JS-driven).
       These are REMOVABLE selected tags: filled accent, × button.

   APP VOCABULARY (live crawl — binding):
     .chip-row > label.chip > input.chip__checkbox.sr-only
       SELECTABLE toggle chips (pairing tags, grape pickers).
       Selected: .chip--selected or :has(.chip__checkbox:checked).
       75 instances on panel-vina-novo.html + panel-vina-id-redakcia.html.

   TWO FORMS — THREE SELECTION SIGNALS — ONE COMPONENT:
   The same visual pill serves two interaction patterns:
     A) SELECTABLE: label wraps a hidden checkbox; JS selects by checking it.
        Expressed by :has(.chip__checkbox:checked).
     B) REMOVABLE: li carries the chip; a × button removes the li from the list.
        Expressed by :has(.chip__remove).
   Both render as accent fill + white text. A third explicit class (.chip--selected)
   covers the static/server-rendered selected case.
   All three must produce identical computed styles — tested at build time.

   AD RULING (260828): component accepted with these three co-equal selected
   expressions. The hover state follows filter-chip's token pattern (border-strong,
   surface-sunk for unselected; accent-hover for selected).

   CONTAINER (.chip-row):
   Flex wrap row with gap-sm. min-height = --touch-target-min so an empty
   row (e.g. before the user adds any chips) does not collapse to zero.

   FOCUS:
   chip__checkbox is visually hidden by the component itself (one-class contract, see below).
   The .sr-only class lives in base/base.css; do not re-declare its geometry here.
   :has(.chip__checkbox:focus-visible) shows --shadow-focus-ring on the chip label.

   REMOVE BUTTON:
   Visual glyph 24×24 px. Hit area expanded to 44×44 px via ::before pseudo-element.
   inset = calc((24px − --touch-target-min) / 2). The 24px is the element's own
   min-width/min-height (a layout constant, not a design-system token). */

/* ── Container ──────────────────────────────────────────────────────────── */

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-sm);
  /* Reserve row height so an empty state does not collapse */
  min-height: var(--touch-target-min);
}

/* ── Chip base — unselected ─────────────────────────────────────────────── */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-full);
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-ui-medium);
  color: var(--color-text-primary);
  background: var(--color-surface-raised);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-fast), border-color var(--duration-fast),
    color var(--duration-fast);
}

.chip:hover {
  border-color: var(--color-border-strong);
  background: var(--color-surface-sunk);
}

/* ── Selected — three co-equal expressions ──────────────────────────────── */

/* (1) Explicit class — static HTML and server-rendered */
/* (2) :has(checkbox:checked) — selectable toggle chips (app pattern) */
/* (3) :has(.chip__remove) — a removable chip is by definition selected */
.chip--selected,
.chip:has(.chip__checkbox:checked),
.chip:has(.chip__remove) {
  background: var(--color-accent-primary);
  border-color: var(--color-accent-primary);
  color: var(--color-text-on-accent);
}

.chip--selected:hover,
.chip:has(.chip__checkbox:checked):hover,
.chip:has(.chip__remove):hover {
  background: var(--color-accent-primary-hover);
  border-color: var(--color-accent-primary-hover);
  color: var(--color-text-on-accent);
}

/* ── Checkbox (hidden, focusable) ───────────────────────────────────────── */

/* The native control is visually hidden but stays in the tab order, so the
   chip is keyboard-selectable and announced as a checkbox. The geometry is
   the .sr-only body from base/base.css, repeated here on purpose (AD 260827):
   the app's markup — which is binding — is `<input class="chip__checkbox">`
   with no second class, and a component that needs the page to remember
   `sr-only` renders a raw checkbox inside the pill the first time someone
   forgets. check-duplication will list this beside .sr-only; that is the
   accepted cost of a one-class contract. */
.chip__checkbox {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Focus ring on the containing label */
.chip:has(.chip__checkbox:focus-visible) {
  box-shadow: var(--shadow-focus-ring);
  outline: none;
}

/* ── Remove button ──────────────────────────────────────────────────────── */

.chip__remove {
  background: none;
  border: none;
  color: inherit;
  font-size: var(--font-size-body);
  line-height: 1;
  cursor: pointer;
  padding: 0 0 0 var(--spacing-xs);
  /* Establish containing block for the ::before hit-area expansion */
  position: relative;
  min-width: 24px;
  min-height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Expand click/tap area to 44×44 px without affecting layout */
.chip__remove::before {
  content: "";
  position: absolute;
  /* inset = (glyph-size − touch-target) / 2 = (24px − 44px) / 2 */
  inset: calc((24px - var(--touch-target-min)) / 2);
}

/* ── chip--check — counter-limited checkbox tile (W12-C3) ───────────────── */
/*
 * Markup:
 *   <fieldset data-limit="6" aria-describedby="counter-{id}">
 *     <legend>Подходящо с</legend>
 *     <div class="chip-row">
 *       <label class="chip chip--check">
 *         <input type="checkbox" name="pairing[]" value="1"> 🍖 Дивеч
 *       </label>
 *       …more labels…
 *     </div>
 *     <!-- chip-limit.js injects or populates: -->
 *     <div class="chip-limit-counter" aria-live="polite" aria-atomic="true"
 *          id="counter-{id}">0 / 6 избрани</div>
 *   </fieldset>
 *
 * Selected tone   = existing chip selected (accent fill) via :has(input:checked)
 * Disabled tone   = opacity 0.5 + cursor not-allowed (not colour-only)
 * Focus ring      = --shadow-focus-ring on the label
 *
 * Relationship to filter-chip:
 *   filter-chip is a navigation element (<a> or <button>, no input).
 *   chip--check is a form control (<label> + hidden <input type="checkbox">).
 *   Hover/selected tones are aligned; rules are not duplicated.
 *
 * Pre-existing chip--check vs chip__checkbox:
 *   chip__checkbox (existing) is hidden by component class on the input.
 *   chip--check hides the native input via a child selector so the one-class
 *   app contract (input.chip__checkbox) is unaffected.
 */

.chip--check {
  /* Allow labels like «Телешко и агнешко» to wrap within the tile. */
  white-space: normal;
  /* 44px minimum touch target height — spec W12-C3 + W12-8. */
  min-height: var(--touch-target-min);
  /* Flex layout: glyph + label text inline. */
  align-items: center;
}

/* Hide native checkbox visually while keeping it keyboard-accessible. */
.chip--check input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Selected state — reuse existing accent tones (no duplicate rules). */
.chip--check:has(input[type="checkbox"]:checked) {
  background: var(--color-accent-primary);
  border-color: var(--color-accent-primary);
  color: var(--color-text-on-accent);
}

.chip--check:has(input[type="checkbox"]:checked):hover {
  background: var(--color-accent-primary-hover);
  border-color: var(--color-accent-primary-hover);
  color: var(--color-text-on-accent);
}

/* Focus ring on label when hidden input is focused. */
.chip--check:has(input[type="checkbox"]:focus-visible) {
  box-shadow: var(--shadow-focus-ring);
  outline: none;
}

/* ── Disabled at limit (not colour-only) ────────────────────────────────── */
/*
 * chip-limit.js sets input.disabled = true on unchecked boxes when the
 * group reaches its limit. The chip also receives aria-disabled="true"
 * for AT consumers.
 *
 * NOT colour-only: opacity reduces ALL visual properties (text, border,
 * background, glyph) uniformly. cursor:not-allowed adds a non-colour signal.
 * Spec W12-C3: "reduced opacity + aria-disabled" satisfies WCAG 1.4.1.
 */
.chip--check:has(input[type="checkbox"]:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

.chip--check:has(input[type="checkbox"]:disabled) input[type="checkbox"] {
  cursor: not-allowed;
}

/* Counter row below the chip-row — injected / targeted by chip-limit.js */
.chip-limit-counter {
  font-family: var(--font-ui);
  font-size: var(--font-size-micro);
  color: var(--color-text-muted);
  text-align: right;
  margin-top: var(--spacing-xs);
  width: 100%;
}

/* ── Reduced motion ─────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .chip {
    transition: none;
  }
}

/* ── chip--check: the check mark (DD 2026-08-31) ────────────────────────────
   «да има чекче или нещо такова». The accent fill alone said «selected» only
   to someone who knew the unselected tone — and on the glass tiles, where a
   photographic render sits on the fill, the tone reads as a background rather
   than as a state. A ✓ says it outright.

   ::after, so no markup changes and no extra node per tile. It appears only
   when the input is checked, and it is NOT the only signal — fill, border and
   text colour all still change, so this adds redundancy rather than replacing
   colour (WCAG 1.4.1 was already satisfied; this makes it obvious). */
.chip--check::after {
  content: "";
  flex: 0 0 auto;
  width: 0;
  overflow: hidden;
  transition: width var(--duration-fast);
}
.chip--check:has(input[type="checkbox"]:checked)::after {
  content: "\2713";
  width: 1em;
  margin-left: var(--spacing-xs);
  font-weight: var(--font-weight-ui-semibold);
  line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
  .chip--check::after { transition: none; }
}
}

@layer components {
/* === components/modal/modal.css === */
/* ============================================================
   MODAL
   bundle:core layer:components
   W12-C2 (2026-08-31) — promoted from ADM_24 page-local modal.
   Uses native <dialog> + showModal(). ::backdrop replaces the
   wrapper div overlay. No JS in this file — see modal.js.

   Anatomy:
     <dialog class="modal [modal--wide] [modal--narrow]"
             id="…" aria-labelledby="modal-title-…">
       <div class="modal__header">
         <h2 class="modal__title" id="modal-title-…">…</h2>
         <button class="modal__close" data-dialog-close
                 aria-label="Затвори">×</button>
       </div>
       <div class="modal__body">…</div>
       <div class="modal__footer">…</div>  <!-- optional -->
     </dialog>

   Trigger: <button data-dialog-open="dialog-id">…</button>
   Close:   data-dialog-close on any element inside the dialog.
   App class vocabulary (binding — panel-vina-novo.html):
     .modal  .modal__header  .modal__title  .modal__close
   ============================================================ */

/* ── Dialog reset ────────────────────────────────────────────────────────── */

dialog.modal {
  /* Override UA dialog positioning so margin:auto centres within viewport. */
  position: fixed;
  inset: 0;
  margin: auto;
  padding: 0;

  /* Size — default is medium. */
  width: 100%;
  max-width: var(--modal-max-width-md);

  /* Visual */
  background: var(--modal-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--modal-radius);
  box-shadow: var(--modal-shadow);

  /* Stacking — above all page content, below toasts. */
  z-index: var(--z-modal);

  /* Remove browser default border. */
  outline: none;
}

/* ── Overlay (::backdrop) ────────────────────────────────────────────────── */

dialog.modal::backdrop {
  background: var(--modal-overlay-bg);
}

/* ── Sizes ───────────────────────────────────────────────────────────────── */

dialog.modal.modal--wide {
  max-width: var(--modal-max-width-lg);
}

dialog.modal.modal--narrow {
  max-width: var(--modal-max-width-sm);
}

/* ── Enter animation ─────────────────────────────────────────────────────── */

@keyframes modal-enter {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

dialog.modal[open] {
  animation: modal-enter var(--modal-enter-duration) ease-out;
}

/* ── Header ──────────────────────────────────────────────────────────────── */

.modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--spacing-md);
  padding: var(--modal-header-padding);
  border-bottom: 1px solid var(--color-border-default);
}

.modal__title {
  font-family: var(--font-display);
  font-size: var(--font-size-h4);
  font-weight: var(--font-weight-display-medium);
  color: var(--color-text-primary);
  margin: 0;
  line-height: var(--line-height-compact);
}

.modal__close {
  flex-shrink: 0;
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--font-size-body-lg);
  line-height: 1;
  color: var(--color-text-muted);
  padding: 0;
  /* Ensure minimum touch target — expands click area via padding+margin trick. */
  min-width: var(--touch-target-min);
  min-height: var(--touch-target-min);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Pull right-edge to align with header padding. */
  margin: calc(var(--spacing-xs) * -1) calc(var(--spacing-xs) * -1) 0 0;
  border-radius: var(--radius-button);
  transition: color var(--duration-fast), background var(--duration-fast);
}

/* M-01 W11-6d: hover guarded to prevent sticky state on touch/band devices */
@media (hover: hover) {
  .modal__close:hover {
    color: var(--color-text-primary);
    background: var(--color-surface-sunk);
  }
}

.modal__close:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus-ring);
}

/* ── Body ────────────────────────────────────────────────────────────────── */

.modal__body {
  padding: var(--modal-padding);
  font-family: var(--font-ui);
  font-size: var(--font-size-small);
  color: var(--color-text-primary);
  line-height: var(--line-height-body);
}

/* ── Footer (optional actions slot) ─────────────────────────────────────── */

.modal__footer {
  display: flex;
  gap: var(--spacing-md);
  flex-wrap: wrap;
  padding: var(--spacing-md) var(--modal-padding) var(--modal-padding);
  border-top: 1px solid var(--color-border-default);
}

/* Default: actions aligned to the right (cancel first per guardrail UX). */
.modal__footer--end {
  justify-content: flex-end;
}

/* ── Mobile full-width (≤ 640px) ─────────────────────────────────────────── */

@media (max-width: 640px) {
  dialog.modal {
    /* Full-width with safe-area gutters; inset-inline overrides margin:auto sides. */
    max-width: none;
    margin: auto var(--spacing-md) var(--spacing-md);
    border-radius: var(--modal-radius) var(--modal-radius) 0 0;
    /* Stick to bottom on mobile (sheet pattern). */
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    inset: auto 0 0 0;
    margin: 0;
  }

  .modal__footer {
    flex-direction: column;
  }

  /* Full-width buttons in footer on mobile. */
  .modal__footer .btn {
    width: 100%;
    justify-content: center;
  }
}

/* ── Dark theme ──────────────────────────────────────────────────────────── */

/* Tokens remap automatically via [data-theme="dark"] in tokens.css.
   No extra rules needed — all values trace to semantic tokens. */

/* ── Reduced motion ──────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  dialog.modal[open] {
    animation: none;
  }
}
}
