/* UI tokens + accessibility defaults (Wise-inspired, not copied) */
/* This file is intentionally plain CSS (served by Propshaft) so it is easy to revert. */

:root {
  /* Tell the browser we support both schemes; actual colors come from tokens. */
  color-scheme: light;

  /* Global chrome */
  --app-header-height: 64px;

  /* Typography */
  --ui-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  --ui-font-display: "Archivo Black", var(--ui-font-sans);
  --ui-font-interface: var(--ui-font-sans); /* Inter — UI/body voice (alias used by SuccessModal etc.) */

  /* Surfaces (light) */
  --bg: #f5f5f0;
  --surface: #e8ebe4; /* sage/gray: Wise-inspired container background */
  --surface2: #ecefec; /* cards float on surface */
  --surface2-hover: #e1e5de; /* card hover state */
  --border: #d4d9ce;

  /* Text (light) */
  --text: #0b1220;
  --muted: #44506a;
  --subtle: #6b7280;

  /* Brand (core blue) */
  --primary: var(--core-ocean-blue);
  --accent: var(--primary); /* legacy alias used across payment_links + shared button/focus rings */
  --primary_hover: color-mix(in oklab, var(--primary) 85%, black);
  --primary_active: color-mix(in oklab, var(--primary) 70%, black);
  --primary_fg: var(--core-bright-blue);
  --link: var(--core-ocean-blue);
  --button-secondary-text: var(--core-ocean-blue);

  /* Mono base aliases — resolvable in every scope (legacy :root + .app-mono).
     Defined here so the retired state tokens below resolve even on pages that
     have not yet opted into the .app-mono scope. */
  --white: #ffffff;
  --paper: #ffffff; /* same value in .landing + .app-mono; aliased here so registry components (e.g. .ds-search__submit--round) resolve in every scope */
  --ink: #0e0e0c;
  --gray: #6b6b66;
  --line: #e4e4e0;
  --brick: #993d33;
  --stone: #999998;

  /* States — retired to Mono (ink + brick only): success/warning = ink,
     danger = brick, lifecycle ramp mirrors the canonical .app-mono block. */
  --success: var(--ink);
  --success_fg: #ffffff;
  --warning: var(--ink);
  --warning_fg: #ffffff;
  --danger: var(--brick);
  --danger_fg: #ffffff;
  --error: var(--danger);
  --status-in-stock: color-mix(in oklab, var(--ink) 16%, var(--white));
  --status-reserved: var(--stone);
  --status-listed: var(--gray);
  --status-sold: var(--ink);
  --status-defective: var(--brick);

  /* Bright accent colors for status bars (light mode) */
  --bright-orange: #FFC091;
  --bright-yellow: #FFEB69;
  --bright-blue: #A0E1E1;
  --bright-pink: #FFD7EF;

  /* Focus: must stay visible on both light and dark surfaces */
  --focus_ring: #3080ff; /* close to Tailwind blue-500 */
  --focus_ring_offset: #ffffff;

  /* Padding tokens */
  --padding-x-small: 8px;
  --padding-small: 16px;
  --padding-medium: 24px;
  --padding-large: 32px;

  /* Size tokens */
  --size-x-small: 24px;
  --size-medium: 40px;
  --size-large: 48px;
  --size-x-large: 56px;
  --size-2x-large: 72px;

  /* Radius tokens (desktop scale) */
  --radius-small: 16px;
  --radius-medium: 20px;
  --radius-large: 30px;
  --radius-x-large: 40px;
  --radius-2x-large: 60px;

  /* Shadow tokens */
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);

  /* Foundational spacing tokens */
  --size-4: 4px;
  --size-8: 8px;
  --size-12: 12px;
  --size-16: 16px;
  --size-24: 24px;
  --size-32: 32px;
  --size-40: 40px;
  --size-48: 48px;
  --size-56: 56px;
  --size-64: 64px;
  --size-72: 72px;
  --size-80: 80px;
  --size-88: 88px;
  --size-96: 96px;
  --size-104: 104px;
  --size-112: 112px;
  --size-120: 120px;
  --size-128: 128px;
}

html[data-theme="dark"] {
  color-scheme: dark;

  /* Surfaces (dark) */
  --bg: #121511;
  --surface: #1a1d18; /* darker container */
  --surface2: #292d29; /* cards */
  --surface2-hover: #404441; /* card hover state */
  --border: #2e332b;

  /* Text (dark) */
  --text: #e6eaf2;
  --muted: #b8c0d4;
  --subtle: #95a0b8;

  /* Brand (core blue) */
  --primary: var(--core-bright-blue);
  --accent: var(--primary);
  --primary_fg: var(--core-ocean-blue);
  --link: var(--core-bright-blue);
  --button-secondary-text: var(--core-bright-blue);

  /* States — Mono retirement (dark): success/warning read as dark-scope ink
     (--text, the light foreground), danger = brick, status blues → neutral.
     Surface/background tokens above are intentionally left intact. */
  --success: var(--text);
  --success_fg: #071215;
  --warning: var(--text);
  --warning_fg: #141106;
  --danger: var(--brick);
  --danger_fg: #ffffff;
  --status-in-stock: var(--subtle);
  --status-listed: var(--subtle);
  --status-sold: var(--success);
  --status-defective: var(--danger);

  /* Bright accent colors for status bars (dark mode)
     Use same bright colors as light mode for visibility */
  --bright-orange: #FFC091;
  --bright-yellow: #FFEB69;
  --bright-blue: #A0E1E1;
  --bright-pink: #FFD7EF;

  --focus_ring: #60a5fa;
  --focus_ring_offset: #070b12;

  /* Shadow tokens (dark mode - slightly more visible) */
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.16);
}

@media (max-width: 767px) {
  :root {
    /* Radius tokens (mobile scale) */
    --radius-small: 10px;
    --radius-medium: 16px;
    --radius-large: 24px;
    --radius-x-large: 32px;
    --radius-2x-large: 48px;
  }
}

html {
  font-family: var(--ui-font-sans);
}

/* Grid container (Wise-like margins) */
.ds-grid-container {
  max-width: 1440px;
  margin: 0 auto;
  padding-inline: 20px;
}

@media (min-width: 480px) {
  .ds-grid-container {
    padding-inline: 32px;
  }
}

@media (min-width: 768px) {
  .ds-grid-container {
    padding-inline: 40px;
  }
}

@media (min-width: 992px) {
  .ds-grid-container {
    padding-inline: 80px;
  }
}

@media (min-width: 1200px) {
  .ds-grid-container {
    padding-inline: 100px;
  }
}

/* Typography foundations helpers */
.design-typography__display {
  font-family: var(--ui-font-display);
  letter-spacing: 0.01em;
}

/* Title Screen: Main page title (30px, semibold, -2.5% tracking, 34px line-height) */
.ds-title-screen {
  font-size: 30px;
  font-weight: 600;
  line-height: 34px;
  letter-spacing: -0.025em;
  color: var(--text);
}

/* Title Section: Section headings (26px, semibold, -1.5% tracking) */
.ds-title-section {
  font-size: 26px;
  font-weight: 600;
  line-height: 32px;
  letter-spacing: -0.015em;
  color: var(--text);
}

/* Title Subsection: Subsection headings (22px, semibold, -1.5% tracking) */
.ds-title-subsection {
  font-size: 22px;
  font-weight: 600;
  line-height: 28px;
  letter-spacing: -0.015em;
  color: var(--text);
}

/* Title Body: Body headings (18px, semibold, -1% tracking) */
.ds-title-body {
  font-size: 18px;
  font-weight: 600;
  line-height: 24px;
  letter-spacing: -0.01em;
  color: var(--text);
}

/* Padding tokens helpers */
.ui-padding-x-small {
  padding: var(--padding-x-small);
}

.ui-padding-small {
  padding: var(--padding-small);
}

.ui-padding-medium {
  padding: var(--padding-medium);
}

.ui-padding-large {
  padding: var(--padding-large);
}

/* Size tokens helpers */
.ui-size-x-small {
  width: var(--size-x-small);
  height: var(--size-x-small);
}

.ui-size-medium {
  width: var(--size-medium);
  height: var(--size-medium);
}

.ui-size-large {
  width: var(--size-large);
  height: var(--size-large);
}

.ui-size-x-large {
  width: var(--size-x-large);
  height: var(--size-x-large);
}

.ui-size-2x-large {
  width: var(--size-2x-large);
  height: var(--size-2x-large);
}

.ui-size-circle {
  border-radius: 9999px;
  background: color-mix(in oklab, var(--primary) 18%, transparent);
  border: 1px solid color-mix(in oklab, var(--primary) 35%, transparent);
}

/* Radius helpers */
.ui-radius-small {
  border-radius: var(--radius-small);
}

.ui-radius-medium {
  border-radius: var(--radius-medium);
}

.ui-radius-large {
  border-radius: var(--radius-large);
}

.ui-radius-x-large {
  border-radius: var(--radius-x-large);
}

.ui-radius-2x-large {
  border-radius: var(--radius-2x-large);
}

/* Focus ring helper */
.ui-focus-ring:focus-visible {
  outline: 2px solid var(--focus_ring);
  outline-offset: 2px;
}

/* Form controls */
.ui-input {
  display: flex;
  align-items: center;
  gap: var(--size-8);
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-medium);
  padding: var(--size-8) var(--padding-small);
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

/* Mono Input focus = solid 2px ink edge (1px border + 1px inset), NOT a glow. Ink (not
   --focus_ring) so it matches .ds-field on every surface, incl. root/landing where focus_ring is blue. */
.ui-input:focus-within {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.ui-input__field {
  flex: 1;
  min-width: 0;
  background: transparent;
  color: var(--text);
}

.ui-input__field:focus-visible {
  outline: none;
}

.ui-input__field::placeholder {
  color: var(--subtle);
}

.ui-control {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-medium);
  padding: var(--size-8) var(--padding-small);
  color: var(--text);
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.ui-control:focus-visible {
  outline: 2px solid var(--focus_ring);
  outline-offset: 2px;
  border-color: var(--focus_ring);
}

.ui-button-danger {
  background: var(--danger);
  color: var(--danger_fg);
  border: 1px solid color-mix(in oklab, var(--danger) 80%, transparent);
  transition: background-color 150ms ease, border-color 150ms ease;
}

.ui-button-danger:hover {
  background: color-mix(in oklab, var(--danger) 85%, black);
  border-color: color-mix(in oklab, var(--danger) 85%, black);
}

body {
  background: var(--bg);
  color: var(--text);
}

/* Account menu (avatar dropdown) */
.account-menu__panel {
  /* Ensure the dropdown never becomes transparent, even if utility classes change */
  background-color: var(--surface2) !important;
  /* Grows from the trigger it hangs off (the panel is anchored right-0 under the avatar), so the
     menu reads as coming FROM the button rather than materialising over it. */
  transform-origin: top right;
}

.account-menu__panel a.account-menu__item,
.account-menu__panel a.account-menu__item:hover,
.account-menu__panel a.account-menu__item:focus,
.account-menu__panel a.account-menu__item:active {
  /* Prevent global link styling (blue + underline) inside dropdown rows */
  color: inherit;
  text-decoration: none;
}

.account-menu__item {
  /* Make anchors look like the other dropdown "buttons" */
  color: var(--text);
  text-decoration: none;
}

/* Physicality: the avatar trigger presses with the one Mono spring (transform only —
   a quiet control, no tint), reduced-motion-safe. Standalone (never combined with
   ds-button). */
.account-menu__trigger {
  transition: background-color 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.account-menu__trigger:hover,
.account-menu__trigger:focus-visible,
.account-menu__trigger[aria-expanded="true"] {
  /* Match the desktop sidebar active highlight */
  background-color: color-mix(in oklab, var(--primary) 10%, transparent) !important;
}
@media (prefers-reduced-motion: no-preference) {
  .account-menu__trigger:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}

.account-menu__item:hover,
.account-menu__item:focus-visible {
  /* Same hover behavior as sidebar items */
  background-color: color-mix(in oklab, var(--primary) 10%, transparent);
}

.account-menu__icon-wrap {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  line-height: 0;
}

.account-menu__icon {
  display: block;
  color: var(--muted);
  align-self: center;
}

.account-menu__item:hover .account-menu__icon,
.account-menu__item:focus-visible .account-menu__icon {
  color: var(--primary);
}

.inventory-status__modal-overlay {
  background-color: rgba(0, 0, 0, 0.6);
}

.inventory-status__modal-panel {
  background-color: var(--surface2) !important;
}

.inventory-table__grid {
  display: grid;
  grid-template-columns: 36px minmax(200px, 2.8fr) minmax(80px, 0.7fr) minmax(110px, 0.85fr) minmax(90px, 0.7fr) 28px;
  gap: 10px;
  align-items: center;
}

.inventory-table__frame {
  background: var(--surface2);
  box-shadow: var(--shadow-card);
  padding: 8px;
  border-radius: 24px;
}

.inventory-table__card {
  background: var(--surface2);
  border-radius: 18px;
  overflow: hidden;
}

.inventory-table__header {
  background-color: transparent;
}

.inventory-table__row {
  position: relative;
}

.inventory-table__separator {
  height: 1px;
  margin: 0 22px;
  background-color: color-mix(in oklab, var(--text) 12%, transparent);
}

.inventory-table__card > turbo-frame:last-child .inventory-table__separator {
  display: none;
}

.inventory-table__row::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  top: 6px;
  bottom: 6px;
  border-radius: 16px;
  background-color: transparent;
  transition: background-color 150ms ease;
  pointer-events: none;
  z-index: 0;
}

.inventory-table__row > .inventory-table__grid {
  position: relative;
  z-index: 1;
}

.inventory-table__row:hover::after,
.inventory-table__row:focus-visible::after {
  background-color: color-mix(in oklab, var(--primary) 10%, transparent);
}

.status-bar__fill {
  background-color: var(--status-in-stock);
}

.status-bar__fill--in_stock {
  background-color: var(--status-in-stock);
}

.status-bar__fill--listed {
  background-color: var(--status-listed);
}

.status-bar__fill--sold {
  background-color: var(--status-sold);
}

.status-bar__fill--defective {
  background-color: var(--status-defective);
}

.peer:checked + .status-pill--in_stock {
  background-color: var(--status-in-stock);
  border-color: transparent;
  color: var(--text);
}

.peer:checked + .status-pill--listed {
  background-color: var(--status-listed);
  border-color: transparent;
  color: var(--text);
}

.peer:checked + .status-pill--sold {
  background-color: var(--status-sold);
  border-color: transparent;
  color: var(--text);
}

.peer:checked + .status-pill--defective {
  background-color: var(--status-defective);
  border-color: transparent;
  color: var(--text);
}

.status-badge {
  color: var(--text);
}

.status-badge--in_stock {
  background-color: var(--status-in-stock);
  border-color: transparent;
  color: var(--text);
}

.status-badge--listed {
  background-color: var(--status-listed);
  border-color: transparent;
  color: var(--text);
}

.status-badge--sold {
  background-color: var(--status-sold);
  border-color: transparent;
  color: var(--text);
}

.status-badge--defective {
  background-color: var(--status-defective);
  border-color: transparent;
  color: var(--text);
}

.account-menu__segment[aria-checked="true"] .account-menu__icon {
  color: var(--text);
}

.account-menu__segment:hover .account-menu__icon,
.account-menu__segment:focus-visible .account-menu__icon {
  color: var(--primary);
}

/* Segmented control (Wise-ish) used inside account menu */
.account-menu__segmented {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  height: 40px;
  min-width: 170px;
  padding: 4px;
  border-radius: 9999px;
  background: color-mix(in oklab, var(--text) 8%, transparent);
  overflow: hidden;
}

.account-menu__theme-row:hover .account-menu__segmented,
.account-menu__theme-row:focus-within .account-menu__segmented {
  /* Hover highlight should be on the pill itself (Wise-like), not on a square row */
  background: color-mix(in oklab, var(--text) 12%, transparent);
}

.account-menu__segmented::before {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: calc(50% - 4px);
  height: calc(100% - 8px);
  border-radius: 9999px;
  background: var(--surface2);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
  transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.account-menu__segmented[data-active="dark"]::before,
html[data-theme="dark"] .account-menu__segmented::before {
  transform: translateX(100%);
}

.account-menu__segment {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  height: 100%;
  border-radius: 9999px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  background: transparent;
  padding: 0;
  border: none;
  outline: none;
}

.account-menu__segmented--menu {
  width: 100%;
  min-width: 0;
  /* Base segmented has 4px padding, so make the content box 36px tall:
     44px total height - 8px padding = 36px (matches .account-menu__icon-wrap) */
  height: 44px;
}

.account-menu__segmented[data-active="light"] .account-menu__segment:first-child,
html[data-theme="light"] .account-menu__segment:first-child {
  color: var(--text);
}

.account-menu__segmented[data-active="dark"] .account-menu__segment:last-child,
html[data-theme="dark"] .account-menu__segment:last-child {
  color: var(--text);
}

.account-menu__segment:focus-visible {
  outline: 2px solid var(--focus_ring);
  outline-offset: 2px;
}

/* Links — kept in @layer base so Tailwind's layered text-color / text-decoration
   utilities (in @layer utilities, which tailwind.css orders AFTER base and loads
   before this sheet) win on <a> elements. An UNLAYERED `a {}` outranks layered
   utilities regardless of specificity, which silently forced e.g. `text-[var(--bg)]`
   on an anchor back to the link colour (the invisible active-pill bug, PR #290).
   Links without a colour utility still get var(--link) — behaviour unchanged. */
@layer base {
  a {
    color: var(--link);
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
  }

  a:hover {
    text-decoration: underline;
  }
}

/* Sidebar should NOT look like a regular "link list" (no underline, no link-blue). */
.desktop-sidebar a,
.desktop-sidebar a:hover,
.desktop-sidebar a:focus,
.desktop-sidebar a:active,
.mobile-bottom-nav a,
.mobile-bottom-nav a:hover,
.mobile-bottom-nav a:focus,
.mobile-bottom-nav a:active {
  text-decoration: none;
}

/* Theme toggle icon swapping (works without JS too) */
.theme-icon-sun {
  display: none;
}

html[data-theme="dark"] .theme-icon-sun {
  display: block;
}

html[data-theme="dark"] .theme-icon-moon {
  display: none;
}

/* Header logo swapping */
.logo-dark {
  display: none;
}

html[data-theme="dark"] .logo-dark {
  display: block;
}

html[data-theme="dark"] .logo-light {
  display: none;
}

/* Token-driven action buttons — ink primary / brick danger under .app-mono.
   Plain classes, no Tailwind freeze. Identical block also in the other Mono PRs;
   keep one copy on merge. */
.btn-token-primary {
  background: var(--primary);
  color: var(--primary_fg);
  border: 1px solid var(--primary);
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.btn-token-primary:hover {
  background: var(--primary_hover);
}
.btn-token-danger {
  background: var(--danger);
  color: var(--danger_fg);
  border: 1px solid var(--danger);
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.btn-token-danger:hover {
  background: color-mix(in oklab, var(--danger) 88%, #000);
}
/* Token outline secondary — the quiet sibling of btn-token-primary. A visible, one-tap
   button with a lighter visual weight (filled surface + ink border + ink text), used where
   a screen needs a clear hierarchy between a single filled primary and a still-present
   secondary (e.g. the Sell screen's "Paid in cash" beside the Every Pay primary). Mono:
   ink + surface only, no colour. */
.btn-token-secondary {
  background: var(--surface);
  color: var(--text);
  /* Resting boundary clears WCAG 1.4.11 non-text contrast (>=3:1) against the bare
     paper page (the Sell payment grid sits on --bg, not a card) — touch has no :hover,
     so the affordance must hold at rest. A 55% ink mix reads visibly (>=3:1 on white)
     yet stays the quiet sibling of the filled primary; :hover still raises to full ink. */
  border: 1.5px solid color-mix(in oklab, var(--text) 55%, var(--paper));
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.btn-token-secondary:hover {
  background: var(--surface2-hover);
  border-color: var(--text);
}

/* Physicality: the token buttons press with the one Mono spring — same treatment
   ds-button got (#327), reduced-motion-safe (the :active scale only applies under
   no-preference; the base color/border transitions stay unconditional, non-vestibular).
   Covers btn-token-primary / -secondary / -danger. Never combined with ds-button. A few
   sell/index buttons also wear .mono-press: that is a BENIGN redundancy — .mono-press:active
   sets the IDENTICAL transform: scale(var(--mono-press-scale)); the higher-specificity rule
   here wins but with the same value, so the press is unchanged (no dead-rule conflict). The
   redundant .mono-press class on those buttons is now removable (the base class presses). */
@media (prefers-reduced-motion: no-preference) {
  .btn-token-primary:active:not(:disabled),
  .btn-token-secondary:active:not(:disabled),
  .btn-token-danger:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}

/* Wizard AI-describe status (photo→describe flow) — Mono token classes that replace the
   old inline color-mix style= attributes (which were off-brand amber/green/red and tripped
   the design gates). "Ink does everything; brick is the only colour, reserved for losses."
   The designed WAIT itself (labelled spinner + role/aria-live + retry) lives in the partial;
   these classes carry ONLY the colour/brand layer.

   pending → INK / neutral: a process is NOT a warning. Quiet surface card, ink text, muted
   spinner. done → INK: a calm ✓ confirmation, no green. Both share the neutral process card.
   Text colour lives here (not Tailwind arbitrary utilities) so no new utility needs a freeze. */
.wizard-ai-status--process {
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text);
}
.wizard-ai-status--process .wizard-ai-spinner {
  color: var(--muted);
}

/* failed → BRICK: a recoverable loss, framed CALM not hostile. Mirrors the Ui::AlertComponent
   danger idiom — quiet surface card, hairline border, a 2px left brick rule, brick text — the
   one colour moment the palette allows. Retry stays (styled .btn-token-danger in the partial). */
.wizard-ai-status--failed {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-left: 2px solid var(--danger);
}
.wizard-ai-status--failed .wizard-ai-failed-text {
  color: var(--danger);
}
.wizard-ai-status--failed .wizard-ai-error-ref {
  color: var(--muted);
}

/* WhatsApp share button — the one sanctioned off-token colour (WhatsApp's brand-locked
   logo green). A real CSS :hover, NOT inline onmouseover JS (which the nonce-based CSP
   blocks). Keep the brand-green pair in this single place. */
.whatsapp-btn {
  background-color: #25d366;
  color: #fff;
  transition: background-color 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.whatsapp-btn:hover {
  background-color: #128c7e;
}
/* Physicality: the WhatsApp share button presses with the one Mono spring,
   reduced-motion-safe. Standalone (never combined with ds-button). */
@media (prefers-reduced-motion: no-preference) {
  .whatsapp-btn:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}

/* Lucro wordmark — Mono: Fraunces + ink on EVERY in-app page (not only inside
   .app-mono). Base --ui-font-display is Archivo Black and --core-ocean-blue is
   off-brand, so pin Fraunces + ink unconditionally for the partial Mono rollout. */
.lucro-logo-text {
  font-family: "Fraunces", Georgia, "Times New Roman", serif;
  font-style: normal;
  font-weight: 700;
  font-size: 31px;
  line-height: 1;
  height: auto;
  display: block;
  /* "brechó" is a single word: never let a squeezed flex row wrap it char-by-char into a
     vertical e/ch/ó stack (the mobile header where a long account email crowds the row). */
  white-space: nowrap;
  letter-spacing: -0.02em;
  color: var(--ink);
}

/* Keep the wordmark at its natural width in the flex header — the account controls on the
   right shrink/truncate instead of crushing the brand mark below its glyphs. */
.header-branding {
  flex-shrink: 0;
}

/* Autosize down on phones: 31px Fraunces is tuned for desktop; on the narrow in-app header
   it's the last few px that would force an overflow. 24px keeps "brechó" whole and aligned. */
@media (max-width: 640px) {
  .lucro-logo-text {
    font-size: 24px;
  }
}

html[data-theme="dark"] .lucro-logo-text {
  color: var(--core-bright-blue);
}

/* Mono Edition: the wordmark is ink (brand-correct), not ocean-blue.
   The dark-theme selector above is (0,2,1) and would otherwise win on a
   dark-theme user's Mono page, resolving --core-bright-blue to paper (white)
   on a white header — an invisible wordmark. Qualifying the dark case here
   (0,3,1) keeps Mono ink-on-paper regardless of the persisted theme. */
.app-mono .lucro-logo-text,
html[data-theme="dark"] .app-mono .lucro-logo-text {
  color: var(--ink);
}

/* Avatar (Wise-inspired) */
.ui-avatar {
  position: relative;
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--muted);
  overflow: hidden;
}

.ui-avatar--interactive {
  cursor: pointer;
}

.ui-avatar--borderless {
  border: none;
}

.ui-avatar__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform: scale(1.2);
  transform-origin: center;
  border-radius: 9999px;
  display: block;
}

.ui-avatar__text {
  font-size: var(--avatar-text-size, 12px);
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Mono Avatar: hairline ink ring on white, serif (Fraunces) ink initial. */
.app-mono .ui-avatar {
  border-color: var(--ink);
  background: var(--paper);
  color: var(--ink);
}

.app-mono .ui-avatar__text {
  font-family: var(--ui-font-display);
  font-weight: 400;
  letter-spacing: normal;
  color: var(--ink);
}

.ui-avatar__badge {
  position: absolute;
  bottom: -2px;
  right: -2px;
  width: var(--badge-size, 14px);
  height: var(--badge-size, 14px);
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
}

.ui-avatar__badge-text {
  font-size: 9px;
  font-weight: 700;
  color: var(--text);
}

.ui-avatar__badge-icon {
  width: 10px;
  height: 10px;
  color: var(--text);
}

.ui-avatar__badge-image {
  width: 100%;
  height: 100%;
  border-radius: 9999px;
  object-fit: cover;
}

.ui-avatar__dot {
  position: absolute;
  top: -2px;
  right: -2px;
  width: 10px;
  height: 10px;
  border-radius: 9999px;
  background: var(--danger);
  border: 2px solid var(--bg);
}

/* Avatar groups */
.ui-avatar-group {
  display: inline-flex;
  align-items: center;
}

.ui-avatar-group__item + .ui-avatar-group__item {
  margin-left: calc(var(--avatar-overlap, 10px) * -1);
}

.ui-avatar-group--diagonal .ui-avatar-group__item + .ui-avatar-group__item {
  transform: translateY(var(--avatar-offset, 6px));
}

/* <details> accordion polish (used for list-item expanders) */
details > summary {
  list-style: none;
}
details > summary::-webkit-details-marker {
  display: none;
}

/* Global focus ring (keyboard users) */
:focus-visible {
  outline: 2px solid var(--focus_ring);
  outline-offset: 2px;
}

/* Prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }
}

/* OrderItem page: enforce 2-column layout at md+ without relying on Tailwind rebuilds */
.order-item-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}

@media (min-width: 768px) {
  .order-item-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    align-items: start;
  }
}

/* ============================================================
   Landing / marketing pages — Mono Edition ("Ink does everything")
   Warm monochrome: ink on paper, hairline rules over boxes,
   emphasis by weight + scale, Fraunces display + Plex Mono ledger.
   The ONLY color is brick (#993D33), reserved for losses.
   No dark mode on marketing pages: .landing pins its own tokens,
   so every var()-based component inside re-skins automatically.
   ============================================================ */
.landing {
  color-scheme: light;

  /* Mono palette */
  --ink: #0E0E0C;
  --paper: #FFFFFF;
  --mist: #F4F4F2;
  --gray: #6B6B66;
  --line: #E4E4E0;
  --brick: #993D33;

  /* Re-skin the shared token vocabulary inside the marketing scope */
  --bg: var(--paper);
  --surface: var(--mist);
  --surface2: var(--paper);
  --surface2-hover: var(--mist);
  --border: var(--line);
  --text: var(--ink);
  --muted: var(--gray);
  --subtle: var(--gray);
  --primary: var(--ink);
  --accent: var(--primary);
  --primary_hover: #2A2A26;
  --primary_active: #000000;
  --primary_fg: var(--paper);
  --link: var(--ink);
  --focus_ring: var(--ink);
  --focus_ring_offset: var(--paper);
  --button-secondary-text: var(--ink);

  /* Legacy palette aliases: old "ocean/mist" landing classes resolve to ink/mist */
  --core-ocean-blue: var(--ink);
  --core-mist-blue: var(--mist);
  --core-bright-blue: var(--paper);

  /* Type voices */
  --ui-font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --ui-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Hairline aesthetic: data sits on 2px corners, never bubbles */
  --radius-small: 2px;
  --radius-medium: 2px;
  --radius-large: 2px;
  --radius-x-large: 2px;

  background: var(--bg);
  color: var(--text);
}

/* ----- Marketing chrome (shared header/footer) ----- */
.landing-nav {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

.landing-wordmark {
  display: inline-block; /* was inherited from .ink-bleed; owned here now (see .ink-bleed) */
  font-family: var(--ui-font-display);
  font-weight: 700;
  font-size: 31px;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.landing-wordmark--footer {
  font-size: 26px;
}

.landing-nav__link {
  font-size: 14px;
  color: var(--gray);
  transition: color 150ms ease;
}

.landing-nav__link:hover {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 4px;
}

.landing-footer {
  background: var(--paper);
  border-top: 1px solid var(--line);
}

/* ----- Voice helpers ----- */
.landing-kicker {
  font-family: var(--ui-font-mono);
  font-size: 11px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--gray);
}

.landing-kicker--ink {
  color: var(--ink);
}

.landing-mono {
  font-family: var(--ui-font-mono);
}

.landing-money {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.landing-money--quiet {
  font-weight: 400;
  color: var(--gray);
}

.landing-money--loss {
  color: var(--brick);
}

/* ----- Display typography (Fraunces, maison voice) ----- */
.landing-display-xl {
  font-family: var(--ui-font-display);
  font-weight: 340;
  font-size: clamp(2.75rem, 6.5vw, 5.75rem);
  line-height: 1.04;
  letter-spacing: -0.015em;
  color: var(--ink);
}

.landing-display-lg {
  font-family: var(--ui-font-display);
  font-weight: 340;
  font-size: clamp(2rem, 4vw, 3.25rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.landing-display-md {
  font-family: var(--ui-font-display);
  font-weight: 340;
  font-size: clamp(1.625rem, 3vw, 2.5rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.landing-display--light {
  color: var(--paper);
}

/* ----- Buttons: pill is reserved for the primary action ----- */
.landing-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  border-radius: 9999px;
  padding: 10px 22px;
  font-weight: 600;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, transform var(--mono-dur-fast) var(--mono-spring);
}

.landing-button:hover {
  background: var(--primary_hover);
  border-color: var(--primary_hover);
}

/* Ghost = quiet text link (class names are spec-asserted; keep both) */
.landing-button--ghost {
  background: transparent;
  color: var(--ink);
  border-color: transparent;
  padding-left: 12px;
  padding-right: 12px;
}

.landing-button--ghost:hover {
  background: transparent;
  border-color: transparent;
  text-decoration: underline;
  text-underline-offset: 4px;
}

.landing-button-lg {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  border-radius: 9999px;
  padding: 14px 32px;
  font-size: 16px;
  font-weight: 600;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, transform var(--mono-dur-fast) var(--mono-spring);
}

.landing-button-lg:hover {
  background: var(--primary_hover);
  border-color: var(--primary_hover);
  transform: translateY(-1px);
}

/* Physicality: the landing CTAs press with the one Mono spring, reduced-motion-safe.
   On landing-button-lg the :active scale overrides the hover translateY(-1px) on press
   (depresses on touch) — intentional, a press always wins over the lift. Both standalone
   (never combined with ds-button), so no higher-specificity :active conflict. */
@media (prefers-reduced-motion: no-preference) {
  .landing-button:active:not(:disabled),
  .landing-button-lg:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}

/* Inverted = paper pill on the ink CTA band */
.landing-button--inverted {
  background: var(--paper);
  color: var(--ink);
  border-color: var(--paper);
}

.landing-button--inverted:hover {
  background: var(--mist);
  border-color: var(--mist);
}

/* Quiet arrow text-link (secondary CTA: "Ver demo →") */
.landing-textlink {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
  color: var(--ink);
  text-underline-offset: 4px;
}

.landing-textlink:hover {
  text-decoration: underline;
}

.landing-textlink--light {
  color: var(--paper);
}

/* ----- Sections ----- */
.landing-section {
  background: var(--paper);
}

.landing-section-mist {
  background: var(--mist);
}

/* Ink band (final CTA) */
.landing-section-ocean {
  background: var(--ink);
  color: var(--paper);
}

.landing-section-ocean .landing-kicker {
  color: color-mix(in oklab, var(--paper) 70%, transparent);
}

.landing-hairline {
  border: 0;
  border-top: 1px solid var(--line);
}

/* ----- Generic card: hairline over box-shadow ----- */
.landing-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: var(--padding-medium);
}

/* ----- Select (locale switch) ----- */
.landing-select {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 8px 32px 8px 12px;
  color: var(--ink);
  font-family: var(--ui-font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230E0E0C' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 12px;
}

/* ----- Auth form field (Mono): tokenized so the 2px radius + ink focus ring
   match the card/buttons. One class instead of a repeated 9-utility string. */
.landing .landing-input {
  width: 100%;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 10px 12px;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.landing .landing-input::placeholder {
  color: var(--subtle);
}

/* Focus = the same 2px ink edge as .ds-field (1px border + 1px inset ink). Was a blue
   `0 0 0 2px var(--focus_ring)` outer ring — unified to the Mono ink frame app-wide. */
.landing .landing-input:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* ----- OTP / verification code field (auth): ONE input styled as cells -----
   Kept a single input (never six boxes) so autocomplete="one-time-code"
   autofill keeps working; the cell look is letter-spacing, not separate fields.
   Symmetric horizontal padding reserves room for the paste icon without
   pulling the centered code off-center. */
.landing .landing-otp {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 14px 44px;
  color: var(--ink);
  font-family: var(--ui-font-mono);
  font-size: 1.5rem;
  text-align: center;
  letter-spacing: 0.6em;
  text-indent: 0.6em; /* re-center the trailing letter-spacing gap */
}

.landing .landing-otp:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 2px var(--focus_ring);
}

/* ----- Flash toast inside the auth (.landing) scope: ink/brick, 2px corners.
   App pages are unaffected — they fall back to the inline hex in the partial. */
.landing {
  --flash-success-bg: var(--ink);
  --flash-error-bg: var(--brick);
}

/* Mono Toast — ink card, white text, one leading status icon. MUST use literal Mono hex,
   NOT var(--ink)/var(--paper): those resolve only inside .landing/.app-mono, so a
   var()-based rule would render transparent on a not-yet-Mono page (PR#184 bug). */
.flash-toast {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 12px 18px;
  background: #0E0E0C;
  color: #FFFFFF;
  border-radius: 6px;
  box-shadow: 0 24px 60px -12px rgba(14, 14, 12, 0.22);
}

.flash-toast__text {
  color: #FFFFFF;
}

.flash-toast__icon {
  width: 18px;
  height: 18px;
}

/* Error/expired: the icon is the only brick moment; text stays white. */
.flash-toast--error .flash-toast__icon {
  color: #993D33;
}

.flash-toast__close {
  color: #FFFFFF;
}

.flash-toast__close:hover {
  background: rgba(255, 255, 255, 0.10);
}

.landing .flash-toast {
  border-radius: 2px;
  width: 100%;
}

/* ----- Bullets (legacy name, used by guide/faq bodies) ----- */
.landing-bullet {
  width: 14px;
  height: 1.5px;
  border-radius: 0;
  flex-shrink: 0;
  margin-top: 10px;
  background: var(--ink);
}

.landing-bullet--blue {
  background: var(--ink);
}

.landing-bullet--muted {
  background: var(--gray);
}

/* ----- Screenshot frame (guide pages keep using it) ----- */
.landing-screenshot-frame {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  overflow: hidden;
}

.landing-screenshot-frame__header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--mist);
}

.landing-screenshot-frame__dots {
  display: flex;
  gap: 6px;
}

.landing-screenshot-frame__dot {
  width: 8px;
  height: 8px;
  border-radius: 9999px;
  border: 1px solid var(--gray);
  background: transparent;
}

.landing-screenshot-frame__dot--red,
.landing-screenshot-frame__dot--yellow,
.landing-screenshot-frame__dot--green {
  background: transparent;
}

.landing-screenshot-frame__url {
  font-family: var(--ui-font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--gray);
}

.landing-screenshot-frame__content {
  background: var(--paper);
}

.landing-screenshot-frame__content img {
  display: block;
  width: 100%;
  height: auto;
}

/* ----- Lifecycle dots: the point darkens as the piece advances ----- */
.landing-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 9999px;
  flex-shrink: 0;
}

.landing-dot--stock {
  background: transparent;
  border: 1.5px solid var(--ink);
}

.landing-dot--listed {
  background: var(--gray);
}

.landing-dot--sold {
  background: var(--ink);
}

.landing-dot--defective {
  background: var(--brick);
}

/* ----- Hero ledger card (the product, hand-drawn in HTML) ----- */
.landing-ledger {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  overflow: hidden; /* clip the reel's corners to the card radius */
}

/* ----- Hero reel: the live app in motion (rendered in reels/) ----- */
/* The autoplaying, muted, looping reel is the DEFAULT hero visual; it fills the
   ledger card frame. Autoplay is best-effort — iOS Low Power Mode / Data Saver
   refuse it, so the poster frame is designed to carry the hero on its own. */
.landing-reel {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper);
}

/* The static ledger is the fallback: hidden by default (so it never
   double-renders under the reel), shown when motion is reduced. It stays in the
   DOM regardless, so no-JS / SEO always read the server-rendered money figure. */
.landing-ledger__fallback {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .landing-reel { display: none; }
  .landing-ledger__fallback { display: block; }
}

/* ===== Cinematic hero: golden-ratio video panel + copy overlay =====
   The demo film fills a φ-width panel flush to the right + bottom, header→bottom.
   Paper copy is lifted onto its scrimmed left portion (Mono light-on-dark); the
   left ~38.2% stays paper as editorial negative space. Below lg it collapses to a
   safe stacked layout (video banner + ink copy on paper) so the phone read always
   clears the sunlight/WCAG floor. */
:root {
  --marketing-header-h: 75px; /* measured rendered height of .landing-nav */
  --hero-seam-x: 38.2vw;      /* video's left edge = 100% − φ (1/1.618, viewport units) */
  /* The site grid's left inset — matches .ds-grid-container (max-width 1440, centred,
     stepped padding) so the hero copy shares ONE left edge with the logo, the proof
     strip and every section. Fed to the gradient offset too, so the ink→paper seam
     stays pinned to the video edge as the copy shifts onto the grid. */
  --grid-pad: 20px;
  --grid-inset: calc(max((100vw - 1440px) / 2, 0px) + var(--grid-pad));
}
@media (min-width: 480px)  { :root { --grid-pad: 32px; } }
@media (min-width: 768px)  { :root { --grid-pad: 40px; } }
@media (min-width: 992px)  { :root { --grid-pad: 80px; } }
@media (min-width: 1200px) { :root { --grid-pad: 100px; } }

.landing-hero {
  position: relative;
  min-height: calc(100svh - var(--marketing-header-h));
  background: var(--paper);
  overflow: hidden;
}

/* Right golden-ratio panel: flush to right + bottom, full height */
.landing-hero__media {
  position: absolute;
  inset: 0 0 0 var(--hero-seam-x); /* left edge pinned to the seam so the
                                      headline's ink→paper boundary aligns to it */
  margin: 0;
}

.landing-hero__still,
.landing-hero__reel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;          /* crop top & bottom; never sideways */
  object-position: center;
}
/* Only the BOTTOM layer carries the dark base (shown while the poster loads and
   whenever the reel has no frame). The reel stays transparent until it plays, so
   it never occludes the poster. */
.landing-hero__still { z-index: 0; background: var(--ink); }
.landing-hero__reel  { z-index: 1; }

/* Left-weighted ink scrim: dark under the copy, fading toward centre so the
   seller stays visible, with a faint floor to the right edge so line-tails and
   the CTA never fall onto a bright frame. Tuned on the RENDERED page to keep
   paper copy ≥4.5:1 (≥5:1 under the primary CTA); worst case = her bright shirt. */
.landing-hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background:
    linear-gradient(90deg,
      color-mix(in oklab, var(--ink) 90%, transparent) 0%,
      color-mix(in oklab, var(--ink) 82%, transparent) 30%,
      color-mix(in oklab, var(--ink) 70%, transparent) 50%,
      color-mix(in oklab, var(--ink) 22%, transparent) 74%,
      color-mix(in oklab, var(--ink) 12%, transparent) 100%);
}

/* Copy pinned just inside the video's left edge at any viewport width:
   (100% − 61.8%) = the 38.2% paper gutter, then a comfortable reading gutter. */
.landing-hero__copy {
  position: relative;
  z-index: 3;
  padding-left: var(--grid-inset); /* share the site grid's left edge (logo + sections) */
  padding-right: clamp(20px, 4vw, 80px);
  padding-top: clamp(2rem, 10vh, 6.5rem);
  padding-bottom: 2.5rem;
}

/* The headline starts on paper (ink) and bridges the seam onto the video (paper).
   A hard colour stop at the video's left edge — offset by the copy's left padding
   so it pins to the viewport seam — flips each glyph ink→paper as it crosses. The
   scrim keeps the paper half legible; the subhead + CTAs stay left of the seam so
   nothing else ever lands ink-on-dark. */
.landing-hero__copy .landing-display-xl {
  max-width: min(56rem, 66vw);
  font-size: clamp(2.75rem, 7vw, 5.75rem);
  line-height: 1.03;
  letter-spacing: -0.02em;
  /* background-clip:text fills only within the box, so the tight line-height clips
     the last line's descenders (the "y" in "system."). A little bottom padding
     extends the fill box so descenders render fully. */
  padding-bottom: 0.16em;
  background-image: linear-gradient(90deg,
    var(--ink) 0 calc(var(--hero-seam-x) - var(--grid-inset)),
    var(--paper) calc(var(--hero-seam-x) - var(--grid-inset)) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.landing-hero__sub {
  max-width: min(28rem, 32vw); /* stays left of the seam → ink on paper */
  color: var(--muted);
  font-size: 1.0625rem;
  line-height: 1.6;
}

/* Actions stay on paper, left of the seam (never ink-on-dark). */
.landing-hero__actions {
  max-width: min(30rem, 33vw);
  flex-wrap: wrap;
}

/* Forced-colors / Windows High Contrast: the clipped-gradient headline uses a
   transparent fill, which would render invisible. Fall back to the system text
   colour (the DOM text is already intact for screen readers; this is the visual
   path). Mobile is already safe — it sets solid ink. */
@media (forced-colors: active) {
  .landing-hero__copy .landing-display-xl {
    background-image: none;
    -webkit-text-fill-color: CanvasText;
    color: CanvasText;
  }
}

/* Reduced motion / autoplay refused: hide the reel; the poster still carries it. */
@media (prefers-reduced-motion: reduce) {
  .landing-hero__reel { display: none; }
}

/* Below lg: stack (video banner on top, copy below) and flip copy back to
   ink-on-paper so the phone read is never paper-on-bright. */
@media (max-width: 1023px) {
  .landing-hero {
    min-height: 0;
    overflow: visible;
  }
  .landing-hero__media {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 3 / 4;
  }
  .landing-hero__scrim {
    background: linear-gradient(0deg,
      color-mix(in oklab, var(--ink) 22%, transparent) 0%, transparent 46%);
  }
  .landing-hero__copy {
    padding: 2.25rem var(--grid-inset) 0.5rem; /* copy aligns with the grid on mobile too */
  }
  /* Drop the two-tone bridge: solid ink headline on paper, nothing over the video. */
  .landing-hero__copy .landing-display-xl {
    max-width: none;
    font-size: clamp(2.5rem, 9vw, 3.25rem);
    background-image: none;
    -webkit-background-clip: border-box;
    background-clip: border-box;
    -webkit-text-fill-color: var(--ink);
    color: var(--ink);
  }
  .landing-hero__sub { max-width: none; }
  .landing-hero__actions { max-width: none; }
}

.landing-ledger__kpi {
  background: var(--mist);
  border-radius: 2px;
  padding: 16px 18px;
}

/* Figure tile (Analyze money anchor) — bordered white, no fill, mirroring the
   canonical AnalyzeSheet tile. White (not mist) keeps the gray kicker label that
   captions each figure above the ≥5:1 money/primary sunlight floor. */
.landing-figure {
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 14px 16px;
}

.landing-ledger__row {
  border-top: 1px solid var(--line);
}

/* Order-status chip — a single state (the real app shows one status, not a toggle).
   One line, never wraps; ink label on mist ≈ 18:1. */
.landing-status-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  white-space: nowrap;
  background: var(--mist);
  border-radius: 9999px;
  padding: 6px 14px;
  font-family: var(--ui-font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink);
}

/* Hand-written 2-digit piece code — the real product's selling point: a tag you
   write on any label, no barcode/QR printer. Rendered in ink (it's the feature). */
.landing-code {
  display: inline-block;
  min-width: 1.75rem;
  text-align: center;
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 1px 6px;
  font-family: var(--ui-font-mono);
  font-size: 11px;
  color: var(--ink);
}

/* Line-art receipt (Capture step) — the parsed source, drawn in HTML (never a
   raster photo). A dashed edge evokes a torn paper receipt; stays strict Mono. */
.landing-receipt {
  background: var(--paper);
  border: 1px dashed var(--line);
  border-radius: 2px;
  padding: 16px;
}

/* Neutral photo placeholder for a piece thumbnail in the journey mocks. Grayscale
   only — Mono keeps colour for profit/loss, never for product imagery. Mirrors the
   46×56 swatch of the real InventoryScreen list row, scaled for the landing. */
.landing-swatch {
  flex-shrink: 0;
  width: 40px;
  height: 52px;
  background: var(--mist);
  border: 1px solid var(--line);
  border-radius: 2px;
}

/* Lightweight scope bar (Inventory mock) — reinforces which slice of the list you
   are looking at (✅ WWDC26-292 scope bar). Carries its active state by ink weight +
   underline, never a fill: it is a filter, not a tab bar and not a 2-state toggle. */
.landing-scope {
  display: flex;
  gap: 20px;
  border-bottom: 1px solid var(--line);
}

.landing-scope__item {
  font-family: var(--ui-font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--gray);
  padding-bottom: 10px;
  margin-bottom: -1px; /* sit the active underline on the bar's own hairline */
  border-bottom: 1.5px solid transparent;
}

.landing-scope__item--active {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/* ----- Proof strip ----- */
.landing-proof__value {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.landing-proof__label {
  font-family: var(--ui-font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--gray);
  margin-top: 8px;
}

/* ----- How-it-works hairline rows ----- */
.landing-step-num {
  font-family: var(--ui-font-display);
  font-weight: 340;
  font-size: 32px;
  line-height: 1;
  color: var(--gray);
}

/* ----- Static profit chart (illustrative, ink bars) ----- */
.landing-chart {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
}

/* Split track: a tall gains zone above the x-axis and a short loss band below it.
   Profit bars stand on the axis growing up; the loss bar hangs beneath it growing
   down — so a loss reads as a loss, not a short red profit. Columns are explicit so
   each bar lines up with its weekday label in .landing-chart__axis. */
.landing-chart__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-template-rows: 180px 40px;
  column-gap: 4%;
}

/* the x-axis, drawn full-width at the gains/loss boundary */
.landing-chart__grid::after {
  content: "";
  grid-column: 1 / -1;
  grid-row: 1 / 2;
  align-self: end;
  border-bottom: 1.5px solid var(--ink);
  pointer-events: none;
}

.landing-chart__bar {
  grid-row: 1;
  align-self: end;
  background: var(--ink);
  border-radius: 1px 1px 0 0;
}

.landing-chart__bar:nth-child(1) { grid-column: 1; }
.landing-chart__bar:nth-child(2) { grid-column: 2; }
.landing-chart__bar:nth-child(3) { grid-column: 3; }
.landing-chart__bar:nth-child(4) { grid-column: 4; }
.landing-chart__bar:nth-child(5) { grid-column: 5; }
.landing-chart__bar:nth-child(6) { grid-column: 6; }
.landing-chart__bar:nth-child(7) { grid-column: 7; }

.landing-chart__bar--loss {
  grid-row: 2;
  align-self: start;
  background: var(--brick);
  border-radius: 0 0 1px 1px;
}

.landing-chart__axis {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  column-gap: 4%;
  margin-top: 8px;
}

.landing-chart__axis span {
  text-align: center;
}

/* ----- Pricing bento (Mono: the bento IS the plan) -----
   Asymmetric board: ink 2×2 hero (the €0 price), wide fees tile (poster 15%),
   quiet feature tiles, full-width CTA. Spans are declared per breakpoint so the
   8-tile EU and 7-tile BR variants both tile hole-free via source order alone. */
.landing-bento {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

.landing-bento__tile {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 20px;
  display: flex;
  flex-direction: column;
}

.landing-bento__tile--hero {
  padding: 28px;
  justify-content: space-between;
  gap: 40px;
}

.landing-bento__cta {
  grid-column: 1 / -1;
}

@media (min-width: 640px) {
  .landing-bento {
    grid-template-columns: repeat(2, 1fr);
  }

  .landing-bento__tile--hero,
  .landing-bento__tile--wide {
    grid-column: span 2;
  }
}

@media (min-width: 1024px) {
  .landing-bento {
    grid-template-columns: repeat(4, 1fr);
  }

  .landing-bento__tile--hero {
    grid-column: span 2;
    grid-row: span 2;
  }
}

.landing-bento__tile--ink {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
  /* local re-skin: everything var()-based inside the ink tile flips to paper-on-ink */
  --text: var(--paper);
  --muted: color-mix(in oklab, var(--paper) 72%, transparent);
  --subtle: color-mix(in oklab, var(--paper) 60%, transparent);
  --border: color-mix(in oklab, var(--paper) 22%, transparent);
  --line: color-mix(in oklab, var(--paper) 22%, transparent);
  --ink-local: var(--paper);
}

.landing-bento__tile--ink .landing-kicker {
  color: color-mix(in oklab, var(--paper) 70%, transparent);
}

/* .landing-display-* hardcode color: var(--ink) — flip them inside the ink tile */
.landing-bento__tile--ink .landing-display-md,
.landing-bento__tile--ink .landing-display-lg {
  color: var(--paper);
}

/* The €0 poster price inside the hero (color inherits: paper on ink) */
.landing-bento__price {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  font-size: clamp(2.25rem, 4vw, 3.25rem);
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

/* Poster figure on paper tiles (100%, ∞) — one dominant element per tile */
.landing-bento__figure {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  font-size: clamp(2rem, 3.5vw, 3rem);
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ∞ has a small glyph body in Plex Mono — scale it up so the glyph poster
   reads at the same optical size as the numeric figures */
.landing-bento__figure--glyph {
  font-size: clamp(2.75rem, 4.8vw, 4.125rem);
}

/* ----- FAQ: hairline accordion rows ----- */
.landing-accordion {
  border-top: 1px solid var(--line);
}

.landing-accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 0;
  cursor: pointer;
  list-style: none;
  font-weight: 500;
  color: var(--ink);
}

.landing-accordion summary::-webkit-details-marker,
.landing-accordion summary::marker {
  display: none;
}

.landing-accordion__icon {
  font-family: var(--ui-font-mono);
  font-size: 20px;
  font-weight: 400;
  line-height: 1;
  color: var(--gray);
  flex-shrink: 0;
  transition: transform 200ms ease;
}

.landing-accordion[open] .landing-accordion__icon {
  transform: rotate(45deg);
}

.landing-accordion__body {
  padding: 0 0 20px;
  color: var(--gray);
  max-width: 65ch;
}

/* FAQ accordion (legacy helper used on /faq page) */
.faq-accordion summary::-webkit-details-marker,
.faq-accordion summary::marker {
  display: none;
}

/* ----- Focus ring: ink everywhere, paper inside the ink band ----- */
.landing a:focus-visible,
.landing button:focus-visible,
.landing select:focus-visible,
.landing summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 2px;
}

.landing .landing-button:focus-visible,
.landing .landing-button-lg:focus-visible {
  border-radius: 9999px;
}

.landing-section-ocean a:focus-visible,
.landing-section-ocean button:focus-visible,
.landing-bento__tile--ink a:focus-visible,
.landing-bento__tile--ink button:focus-visible {
  outline-color: var(--paper);
}

/* ----- Scroll reveal (JS-gated: nothing hides without html.js-anim) ----- */
html.js-anim .landing [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 500ms cubic-bezier(0.16, 1, 0.3, 1), transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}

html.js-anim .landing [data-reveal].reveal--in {
  opacity: 1;
  transform: none;
}

/* Auth pages fade in place — drop the 16px rise so the form doesn't shift
   under the autofocused field. Opacity still animates. Specificity (0,4,1)
   beats the base reveal rule (0,3,1); .reveal--in is already transform:none. */
html.js-anim .landing.landing-auth [data-reveal] {
  transform: none;
}

/* Pricing bento cascade: the grid container itself never hides (hiding it
   would defeat the stagger — everything would pop as one), only its children
   do, rising 8px in 42ms nth-child steps once the container's single
   IntersectionObserver entry adds .reveal--in. Specificity (0,4,1) beats the
   base reveal rule (0,3,1). */
html.js-anim .landing .landing-bento[data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}

html.js-anim .landing .landing-bento[data-reveal] > * {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--mono-dur-enter) var(--mono-ease-emph), transform var(--mono-dur-enter) var(--mono-ease-emph);
}

html.js-anim .landing .landing-bento[data-reveal].reveal--in > * {
  opacity: 1;
  transform: none;
}

html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(2) { transition-delay: calc(var(--mono-stagger-step) * 1); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(3) { transition-delay: calc(var(--mono-stagger-step) * 2); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(4) { transition-delay: calc(var(--mono-stagger-step) * 3); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(5) { transition-delay: calc(var(--mono-stagger-step) * 4); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(6) { transition-delay: calc(var(--mono-stagger-step) * 5); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(7) { transition-delay: calc(var(--mono-stagger-step) * 6); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(8) { transition-delay: calc(var(--mono-stagger-step) * 7); }
html.js-anim .landing .landing-bento[data-reveal] > *:nth-child(n+9) { transition-delay: calc(var(--mono-stagger-step) * 8); }

/* Failsafe: if the reveal controller never connects (JS module-graph load
   failure), an inline timeout adds .js-anim-failsafe so content hidden by
   html.js-anim can never stay stranded invisible. */
html.js-anim-failsafe .landing [data-reveal] {
  opacity: 1 !important;
  transform: none !important;
}

html.js-anim-failsafe .landing .landing-bento[data-reveal] > * {
  opacity: 1 !important;
  transform: none !important;
}

@media (prefers-reduced-motion: reduce) {
  html.js-anim .landing [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  html.js-anim .landing .landing-bento[data-reveal] > * {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .landing-accordion__icon {
    transition: none;
  }
}

/* Print/PDF: content hidden behind the JS reveal must land on paper — the
   mono-motion contract ("print, PDF, and reduced-motion always land on
   content, never a stuck opacity:0") applies to the scroll reveal too. */
@media print {
  html.js-anim .landing [data-reveal],
  html.js-anim .landing .landing-bento[data-reveal] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* Legacy divider (keep for backward compatibility) */
.landing-divider {
  border-top: 1px solid var(--line);
}

.text-muted {
  color: var(--muted);
}

.text-subtle {
  color: var(--subtle);
}

.text-link {
  color: var(--link);
}

.text-primary {
  color: var(--primary);
}

.text-success {
  color: var(--success);
}

.text-warning {
  color: var(--warning);
}

.text-danger {
  color: var(--danger);
}

/* Design system buttons */
.ds-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--size-8);
  /* Defaults for buttons without a --sm/--md/--lg size modifier.
     Size modifiers below override padding/font-size/min-height. */
  padding: 12px 24px;
  border: 1px solid transparent;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, transform var(--mono-dur-fast) var(--mono-spring);
}

.ds-button:focus-visible {
  outline: 2px solid var(--focus_ring);
  outline-offset: 2px;
}

.ds-button--lg {
  min-height: var(--size-x-large);
  padding: 0 var(--padding-large);
  font-size: 16px;
}

.ds-button--md {
  min-height: var(--size-large);
  padding: 0 var(--padding-medium);
  font-size: 14px;
}

.ds-button--sm {
  min-height: var(--size-medium);
  padding: 0 var(--padding-small);
  font-size: 12px;
}

.ds-button--primary {
  background: var(--primary);
  color: var(--primary_fg);
  border-color: var(--primary);
}

.ds-button--secondary {
  background: color-mix(in oklab, var(--button-secondary-color, var(--secondary-blue)) 35%, var(--surface2));
  color: var(--button-secondary-text, var(--text));
  border-color: color-mix(in oklab, var(--button-secondary-color, var(--secondary-blue)) 55%, var(--border));
}

.ds-button--secondary-neutral {
  background: var(--surface2);
  color: var(--text);
  border-color: var(--border);
}

/* The inventory "to review" filter chip, when its filter is ON. An ink ring (not a solid fill)
   reads as a SELECTED filter while leaving "New batch" the only filled CTA on the view. */
.ds-button--secondary-neutral.is-active {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
  color: var(--ink);
}

/* ── Mono field — the ONE selected/focus frame for every form control (text input / select /
   textarea). DS forms/Input.jsx + Select.jsx: "hairline border thickens to a 2px ink edge on
   focus — no glow, no colour." On focus the field's resting border recolours to ink and a 1px
   inset ink line completes the SAME 2px ink edge as the .ds-button--secondary-neutral.is-active
   "to review" chip above (no layout shift); invalid = brick. This is the FOCUS FRAME ONLY —
   each field keeps its own resting border + radius (squaring legacy rounded-xl/lg fields to the
   Mono 2px is a separate follow-up). Replaces the ring-2 primary / focus_ring / accent /
   default-blue / literal-blue / color-mix-halo one-offs that had accreted across the app.
   `--ink` (not `--focus_ring`) so the frame is ink on EVERY surface, incl. the root/landing
   scope where --focus_ring is blue. */
.ds-field {
  transition: border-color var(--mono-dur-hover, 140ms) var(--mono-ease, ease),
              box-shadow var(--mono-dur-hover, 140ms) var(--mono-ease, ease);
}

.ds-field:focus,
.ds-field:focus-visible,
.ds-field:focus-within {
  outline: none;
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.ds-field[aria-invalid="true"],
.ds-field.is-invalid {
  border-color: var(--brick);
  box-shadow: inset 0 0 0 1px var(--brick);
}

.ds-button--tertiary {
  background: transparent;
  color: var(--text);
  border-color: transparent;
}

.ds-button--negative {
  background: var(--danger);
  color: var(--danger_fg);
  border-color: var(--danger);
}

.ds-button--negative-secondary {
  background: color-mix(in oklab, var(--danger) 12%, var(--surface2));
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 30%, var(--border));
}

.ds-button--negative-tertiary {
  background: transparent;
  color: var(--danger);
  border-color: transparent;
}

.ds-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Physicality: EVERY ds-button presses with the one Mono spring (generalizes the old
   primary-only scale 0.98), reduced-motion-safe. Pairs with the transform spring in the
   base .ds-button transition above. Under reduced-motion the :active scale simply doesn't
   apply, so nothing moves. (Intentional scoping difference vs .mono-press, which gates BOTH
   its transition and :active under no-preference: here the base transition stays unconditional
   so color/background transitions — non-vestibular — survive reduced-motion; only the
   transform :active is gated. Functionally identical press, no vestibular motion when reduced.) */
@media (prefers-reduced-motion: no-preference) {
  .ds-button:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}

.ds-button--primary:disabled {
  background: var(--surface2);
  color: var(--muted);
  border: 1px solid var(--border);
  cursor: not-allowed;
  opacity: 0.7;
}

.ds-button--full-width {
  width: 100%;
}

/* Icon-leading: pin the leading icon to the start edge while the label stays
   optically centered in the button. For full-width (flex-1) CTAs where the base
   justify-center floats a short icon+label pair with lopsided whitespace. The grid
   is [icon | label | phantom]: the trailing column mirrors the icon's width so the
   1fr label column is centered relative to the whole button, not the icon+label
   pair. Icon column width matches the w-5 (1.25rem) icons these CTAs use. */
.ds-button--icon-leading {
  display: grid;
  grid-template-columns: 1.25rem 1fr 1.25rem;
  gap: 0;
  justify-content: normal;
}

.ds-button--icon-leading > :first-child {
  grid-column: 1;
  justify-self: start;
}

.ds-button--icon-leading > span {
  grid-column: 2;
  text-align: center;
}

@media (hover: hover) {
  .ds-button--primary:hover:not(:disabled) {
    background: var(--primary_hover);
    border-color: var(--primary_hover);
  }

  .ds-button--secondary:hover {
    background: color-mix(in oklab, var(--button-secondary-color, var(--secondary-blue)) 45%, var(--surface2));
  }

  .ds-button--secondary-neutral:hover {
    background: var(--surface);
  }

  .ds-button--tertiary:hover {
    background: var(--surface);
  }

  .ds-button--negative:hover {
    background: color-mix(in oklab, var(--danger) 85%, black);
    border-color: color-mix(in oklab, var(--danger) 85%, black);
  }

  .ds-button--negative-secondary:hover {
    background: color-mix(in oklab, var(--danger) 18%, var(--surface2));
  }

  .ds-button--negative-tertiary:hover {
    background: color-mix(in oklab, var(--danger) 12%, transparent);
  }
}

/* ── Toolbar — the screen's sticky top bar (DS ui_kits/erp/Toolbar.jsx → Mono) ──
   Answers "where am I" (the page <h1> title) + "what can I do" (icon+label actions).
   Translucent paper bg + blur + a bottom hairline; ink only, no gradient. Hand-authored
   in the Mono token idiom (NOT a Tailwind utility, so no tailwind:freeze needed). */
.ds-toolbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--size-12);
  padding: 14px 0 12px;
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  border-bottom: 1px solid var(--border);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
}

.ds-toolbar__lead {
  display: flex;
  align-items: baseline;
  gap: var(--size-12);
  min-width: 0; /* allow the title to wrap, never force overflow on the row */
}

/* The page title — display voice (Fraunces under .app-mono), ink, balanced, never truncates.
   Scoped under .ds-toolbar (specificity 0,2,0) so the DS-contract weight 360 beats the
   pre-existing `.app-mono h1 { font-weight: 600 }` (0,1,1) on the analyze screen. */
.ds-toolbar .ds-toolbar__title {
  font-family: var(--ui-font-display);
  font-size: 26px;
  font-weight: 360;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink, var(--text));
  text-wrap: balance;
  margin: 0;
}

/* Optional count — mono ledger voice, uppercase, muted, nowrap. */
.ds-toolbar__count {
  font-family: var(--ui-font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  align-self: center;
}

/* Catalog cart count, shown inside the toolbar "Sell" (ds-button--primary). Inverts the button's
   own ink/paper so it reads as a chip; .mono-tick gives it a non-money overshoot bump on each add. */
.ds-toolbar__action-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 9999px;
  background: var(--primary_fg);
  color: var(--primary);
  font-family: var(--ui-font-mono);
  font-weight: 600;
  font-size: 12px;
  line-height: 1;
}

.ds-toolbar__actions {
  display: flex;
  align-items: center;
  gap: var(--size-8);
  flex-shrink: 0;
}

/* Toolbar action buttons: icon + label, ≥44px target. */
.ds-toolbar__actions .ds-button {
  min-height: 44px;
  gap: 6px;
  text-decoration: none;
}

/* stack_on_mobile: below sm, the title sits ABOVE the actions (opt-in via Ui::ToolbarComponent)
   so a screen with two actions doesn't crowd the <h1> on a phone. Other toolbars are unaffected. */
@media (max-width: 639px) {
  .ds-toolbar--stack {
    flex-direction: column;
    align-items: stretch;
  }

  /* Three labeled 44px actions (New batch · New pop-up · "to review") overflow a ~360px phone,
     so let the row wrap; the trailing chip drops to a second line as a unit, full label + target
     intact. */
  .ds-toolbar--stack .ds-toolbar__actions {
    justify-content: flex-start;
    flex-wrap: wrap;
  }
}

/* ===== Search field (Ui::SearchFieldComponent) =====
   The one DS-aligned search pill — consolidates the three bespoke `.search-pill` blocks
   (inventory, catalog, marketplace browse). Mono tokens only; pill-shaped; INK hairline
   border (focus → stronger ink border + soft ink ring). Leading magnifying glass, scope
   placeholder, trailing clear (×). ≥44px targets. */
.ds-search-form {
  width: 100%;
}

.ds-search {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding-left: var(--size-16);
  gap: var(--size-8);
  background: var(--surface);
  border: 1px solid color-mix(in oklab, var(--text) 30%, transparent);
  border-radius: 9999px;
  font-family: var(--ui-font-sans);
  transition: border-color 0.15s ease;
}

/* Leading magnifying glass — always reads as "search", muted ink. */
.ds-search__lead {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--muted);
  pointer-events: none;
}

/* The input — transparent so the pill is the surface; ink text, muted placeholder. */
.ds-search .ds-search__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 42px;
  background: transparent;
  border: none;
  /* 16px avoids iOS Safari focus auto-zoom; the 42px pill accommodates it. */
  font-size: 16px;
  color: var(--text);
  outline: none !important;
  box-shadow: none !important;
}

.ds-search .ds-search__input::placeholder {
  color: var(--muted);
}

/* Token (pills) mode — the catalog search gathers several codes/searches at once. Pills + the live
   input wrap inside the pill; the input keeps growing to fill the row. */
.ds-search__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--size-8);
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 0;
}

.ds-search__pills .ds-search__input {
  flex: 1 1 80px;
  min-width: 80px;
  height: auto;
}

.ds-search__pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  padding: 3px 6px 3px 10px;
  border-radius: 9999px;
}

.ds-search__pill button {
  line-height: 1;
  font-size: 14px;
}

/* Suppress the native type=search clear (×) so it doesn't double the custom .ds-search__clear. */
.ds-search .ds-search__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

/* Clear (×) — ≥44px hit target, muted → ink on hover. */
.ds-search__clear {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  color: var(--muted);
  transition: color 0.15s ease;
}

.ds-search__clear:hover {
  color: var(--text);
}

/* Optional visible submit (marketplace keeps its submit button).
   No margin: a 44px submit inside a 44px pill keeps the pill at the 44px
   baseline (matching the inventory/catalog pills); inner padding carries the gap. */
.ds-search__submit {
  flex-shrink: 0;
  padding: var(--size-8) var(--size-16);
  min-height: 44px;
  border: none;
  border-radius: 9999px;
  background: var(--primary);
  color: var(--primary_fg, var(--bg));
  font-family: var(--ui-font-sans);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* Round icon-only submit (submit_variant: :round) — the Airbnb CTA: an ink circle carrying
   the magnifier. Overrides the labeled base above; 44px keeps the tap-target floor. */
.ds-search__submit--round {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-height: 0;
  padding: 0;
  border: none;
  border-radius: 9999px;
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
  flex-shrink: 0;
}
@media (hover: hover) {
  .ds-search__submit--round:hover {
    background: color-mix(in oklab, var(--ink) 86%, var(--paper));
  }
}
/* The landing focus-visible reset squares corners (border-radius: 2px); keep the circle
   round while focused — same exception the landing-button carries. */
.landing .ds-search__submit--round:focus-visible,
.landing .mkt-header__search-link:focus-visible {
  border-radius: 9999px;
}

/* Round-CTA pill — the circle never sits flush against the pill border: compact gets a
   4px inset (52 = 4 + 44 + 4), matching the hero anatomy at smaller scale. */
.ds-search--round {
  min-height: 52px;
  padding-block: 4px;
  padding-right: 4px;
}

/* Hero size (size: :hero) — the taller landing proportion; the round CTA sits inset 6px
   like Airbnb's (56 = 6 + 44 + 6). Declared after --round so it wins when both apply. */
.ds-search--hero {
  min-height: 56px;
  padding-block: 6px;
  padding-right: 6px;
}

/* Hero width constraint + centering — the wrapper concern, never the component's. */
.mkt-hero-search {
  max-width: 40rem;
  margin-inline: auto;
}

/* Focus = the same 2px ink edge as .ds-field (1px border + 1px inset ink), NOT a soft halo —
   so the search pill's selected frame matches every other field. Reduced-motion safe (only the
   transition is gated, below). */
.ds-search:focus-within {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

@media (prefers-reduced-motion: no-preference) {
  .ds-search {
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
  }
}

/* Design system cards — Mono: hairline border (replaces Wise bg-only). */
.ds-card {
  background: var(--surface2);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: var(--size-12);
  position: relative;
}

/* Mono "white hairline surface": flat, hairline border, no elevation. */
.ds-surface-card {
  background: var(--surface2);
  border: 1px solid var(--line);
  color: var(--text);
}

.ds-card--small {
  min-height: 88px;
  max-height: 300px;
  padding: var(--padding-small);
  border-radius: var(--radius-medium);
}

.ds-card--large {
  min-height: 300px;
  padding: var(--padding-large);
  border-radius: var(--radius-x-large);
}

.ds-card--soft {
  background: var(--surface2);
}

.ds-card--clickable {
  cursor: pointer;
  transition: background-color 150ms ease, box-shadow 150ms ease, transform var(--mono-dur-fast) var(--mono-spring);
}
/* Physicality: a clickable card presses with the one Mono spring (transform only — like
   .mono-press for cards, no tint), reduced-motion-safe. The press scale must beat the
   .ds-card--clickable:hover translateY(-3px) (both touch transform, equal weight), and that
   :hover rule is LATER in source — so the bare :active would lose on a hovering device
   (desktop mouse-down = :active AND :hover both true). Chaining the always-present base
   `.ds-card` raises the press to specificity (0,3,0) > hover (0,2,0), so it wins regardless
   of source order — same specificity-bump pattern as .landing-button-lg:active:not(:disabled).
   On touch (@media hover:none) the hover rule never applies anyway. */
@media (prefers-reduced-motion: no-preference) {
  .ds-card.ds-card--clickable:active {
    transform: scale(var(--mono-press-scale));
  }
}

.ds-card-grid {
  display: grid;
  gap: var(--size-16);
}

.ds-card-stack {
  display: grid;
  gap: var(--size-16);
}

.ds-card__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-12);
}

.ds-card__stack {
  display: flex;
  flex-direction: column;
  gap: var(--size-4);
}

.ds-card__eyebrow {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--subtle);
  font-weight: 600;
}

.ds-card__label {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.ds-card__title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}

.ds-card__value {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.ds-card__value--lg {
  font-size: 28px;
}

.ds-card__value--xl {
  font-size: 32px;
}

.ds-card__value--sm {
  font-size: 20px;
}

/* StatTile: the figure is always Plex Mono (ledger voice), medium, no tracking. */
.ds-stat-tile__value {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  letter-spacing: normal;
}

/* StatTile label: Plex Mono uppercase eyebrow, 11px, 0.1em tracking, muted. */
.ds-stat-tile__label {
  font-family: var(--ui-font-mono);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Value color modifiers for financial data (accessible contrast maintained) */
.ds-card__value--success {
  color: var(--success);
}

.ds-card__value--danger {
  color: var(--danger);
}

.ds-card__meta {
  font-size: 12px;
  color: var(--muted);
}

.ds-card__divider {
  height: 1px;
  background: var(--border);
}

.ds-card__pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-8);
}

.ds-card__pill {
  padding: var(--size-4) var(--size-12);
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  border: 1px solid transparent;
}

.ds-card__pill--active {
  background: color-mix(in oklab, var(--primary) 14%, var(--surface));
  border-color: color-mix(in oklab, var(--primary) 30%, transparent);
  color: var(--primary);
}

.ds-card__tag {
  padding: var(--size-4) var(--size-12);
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  color: var(--success);
  background: color-mix(in oklab, var(--success) 18%, var(--surface));
}

.ds-card__sparkline {
  width: 100%;
  height: 64px;
}

.ds-card__sparkline path {
  stroke: var(--sparkline-color, var(--success));
  stroke-width: 3;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ds-card__sparkline circle {
  fill: var(--sparkline-color, var(--success));
}

.ds-card__bar {
  height: 12px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--bar-color, var(--primary)) 18%, var(--surface));
  overflow: hidden;
}

.ds-card__bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--bar-color, var(--primary));
}

.ds-card__link {
  color: var(--link);
  font-weight: 600;
  font-size: 13px;
  text-decoration: none;
}

.ds-card__link:hover {
  text-decoration: underline;
}

.ds-card__icon-circle {
  width: var(--size-medium);
  height: var(--size-medium);
  border-radius: 999px;
  background: color-mix(in oklab, var(--icon-color, var(--primary)) 18%, var(--surface));
  color: var(--icon-color, var(--primary));
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.ds-card__footer {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-12);
}

.ds-card__dismiss {
  position: absolute;
  top: var(--size-8);
  right: var(--size-8);
  width: var(--size-medium);
  height: var(--size-medium);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 150ms ease, background-color 150ms ease;
}

.ds-card__dismiss:hover {
  color: var(--text);
  background: color-mix(in oklab, var(--surface) 85%, var(--primary) 15%);
}

.ds-card__media-block {
  height: 120px;
  border-radius: var(--radius-medium);
  background: color-mix(in oklab, var(--primary) 12%, var(--surface));
}

.ds-card__media-block--overlap {
  margin-bottom: calc(var(--size-16) * -1);
}

@media (min-width: 768px) {
  .ds-card-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
  }
}

@media (hover: hover) {
  .ds-card--clickable:hover {
    background: var(--surface2-hover);
    box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.12);
    transform: translateY(-3px);
  }
}

html[data-theme="dark"] .ds-card--clickable:hover {
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5), 0 4px 10px rgba(0, 0, 0, 0.35);
}

/* Design system form inputs */
.ds-input-label {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: 0.0125em;
}

/* Mono: square 2px corners (the brand resists roundness); 48px control height. */
.ds-input-container {
  background: var(--surface2);
  border: 1px solid color-mix(in oklab, var(--text) 30%, transparent);
  border-radius: 2px;
  padding: 8px 12px;
  min-height: 48px;
  display: flex;
  align-items: center;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.ds-input-container:hover {
  border-color: color-mix(in oklab, var(--text) 45%, transparent);
}

/* Mono focus = solid 2px ink edge (1px border + 1px inset), NO coloured ring. Ink (not
   --focus_ring) so it matches .ds-field on every surface, incl. root/landing where focus_ring is blue. */
.ds-input-container:focus-within {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.ds-input {
  width: 100%;
  background: transparent;
  color: var(--text);
  border: none;
  outline: none;
}

.ds-input::placeholder {
  color: var(--subtle);
}

/* Design system upload input (Wise-inspired) */
.ds-upload {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
  background: var(--surface2);
  border: 1px solid color-mix(in oklab, var(--text) 30%, transparent);
  border-radius: 12px;
  padding: 16px 20px;
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.ds-upload:hover {
  background: var(--surface2-hover);
  border-color: color-mix(in oklab, var(--text) 45%, transparent);
}

.ds-upload:focus-within {
  border-color: color-mix(in oklab, var(--text) 60%, transparent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary) 20%, transparent);
}

.ds-upload__icon {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  color: var(--primary);
}

.ds-upload__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
}

.ds-upload__title {
  font-size: 16px;
  font-weight: 600;
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.ds-upload__description {
  font-size: 14px;
  color: var(--muted);
  margin-top: 2px;
}

.ds-upload input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Mobile bottom nav - solid background, permanently pinned to the bottom */
.mobile-bottom-nav {
  background-color: var(--bg);
  border-top: 1px solid var(--border);
}

/* Sales-tab "to ship" count badge. Mono law: this is WORK TO DO (not a loss), so it is
   INK — solid --text fill with the page --bg as the number, never brick. Anchored to the
   top-right of the nav icon; the count drives its presence (rendered only when > 0). */
.nav-to-ship-badge {
  position: absolute;
  top: -6px;
  right: -8px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  background-color: var(--text);
  color: var(--bg);
  font-family: var(--ui-font-mono);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
}

/* Live-tick badge target — a transparent wrapper around the absolute .nav-to-ship-badge pill that
   stays in the DOM at count 0 so the broadcast (update_all .js-to-ship-badge-target) always has a
   target. display:contents removes it from layout, so the pill still positions against the .relative
   icon span and the wrapper never affects spacing. aria-live (on the element) announces a count change.
   a11y caveat (accepted): pre-Chrome-89 / old mobile Firefox dropped display:contents nodes from the
   accessibility tree, which would suppress this announcement; on the modern-phone target audience it is
   reliable, so we accept it. If the audience ever broadens, move aria-live to the .nav-to-ship-badge pill. */
.js-to-ship-badge-target { display: contents; }

/* App chrome: desktop left sidebar (Wise-like) */
.desktop-sidebar {
  display: none;
}

@media (min-width: 1024px) {
  :root {
    /* Future: can be reduced to show icon-only collapsed rail */
    --sidebar-width: 18rem;
  }

  /* When the desktop sidebar is present, make the global header span edge-to-edge. */
  body:has(.desktop-sidebar) .global-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 50;
    background: var(--bg);
    border-bottom: none;
  }

  .desktop-sidebar {
    display: block;
    position: fixed;
    left: 0;
    top: var(--app-header-height);
    bottom: 0;
    width: var(--sidebar-width);
    z-index: 40;
    background: var(--bg);
    border-right: none;
    overflow-y: auto;
  }

  /* Desktop sidebar item states */
  .desktop-sidebar__link {
    color: var(--muted);
  }

  .desktop-sidebar__link:hover {
    background: transparent;
    color: var(--primary);
  }

  /* data-active comes from the ERB partial */
  .desktop-sidebar__link[data-active="true"] {
    background-color: color-mix(in oklab, var(--primary) 10%, transparent);
    color: var(--text);
  }

  /* Remove "outlined icon" look */
  .desktop-sidebar__icon {
    border: none;
    box-shadow: none;
    outline: none;
  }

  /* Only offset pages that actually render the sidebar */
  body:has(.desktop-sidebar) {
    padding-left: var(--sidebar-width);
    padding-top: var(--app-header-height);
  }

  /* Ensure mobile bottom nav never appears on desktop, even if Tailwind breakpoints don't rebuild */
  .mobile-bottom-nav {
    display: none !important;
  }

  /* Sidebar height is now below the fixed header. */
  body:has(.desktop-sidebar) .desktop-sidebar {
    height: calc(100vh - var(--app-header-height));
  }
}

/* ============================================================
   Authenticated app — Mono Edition ("Ink does everything")
   Opt-in per page via <body class="app-mono"> (content_for :body_class).
   Mirrors the .landing token re-pin so every var()-based component
   re-skins automatically; light-only (no dark mode in-app).
   Strict ink + brick: positive/neutral status reads by icon + weight;
   brick (#993D33) is the ONLY color, reserved for error/loss/defective.
   Plain CSS, served as-is by Propshaft — no Tailwind freeze needed.
   Revert a page = delete its `content_for :body_class` line.
   ============================================================ */
.app-mono {
  color-scheme: light;

  /* ── Activation motion + figure tokens (mono_activation_tokens handoff) ──
     Count-up speeds, named springs, tier-0 micro durations, and the money-figure
     scale that the peak / END / StatTile components anchor to. The .mono-* motion
     vocabulary itself lives in the "Mono Motion" section appended to this file. */
  --dur-count: 1000ms;       /* dashboard money tiles */
  --dur-count-peak: 1400ms;  /* first-sale / payout peak figure */
  --dur-ring: 320ms;         /* success ring stamp (SuccessModal / coda mark) */
  --dur-coda: 360ms;         /* END coda screen settling at the exhale */
  --dur-settle: 240ms;       /* a figure's settle as the count-up lands */
  --dur-tap: 90ms;
  --dur-hover: 140ms;
  --dur-flash: 200ms;
  --figure-hero: 52px;       /* the HERO money figure — the anchor */
  --figure-lg: 30px;
  --figure-md: 20px;
  --figure-sm: 15px;

  /* Mono palette */
  --ink: #0E0E0C;
  --paper: #FFFFFF;
  --mist: #F4F4F2;
  --gray: #6B6B66;
  --line: #E4E4E0;
  --brick: #993D33;
  --stone: #999998; /* faintest lifecycle state (reserved dot ring) */

  /* Re-skin the shared token vocabulary */
  --bg: var(--paper);
  --surface: var(--mist);
  --surface2: var(--paper);
  --surface2-hover: var(--mist);
  --border: var(--line);
  --text: var(--ink);
  --muted: #4A4A45; /* Figma DS1 spec: 9:1 on white, distinct from --subtle's #6B6B66 */
  --subtle: var(--gray);
  --primary: var(--ink);
  --accent: var(--primary);
  --primary_hover: #2A2A26;
  --primary_active: #000000;
  --primary_fg: var(--paper);
  --link: var(--ink);
  --focus_ring: var(--ink);
  --focus_ring_offset: var(--paper);
  --button-secondary-text: var(--ink);
  /* Re-pin the legacy teal --secondary-blue to ink: .ds-button--secondary resolves its fill +
     border through var(--button-secondary-color, var(--secondary-blue)), so without this every
     secondary CTA under Mono (first-sale "Share shop", journey-coda, capture/order_items wizard)
     would render the off-brand :root teal. Mono = ink only; brick is the single allowed color. */
  --secondary-blue: var(--ink);

  /* Strict ink + brick: state tokens collapse to ink; only loss = brick.
     This converts every var(--success/--warning/--danger)-based component
     (badges, alerts, the profit chart) to Mono without touching markup. */
  --success: var(--ink);
  --success_fg: var(--paper);
  --warning: var(--ink);
  --warning_fg: var(--paper);
  --danger: var(--brick);
  --danger_fg: var(--paper);
  --error: var(--danger);
  /* Lifecycle ramp — ink DARKENS as the item progresses (Figma StatusChip 24:104):
     in-stock faint → reserved mid → listed gray → sold full ink → defective brick. */
  --status-in-stock: color-mix(in oklab, var(--ink) 16%, var(--paper));
  --status-reserved: color-mix(in oklab, var(--ink) 38%, var(--paper));
  --status-listed: var(--gray);
  --status-sold: var(--ink);
  --status-defective: var(--brick);

  /* Inventory status-bar accents → ink tints; defective → brick */
  --bright-orange: color-mix(in oklab, var(--ink) 14%, var(--paper));
  --bright-yellow: color-mix(in oklab, var(--ink) 22%, var(--paper));
  --bright-blue: color-mix(in oklab, var(--ink) 30%, var(--paper));
  --bright-pink: color-mix(in oklab, var(--brick) 16%, var(--paper));

  /* Flash toast badges — Strict ink+brick (success=ink, error=brick) */
  --flash-success-bg: var(--ink);
  --flash-error-bg: var(--brick);

  /* Legacy ocean/mist aliases resolve to ink/mist */
  --core-ocean-blue: var(--ink);
  --core-mist-blue: var(--mist);
  --core-bright-blue: var(--paper);

  /* Type voices: Fraunces display, IBM Plex Mono ledger */
  --ui-font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --ui-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Hairline aesthetic: flat 2px corners on data containers */
  --radius-small: 2px;
  --radius-medium: 2px;
  --radius-large: 2px;
  --radius-x-large: 2px;
  --radius-2x-large: 2px;

  background: var(--bg);
  color: var(--text);
}

/* ── Mono StatusChip — lifecycle dot + ink label (Figma "StatusChip" 24:104) ──
   Replaces the pastel .status-badge--* fills with a white pill + 8px status dot.
   Dot ramp: in-stock hollow ink ring → listed gray → sold full ink → defective
   brick. Pure CSS, scoped to .app-mono; no markup/JS change. App-wide (any page
   that wears .app-mono: order_items today, sell once it ramps). */
.app-mono .status-badge {
  display: inline-flex !important;
  align-items: center;
  gap: 8px;
  background-color: var(--paper) !important;
  border: 1px solid var(--ink) !important;
  color: var(--ink) !important;
  border-radius: 999px !important;
  height: 28px;
  padding: 0 12px 0 11px !important;
  font-family: var(--ui-font-mono);
  font-weight: 500; /* Plex Mono medium applies to ALL chips */
  font-size: 12px;
  letter-spacing: 0.04em;
  line-height: 1;
}
.app-mono .status-badge::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: var(--ink);
}
.app-mono .status-badge--in_stock::before {
  background-color: var(--paper);
  box-shadow: inset 0 0 0 1.5px var(--ink); /* hollow ink ring — the faintest lifecycle state stays perceivable */
}
.app-mono .status-badge--reserved::before {
  background-color: var(--paper);
  box-shadow: inset 0 0 0 1.5px var(--stone); /* stone ring — reserved is distinct from in_stock's ink ring */
}
.app-mono .status-badge--listed::before { background-color: var(--gray); }
.app-mono .status-badge--sold::before { background-color: var(--ink); }
.app-mono .status-badge--defective::before { background-color: var(--brick); }

/* Status SELECTOR pills (radio group): selected = ink (brick for defective);
   unchecked keep their mist + ink markup defaults. */
.app-mono .peer:checked + .status-pill {
  background-color: var(--ink) !important;
  border-color: var(--ink) !important;
  color: var(--paper) !important;
}
.app-mono .peer:checked + .status-pill--defective {
  background-color: var(--brick) !important;
  border-color: var(--brick) !important;
}

/* Category / tag chips (shared pill-input) → neutral Mono chip; the JS validation
   states (bg-red-800 / bg-emerald-800) → brick / ink. */
.app-mono .category-tag-chip {
  background-color: var(--mist) !important;
  border: 1px solid var(--line) !important;
  color: var(--ink) !important;
}
.app-mono .category-tag-chip button {
  color: var(--gray) !important;
}
.app-mono .category-tag-chip button:hover {
  color: var(--ink) !important;
}
/* Search keyword tags sit on a field whose background IS --mist (== the pill fill), so the default
   --line hairline disappears. Give search pills the field's own ink hairline so each tag reads as a
   distinct chip. Scoped to .ds-search__pills → covers BOTH server-rendered (.ds-search__pill) and
   JS-created (.category-tag-chip) pills; catalog category pills + the Sell code field are untouched. */
.app-mono .ds-search__pills .category-tag-chip {
  border-color: color-mix(in oklab, var(--ink) 24%, transparent) !important;
}
.app-mono [data-pill-input-target="pill"].bg-red-800 {
  background-color: var(--brick) !important;
  border-color: var(--brick) !important;
  color: var(--paper) !important;
}
.app-mono [data-pill-input-target="pill"].bg-emerald-800 {
  background-color: var(--ink) !important;
  border-color: var(--ink) !important;
  color: var(--paper) !important;
}

/* Global #toast → Mono. The toast was shipping literal Tailwind emerald/red: two off-palette hues on
   the app's most-seen chrome (clipboard, share, pill_input, flash_toast, the wizard), right next to
   .sale-toast, which is a brand asset. This is exactly the :root note above — "States — retired to
   Mono (ink + brick only): success/warning = ink, danger = brick".
   These are Tailwind UTILITIES, so neither gate could catch them: off-brand-hex looks for hex
   literals, undefined-token for var(--x). Hence the !important remap.
   Contrast improves: white on brick 6.8:1 vs emerald-700 5.5:1; both clear the 5:1 sunlight bar.

   NOT scoped to .app-mono — unlike the pill states above, which live only inside the rebranded app.
   `.app-mono` is opted into per-page via `content_for :body_class`, so /sellers, /guide and /faq do
   NOT carry it, while #toast is on EVERY page of this layout. Scoping it left the toast emerald on
   most of the app — verified in the browser, invisible to a spec that only asserts the rule exists.
   Safe unscoped: --ink/--brick/--paper are :root tokens, aliased there to resolve in every scope. */
#toast.bg-emerald-700 {
  background-color: var(--ink) !important;
  color: var(--paper) !important;
}
#toast.bg-red-700 {
  background-color: var(--brick) !important;
  color: var(--paper) !important;
}

/* Dark theme override for the success toast — scoped to the pages that ACTUALLY go dark.
   On a genuinely dark page, `--ink` (a :root token the dark block does not re-define; it re-skins
   --bg/--surface/--surface2/--text only) would sit at #0e0e0c on --bg #121511 = ~1.05:1: a toast you
   cannot see. Lift it the way .sale-toast already does under dark (rgba(40,44,40,.85) ≈ --surface2
   #292d29); the toast's own text-white then reads ~14:1.

   `body:not(.app-mono)` is LOAD-BEARING, and its absence INVERTED this fix. `.app-mono` pins
   `color-scheme: light` and re-declares `--surface2: var(--paper)` = #FFFFFF (ui.css:3220), and a
   custom property resolves from the NEAREST DECLARING ANCESTOR — so on those pages the body's white
   beats anything `html[data-theme="dark"]` declares. Specificity is irrelevant; the rules match
   different elements. Unscoped, this painted a WHITE toast carrying white text (1.00:1) across the
   entire seller app — every page the toast actually serves (clipboard, share, pill_input,
   flash_toast, wizard) — while "fixing" the three that weren't broken. It is also unnecessary on
   .app-mono: that scope stays light, so var(--ink) is already right there (19.32:1).

   `:not(.landing)` was here too and is NOT load-bearing — removed. But the REASON is narrower than
   it looks, and two earlier drafts of this comment stated it wrongly:
     - NOT "because .landing is never a body class". It IS one — `layouts/marketplace.html.erb:22`
       ships `<body class="landing …">`.
     - NOT "because .landing is only a wrapper <div> inside yield". True on /guide and /demo, where it
       is a sibling subtree and cannot be an ancestor — but incidental.
   The actual reason: **#toast exists only in `layouts/application.html.erb`**, whose body class comes
   solely from `content_for :body_class` — which never yields `landing`. The layout that DOES put
   .landing on <body> renders no toast at all. So the exclusion cannot fire. Today.
   Put a toast in the marketplace layout and `.landing` re-declares `--surface2`, and this goes
   white-on-white again (measured: a toast injected onto the real server-side body.landing = 1.00:1).
   What catches that is toast_theme_readability_spec.rb, which joins on TOAST-BEARING layouts and
   reads BOTH body-class sources (hardcoded and content_for). An earlier version of this comment
   claimed the guard derived scopes from `content_for` alone — it did, which made it blind to the
   hardcoded case: a control asserted to have an effect that had none, written into the comment
   explaining the fix for the previous instance of exactly that. The guard is fixed; this now matches.

   `!important` IS load-bearing: without it the unscoped base rule above wins and var(--ink) returns.
   Brick needs no override in any scope: #993D33 stands off both pages, and white on brick is 6.8:1. */
html[data-theme="dark"] body:not(.app-mono) #toast.bg-emerald-700 {
  background-color: var(--surface2) !important;
}

/* Page titles wear the Fraunces display voice in-app */
.app-mono h1 {
  font-family: var(--ui-font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Faithful Mono: flatten rounded data containers and design-system buttons to 2px.
   Excludes rounded-full (avatars, pills) and small radii (rounded/sm/md). */
.app-mono .rounded-lg,
.app-mono .rounded-xl,
.app-mono .rounded-2xl,
.app-mono .rounded-3xl,
.app-mono .ds-button { border-radius: 2px; }

/* Square (Mono 2px) corners on form controls APP-WIDE, even outside .app-mono
   (marketplace, ship, pos render on layouts that never get the .app-mono body
   class). Tagging the rounded-* utility with its element (input/select/textarea)
   raises specificity above the bare utility, so this overrides inline
   rounded-md/lg/xl with NO per-view edits and NO Tailwind freeze. Scoped to form
   controls only — cards, avatars, and rounded-full pills are untouched. */
input.rounded-sm, input.rounded, input.rounded-md, input.rounded-lg, input.rounded-xl, input.rounded-2xl,
select.rounded-sm, select.rounded, select.rounded-md, select.rounded-lg, select.rounded-xl, select.rounded-2xl,
textarea.rounded-sm, textarea.rounded, textarea.rounded-md, textarea.rounded-lg, textarea.rounded-xl, textarea.rounded-2xl {
  border-radius: 2px;
}

/* Mono form controls: replace the native select arrow with a custom ink chevron
   (mirrors .landing-select), and pull the native date calendar icon to the SAME
   12px inset + vertical center so the two controls line up. padding-right: 38px
   moves the arrow off the corner and keeps text clear of the icon. */
.app-mono select {
  -webkit-appearance: none;
  appearance: none;
  /* Spec chevron: 12px at stroke-width 1.4 (lighter than the old 14px / 2). */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230E0E0C' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
  padding-right: 38px;
}

.app-mono input[type="date"] {
  padding-right: 38px;
  position: relative;
}

/* WebKit/Blink only — Firefox draws its own non-stylable date control. */
.app-mono input[type="date"]::-webkit-calendar-picker-indicator {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  margin: 0;
  padding: 0;
  cursor: pointer;
}

/* Money / ledger numerals (add .app-money where figures appear) */
.app-mono .app-money {
  font-family: var(--ui-font-mono);
  font-variant-numeric: tabular-nums;
}

/* Capture processing status tiles — base (non-Mono) look.
   `_processing_card` is a SHARED partial: it also renders on the inventory
   index (#email-processing-container) and via Turbo broadcasts, which are NOT
   .app-mono. These un-scoped rules restore the original rounded blue/amber/red
   the markup used to carry inline; .app-mono overrides them to ink/brick below. */
.capture-status__icon {
  border-radius: 0.75rem; /* was Tailwind rounded-xl on the tile */
  background: rgb(59 130 246 / 0.2); /* blue-500/20 — processing/pending */
  color: rgb(96 165 250); /* blue-400 */
}
.capture-status__icon--parsed {
  background: rgb(245 158 11 / 0.2); /* amber-500/20 */
  color: rgb(251 191 36); /* amber-400 */
}
.capture-status__icon--alert {
  background: rgb(239 68 68 / 0.2); /* red-500/20 */
  color: rgb(248 113 113); /* red-400 */
}
.capture-status__msg--alert { color: rgb(248 113 113); } /* red-400 */

/* Capture processing status tiles — Strict ink + brick under Mono.
   Higher specificity (.app-mono …) overrides the base rules above so the
   piloted /capture page reads as strict ink + brick with zero markup edits. */
.app-mono .capture-status__icon {
  border-radius: 2px;
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: var(--ink);
}
.app-mono .capture-status__icon--alert {
  background: color-mix(in oklab, var(--brick) 12%, transparent);
  color: var(--brick);
}
.app-mono .capture-status__msg--alert { color: var(--brick); }

/* Retire dark mode in-app: hide the theme toggle on Mono pages
   (full removal of the control + Stimulus controller deferred to the
   global-promote PR). The token block already pins light regardless. */
.app-mono .account-menu__theme-row,
.app-mono [data-theme-target="toggle"] { display: none !important; }

/* Modal panels must follow the light Mono surface regardless of the persisted
   data-theme. `.arrival-modal__panel` hardcodes a dark #292d29 !important and
   only flips to white under html[data-theme="light"]; but the in-app theme
   toggle is hidden above, so a user with a stored dark theme would keep a dark
   panel while Mono pins text to ink → dark-on-dark title/hint. Pin it to paper. */
.app-mono .arrival-modal__panel {
  background-color: var(--paper) !important;
  background-image: none;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

/* ============================================================
   Marketplace — buyer storefront chrome (Mono via .landing scope)
   ============================================================ */
.mkt-header {
  background: var(--paper);
  border-bottom: 1px solid var(--border);
}

/* Header center (Airbnb anatomy) — a grid STACK: the category tabs (browse index, at rest)
   and the compact search pill occupy the same slot and crossfade on scroll. */
.mkt-header__center {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  margin-inline: 20px;
}
.mkt-header__center > * {
  grid-area: 1 / 1;
}
.mkt-header__search {
  justify-self: center;
  width: 100%;
  max-width: 28rem;
  min-width: 0;
}
.mkt-header__tabs {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-width: 0;
  overflow: hidden;
}

/* Category tab — Airbnb-prominent: ink line-icon + label, visible at rest (a hairline
   frame, full ink text), filled ink pill when active. The icon lifts on hover and the
   whole tab presses (mono-tap) — physical, inviting, still Mono. */
.mkt-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px;
  border-radius: 9999px;
  border: 1px solid color-mix(in oklab, var(--ink) 30%, transparent);
  background: var(--paper);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
}
/* Invisible hit-area extender: the visual pill is ~38px; the tap target must be ≥44px. */
.mkt-tab::after {
  content: "";
  position: absolute;
  inset: -4px 0;
}
/* The landing focus reset squares corners; keep the tab pill round while focused. */
.landing .mkt-tab:focus-visible {
  border-radius: 9999px;
}
.mkt-tab__icon {
  display: flex;
  align-items: center;
}
.mkt-tab:hover {
  border-color: color-mix(in oklab, var(--ink) 45%, transparent);
}
.mkt-tab--active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
@media (prefers-reduced-motion: no-preference) {
  .mkt-tab { transition: border-color var(--mono-dur-hover) var(--mono-ease); }
  .mkt-tab__icon { transition: transform var(--mono-dur-hover) var(--mono-ease); }
  .mkt-tab:hover .mkt-tab__icon { transform: translateY(-1px); }
}

/* Animated category icons — each lucide glyph behaves like its physical object: the shirt
   sways on its hanger, footprints step, the watch rocks on the wrist, the bag sets down,
   the gem catches the light, the tag swings on its hole. Hover (fine pointers) plays ONCE
   per entry — never a loop; the newly-active pill plays once on arrival (selection is a
   full Turbo visit, so arrival IS the tap's affirmation — this also covers touch, where
   hover doesn't exist). Transform/opacity only; every keyframe starts and ends at rest,
   so the base markup is already the reduced-motion state. Origins: fill-box for pivots on
   the glyph's own geometry, view-box for coordinate pivots (watch face 12,12; tag hole 7.5,7.5). */
.mkt-tab__icon svg { overflow: visible; } /* rotation headroom — .mono-check precedent */
.mkt-icon__part--shirt   { transform-box: fill-box; transform-origin: 50% 0%; }   /* hangs from the collar */
.mkt-icon__part--print-a,
.mkt-icon__part--print-b { transform-box: fill-box; transform-origin: 50% 50%; }
.mkt-icon__part--watch   { transform-box: view-box; transform-origin: 50% 50%; }  /* face center (12,12) */
.mkt-icon__part--bag     { transform-box: fill-box; transform-origin: 50% 100%; } /* squashes on its base */
.mkt-icon__part--gem     { transform-box: fill-box; transform-origin: 50% 30%; }  /* pivots near the girdle */
.mkt-icon__part--tag     { transform-box: view-box; transform-origin: 31.25% 31.25%; } /* the hole (7.5,7.5)/24 */

@media (prefers-reduced-motion: no-preference) {
  /* Damped pendulums run on a linear master clock; each segment carries its own ease
     (the mono-check-ring pattern) so the swing decays like a real object. */
  @keyframes mkt-icon-sway {
    0%   { transform: rotate(0deg); animation-timing-function: var(--mono-ease); }
    25%  { transform: rotate(-5deg); animation-timing-function: var(--mono-ease); }
    55%  { transform: rotate(3deg); animation-timing-function: var(--mono-ease); }
    80%  { transform: rotate(-1.5deg); animation-timing-function: var(--mono-ease); }
    100% { transform: rotate(0deg); }
  }
  @keyframes mkt-icon-step {
    0%   { transform: translateY(0); }
    35%  { transform: translateY(-2px); }
    100% { transform: translateY(0); }
  }
  @keyframes mkt-icon-rock {
    0%   { transform: rotate(0deg); animation-timing-function: var(--mono-ease); }
    40%  { transform: rotate(-6deg); animation-timing-function: var(--mono-ease); }
    75%  { transform: rotate(3deg); animation-timing-function: var(--mono-ease); }
    100% { transform: rotate(0deg); }
  }
  @keyframes mkt-icon-setdown {
    0%   { transform: translateY(0) scaleY(1); animation-timing-function: var(--mono-ease); }
    30%  { transform: translateY(-2px) scaleY(1); animation-timing-function: var(--mono-ease-emph); }
    60%  { transform: translateY(0) scaleY(0.95); animation-timing-function: var(--mono-ease); }
    80%  { transform: translateY(0) scaleY(1.01); animation-timing-function: var(--mono-ease); }
    100% { transform: translateY(0) scaleY(1); }
  }
  @keyframes mkt-icon-tilt {
    0%   { transform: rotate(0deg); animation-timing-function: var(--mono-ease); }
    45%  { transform: rotate(-5deg); animation-timing-function: var(--mono-ease); }
    100% { transform: rotate(0deg); }
  }
  @keyframes mkt-icon-glint {
    0%   { opacity: 1; }
    40%  { opacity: 0.25; }
    100% { opacity: 1; }
  }
  @keyframes mkt-icon-swing {
    0%   { transform: rotate(0deg); animation-timing-function: var(--mono-ease); }
    25%  { transform: rotate(-8deg); animation-timing-function: var(--mono-ease); }
    55%  { transform: rotate(5deg); animation-timing-function: var(--mono-ease); }
    80%  { transform: rotate(-2deg); animation-timing-function: var(--mono-ease); }
    100% { transform: rotate(0deg); }
  }

  /* Hover play-once — fine pointers only (touch taps navigate; arrival covers them).
     :not(--active): the selected pill is already affirmed, and excluding it avoids the
     unhover-replays-entrance CSS quirk. */
  @media (hover: hover) and (pointer: fine) {
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--shirt      { animation: mkt-icon-sway var(--mono-dur-draw) linear 1; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--print-a    { animation: mkt-icon-step var(--mono-dur-spring) var(--mono-spring) 1; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--print-b    { animation: mkt-icon-step var(--mono-dur-spring) var(--mono-spring) 1 calc(var(--mono-dur-spring) / 2); }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--watch      { animation: mkt-icon-rock var(--mono-dur-spring) linear 1; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--bag        { animation: mkt-icon-setdown var(--mono-dur-spring) linear 1; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--gem        { animation: mkt-icon-tilt var(--mono-dur-spring) linear 1; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--gem-facets { animation: mkt-icon-glint var(--mono-dur-flash) var(--mono-ease) 1 60ms; }
    .mkt-tab:not(.mkt-tab--active):hover .mkt-icon__part--tag        { animation: mkt-icon-swing var(--mono-dur-draw) linear 1; }
  }

  /* Arrival affirmation on the newly-active pill — plays once per Turbo visit, just after paint. */
  .mkt-tab--active .mkt-icon__part--shirt      { animation: mkt-icon-sway var(--mono-dur-draw) linear 1 var(--mono-dur-fast) backwards; }
  .mkt-tab--active .mkt-icon__part--print-a    { animation: mkt-icon-step var(--mono-dur-spring) var(--mono-spring) 1 var(--mono-dur-fast) backwards; }
  .mkt-tab--active .mkt-icon__part--print-b    { animation: mkt-icon-step var(--mono-dur-spring) var(--mono-spring) 1 calc(var(--mono-dur-fast) + var(--mono-dur-spring) / 2) backwards; }
  .mkt-tab--active .mkt-icon__part--watch      { animation: mkt-icon-rock var(--mono-dur-spring) linear 1 var(--mono-dur-fast) backwards; }
  .mkt-tab--active .mkt-icon__part--bag        { animation: mkt-icon-setdown var(--mono-dur-spring) linear 1 var(--mono-dur-fast) backwards; }
  .mkt-tab--active .mkt-icon__part--gem        { animation: mkt-icon-tilt var(--mono-dur-spring) linear 1 var(--mono-dur-fast) backwards; }
  .mkt-tab--active .mkt-icon__part--gem-facets { animation: mkt-icon-glint var(--mono-dur-flash) var(--mono-ease) 1 calc(var(--mono-dur-fast) + 60ms) backwards; }
  .mkt-tab--active .mkt-icon__part--tag        { animation: mkt-icon-swing var(--mono-dur-draw) linear 1 var(--mono-dur-fast) backwards; }
}
/* Tabs step aside once the buyer scrolls — search becomes the persistent tool. Visible is
   the base state (no-JS keeps the tabs); hiding needs the JS-added html.header-search-in. */
html.header-search-in .mkt-header__tabs {
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}

/* Sell steps aside WITH the tabs: once you're scrolling items you're in browse mode, and Sell is
   the one nav item that isn't part of it. It returns on the way back up — both paths are
   position-mapped, so the reverse is free. ≥640px only in practice: below that Sell isn't
   rendered at all (see the breakpoint block), so this is the desktop/tablet hand-off.
   visibility+opacity, NEVER display: that would collapse the nav and shift every sibling
   mid-scroll, and .mkt-header__center is flex:1 so it would ABSORB the freed width — the pill's
   spring entrance would then play on a target that is itself sliding.
   Gated on --scroll (browse index only): sticky_search_controller mounts on the hero search, so
   header-search-in never fires on PDP/collections — an ungated rule would hide Sell there for
   scroll-driven-animation browsers and for nobody else. */
html.header-search-in .mkt-header__sell--scroll {
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}

/* The hero band (browse index): one visual surface with the sticky header — same paper,
   and it carries the hairline, so the line sits under the SEARCH (Airbnb). Normal flow:
   scrolling IS the collapse (1:1 physical, nothing re-laid-out per frame). The banded
   header goes hairline-less at rest; it regains the line once the band has passed under
   (scroll-driven where supported, class fallback below). */
.mkt-hero-band {
  background: var(--paper);
  border-bottom: 1px solid var(--border);
}
.mkt-hero-band .mkt-hero-search {
  padding-top: 2px;
  padding-bottom: 16px;
}
.mkt-header--banded {
  border-bottom-color: transparent;
}
html.header-search-in .mkt-header--banded {
  border-bottom-color: var(--border);
}

/* <640px the row can't fit wordmark + pill + nav: swap the pill for a round icon link. It is a
   FLEX SIBLING of the header row and must stay one — never a child of .mkt-header__center.
   Flex siblings cannot overlap; grid children can. Housing it in the center's grid slot looked
   tidier and shipped a real bug: at 390px the center collapses to ~4px (flex:1 with no slack to
   distribute), so a 44px item overflows that track rightward and landed 20.4px INSIDE Sell — the
   right half of the ink circle navigated to /sellers on every PDP/collections page, where both
   are ungated (E4/M5) and so neither is ever held inert. A gutter the center has no room to apply
   buys nothing. The icon's breathing room on the index comes from Sell DEPARTING, not from a gap. */
.mkt-header__search-link {
  display: none;
  /* A 30px VISUAL circle inside the 44px hit box — the same transparent-border + padding-box
     clip the compact pill's CTA uses below. A full 44px ink slab beside 16px nav text reads as
     a different species; 30px sits at nav weight and still clears the tap floor. As a flex item
     the hit box abuts the nav without intersecting it, so the 44px target stays whole. */
  border: 7px solid transparent;
  background-clip: padding-box;
}
.mkt-header__search-link svg {
  width: 15px;
  height: 15px;
}

/* Below 900px the header center can't hold unbounded category tabs without mid-pill
   clipping — the index body carries the category row instead (and no-JS desktop keeps
   category access through it too). */
.mkt-cat-mobile {
  display: none;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 32px;
}

/* A long buyer name must not push the sticky row past the viewport at 640-768px —
   the name is the one nav item that can safely truncate. */
.mkt-header__buyer-name {
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 639.9px) {
  .mkt-header__search { display: none; }
  .mkt-header__search-link { display: flex; }
  /* Below 640px the center is EMPTY — the tabs left at 900px and the pill leaves here — so its
     20px gutters are buying nothing. Spend them on the icon instead: the circle gets real air on
     the nav side (where the row previously had none) at zero cost to the row's total width. */
  .mkt-header__center { margin-inline: 0; }
  .mkt-header__search-link { margin-inline: 20px; }
  /* Sell leaves the phone entirely. The row cannot hold wordmark + icon + Sell + Log in + Sign up
     at 390px — 398.5px of content into 358px — and the surplus scrolled the whole PAGE sideways.
     (Pre-existing: master shipped the same overflow, because it showed this icon unconditionally.)
     Sell is the item that goes: the marketplace is buyers-first, Sell already steps aside on
     scroll above 640px, and dropping it also removes the only thing the icon could collide with
     down here. This is a static breakpoint decision — display is never touched by the scroll
     choreography, which still animates visibility/opacity only.
     A signed-in SELLER keeps "Open your app": it is not .mkt-header__sell, and it is their only
     way back into the app. */
  .mkt-header__sell { display: none; }

  /* Tap-to-expand search (M6/M8). The icon toggles html.header-search-open; the header's OWN field
     (#header_q, otherwise display:none here) drops into a second row that OVERLAYS the content below
     — the header is sticky/positioned, so the absolute field never reflows the page and scroll
     position is untouched (the whole point). It wears the header's paper + hairline so it reads as
     the header growing, not a floating popover. */
  /* .mkt-header in the selector is deliberate, not incidental: the --scroll hide gate is
     html.js-anim .mkt-header__search--scroll at (0,2,1), identical to this rule without the
     ancestor, and it sits later in the file — so an equal-specificity rule would LOSE on source
     order (visibility stays hidden, field unfocusable). The .mkt-header ancestor makes this (0,3,1)
     so the reveal wins regardless of order. */
  html.header-search-open .mkt-header .mkt-header__search {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    padding: 0 16px 12px;
    background: var(--paper);
    border-bottom: 1px solid var(--border);
    z-index: 1;
    /* On the browse index this element also carries --scroll, whose gate sets visibility:hidden
       (+ opacity/transform) — which keeps layout but makes the field UNFOCUSABLE. The expand must
       win all three or the caret never lands. */
    visibility: visible;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}
/* The drop-in row eases down in the house motion language (transform+opacity, never a height
   animation — that would fight the sticky overlay). Reduced motion gets the two-state reveal. */
@media (max-width: 639.9px) and (prefers-reduced-motion: no-preference) {
  html.js-anim.header-search-open .mkt-header .mkt-header__search {
    animation: mkt-search-drop var(--mono-dur-enter, 260ms) var(--mono-spring-soft, ease) both;
  }
}
@keyframes mkt-search-drop {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
/* Tabs need more room than the pill: below 900px they'd clip mid-pill between wordmark
   and nav, so the body row takes over earlier than the search swap. */
@media (max-width: 899.9px) {
  .mkt-header__tabs { display: none; }
  .mkt-cat-mobile { display: flex; }
}
/* No-JS: the compact pill can't be revealed (that path is JS/scroll-driven) — remove it from the
   grid stack entirely so it never overlaps the tabs; the hero search in the band carries search.
   The <640px icon link is deliberately NOT hidden here. It looks redundant at rest, but the hero
   is in normal flow and physically leaves by ~126px — it carries search only at scrollY≈0, so it
   is the icon's REPLACEMENT, not its twin. Hiding the icon strands a no-JS phone with no search
   at all the moment it scrolls, which is why the house rule is "no-JS/print/module failure never
   strand the pill hidden". The cost is the double-search at rest for no-JS only — exactly what
   master already shipped to everyone, now narrowed to the one population that can't be gated. */
html:not(.js-anim) .mkt-header__search--scroll {
  display: none;
}
/* Failsafe mirrors the pill reveal by standing the tabs down — this is a GRID-STACK COLLISION
   exception (the force-revealed pill shares their slot), not the general failsafe policy.
   .mkt-header__sell does NOT belong here: hiding it would strand a nav link whenever JS fails to
   boot, and it has nothing to collide with — it is a nav flex item, and so is the icon. */
html.js-anim-failsafe .mkt-header__tabs {
  visibility: hidden;
  pointer-events: none;
}

/* Landing scroll choreography. House rule: the VISIBLE state is the base style —
   .mkt-header__search alone (PDP, collections) is always shown; hiding needs BOTH the
   opt-in --scroll modifier (browse index only) AND html.js-anim, so no-JS/print/module
   failure never strand the pill hidden. js-anim-failsafe force-reveals in the class
   fallback path (in the scroll-driven path the keyframes own visibility, which is fine:
   that path needs no JS, and the hero search is always reachable in the band).
   sticky_search_controller flips html.header-search-in at the hand-off. */
html.js-anim .mkt-header__search--scroll,
html.js-anim .mkt-header__search-link--scroll {
  visibility: hidden;
  opacity: 0;
  transform: translateY(10px) scale(0.94);
  pointer-events: none;
}
/* The icon link joins the failsafe reveal for the same reason the pill does: failsafe means
   Stimulus never booted, so the class fallback can never fire, and the hero has scrolled away by
   ~126px — leaving a <640px phone with NO search at all. Revealing it is only safe because the
   icon is a flex sibling that cannot overlap the nav (see its rule above); when it lived in the
   center's grid slot it overflowed onto Sell, and force-revealing it there handed Sell the right
   half of the search button. Geometry first, then the reveal is free.
   No :focus-within here — a visibility:hidden element is unfocusable, so it can never match (the
   pill's own :focus-within is equally dead; left alone as pre-existing). */
html.header-search-in .mkt-header__search--scroll,
html.header-search-in .mkt-header__search-link--scroll,
html.js-anim-failsafe .mkt-header__search--scroll,
html.js-anim-failsafe .mkt-header__search-link--scroll,
html.js-anim .mkt-header__search--scroll:focus-within {
  visibility: visible;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* The spring entrance — physics only under no-preference; reduced motion snaps instantly.
   visibility is delayed on the way OUT so the exit motion finishes before the pill leaves
   the a11y tree; on the way IN it flips immediately. */
@media (prefers-reduced-motion: no-preference) {
  html.js-anim .mkt-header__search--scroll,
  html.js-anim .mkt-header__search-link--scroll {
    transition:
      transform var(--mono-dur-enter) var(--mono-spring-soft),
      opacity var(--mono-dur-enter) var(--mono-ease),
      visibility 0s linear var(--mono-dur-enter);
  }
  html.header-search-in .mkt-header__search--scroll,
  html.header-search-in .mkt-header__search-link--scroll,
  html.js-anim-failsafe .mkt-header__search--scroll,
  html.js-anim-failsafe .mkt-header__search-link--scroll {
    transition:
      transform var(--mono-dur-enter) var(--mono-spring-soft),
      opacity var(--mono-dur-enter) var(--mono-ease),
      visibility 0s;
  }
  /* Tabs crossfade opposite the pill; the hero row's collapse is the Airbnb header shrink.
     Sell departs on the tabs' timing — it leaves with them, it doesn't arrive with the pill. */
  .mkt-header__tabs,
  .mkt-header__sell--scroll {
    transition:
      transform var(--mono-dur-enter) var(--mono-ease),
      opacity var(--mono-dur-enter) var(--mono-ease),
      visibility 0s;
  }
  html.header-search-in .mkt-header__tabs,
  html.header-search-in .mkt-header__sell--scroll {
    transition:
      transform var(--mono-dur-enter) var(--mono-ease),
      opacity var(--mono-dur-enter) var(--mono-ease),
      visibility 0s linear var(--mono-dur-enter);
  }
  .mkt-header--banded {
    transition: border-bottom-color var(--mono-dur-fast) var(--mono-ease);
  }
}

/* ===== Scroll-driven header choreography (compositor — the jitter-proof path) ==============
   Where animation-timeline: scroll() is supported, the whole hand-off is driven by the
   scroll offset off the main thread: transform/opacity keyframes (plus one paint-only
   border-color for the hairline — no rule here ever animates layout), mapped over the
   first ~220px of scroll. The hero band physically scrolls under the
   header (1:1 with the finger — the "squeeze" is real movement, not a re-layout), while
   the tabs dissolve, the hero pill eases down in scale as it departs, the compact pill
   condenses in, and the header regains its hairline. Keyframe visibility flips discretely
   mid-range, which also gates interactivity — no JS in this path at all (the Stimulus
   controller detects support and stands down; it only drives the class fallback below).
   Reduced motion never enters: the block is gated on no-preference, and the controller's
   fallback provides the instant two-state toggle. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    /* Sell rides the tabs' range and keyframe: both are DEPARTURES, and departures group
       (tabs/hero 0-160) ahead of arrivals (hairline 90-150, pill 120-220). Fading Sell out
       across the pill's arrival range would put two opposing motions on one row. */
    html.js-anim .mkt-header__tabs,
    html.js-anim .mkt-header__sell--scroll {
      animation: mkt-tabs-depart linear both;
      animation-timeline: scroll(root);
      animation-range: 0px 110px;
    }
    /* Ranges must NOT overlap the tabs': while both grid-stacked layers are mid-fade the
       DOM-later pill would intercept taps meant for still-visible tabs. The keyframes also
       carry pointer-events (discrete — flips at 50%), so only one layer is ever interactive.
       The <640px icon link shares the pill's range: same hand-off, smaller viewport, and it
       can never co-render with the tabs (they part company at 900px). */
    html.js-anim .mkt-header__search--scroll,
    html.js-anim .mkt-header__search-link--scroll {
      animation: mkt-pill-condense linear both;
      animation-timeline: scroll(root);
      animation-range: 120px 220px;
    }
    html.js-anim .mkt-hero-band .mkt-hero-search {
      transform-origin: top center;
      animation: mkt-hero-depart linear both;
      animation-timeline: scroll(root);
      animation-range: 0px 160px;
    }
    html.js-anim .mkt-header--banded {
      transition: none;
      animation: mkt-hairline-arrive linear both;
      animation-timeline: scroll(root);
      animation-range: 90px 150px;
    }
  }
}
@keyframes mkt-tabs-depart {
  from { opacity: 1; transform: translateY(0); visibility: visible; pointer-events: auto; }
  to { opacity: 0; transform: translateY(-8px); visibility: hidden; pointer-events: none; }
}
@keyframes mkt-pill-condense {
  from { opacity: 0; transform: translateY(12px) scale(0.9); visibility: hidden; pointer-events: none; }
  to { opacity: 1; transform: translateY(0) scale(1); visibility: visible; pointer-events: auto; }
}
@keyframes mkt-hero-depart {
  from { opacity: 1; transform: scale(1); }
  to { opacity: 0.4; transform: scale(0.96); }
}
@keyframes mkt-hairline-arrive {
  from { border-bottom-color: transparent; }
  to { border-bottom-color: var(--border); }
}

/* Compact header pill sits LOWER than the hero (Airbnb's minimized bar): a 30px visual
   circle breathing 6px above/below inside a 42px pill. The transparent border keeps the
   button's hit box at 44px (background-clip draws the ink circle on the padding box only). */
.mkt-header__search .ds-search--round {
  min-height: 42px;
  padding-block: 6px;
  padding-right: 6px;
}
.mkt-header__search .ds-search__submit--round {
  width: 44px;
  height: 44px;
  margin-block: -7px; /* keep the 44px hit box without inflating the 42px pill */
  border: 7px solid transparent;
  background-clip: padding-box;
}
.mkt-header__search .ds-search__submit--round svg {
  width: 15px;
  height: 15px;
}
.mkt-header__search .ds-search .ds-search__input {
  height: 28px;
  /* font stays 16px (the pill-wide iOS no-zoom rule) — the lower height carries the scale */
}
.mkt-flash {
  margin-bottom: 24px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  background: var(--mist);
  color: var(--ink);
  font-size: 14px;
}
.mkt-flash--alert {
  border-color: var(--brick);
  color: var(--brick);
  background: var(--paper);
}

/* ============================================================
   Product detail page — "The Archive"
   ============================================================ */
.pdp__top {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  align-items: start;
}
@media (min-width: 900px) {
  .pdp__top {
    grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr);
    gap: 56px;
  }
  .pdp__rail { position: sticky; top: 24px; }
}

/* Gallery — the object framed as an archive accession */
.pdp-gallery { margin: 0; }
.pdp-gallery__lead {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--mist);
  border: 1px solid var(--line);
  overflow: hidden;
}
.pdp-gallery__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pdp-gallery__placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ui-font-sans);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gray);
}
.pdp-gallery__counter,
.pdp-catalog-plate {
  position: absolute;
  top: 16px;
  font-family: var(--ui-font-mono);
  font-size: 12px;
  color: var(--gray);
  z-index: 1;
}
.pdp-gallery__counter { left: 16px; }
.pdp-catalog-plate { right: 16px; }
.pdp-gallery__thumbs {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.pdp-gallery__thumb {
  display: block;
  width: 100%;
  padding: 0;
  aspect-ratio: 4 / 5;
  background: var(--mist);
  border: 1px solid var(--line);
  overflow: hidden;
  cursor: pointer;
  /* Physicality: the gallery thumb presses with the one Mono spring, reduced-motion-safe. */
  transition: border-color 0.15s ease, transform var(--mono-dur-fast) var(--mono-spring);
}
.pdp-gallery__thumb[aria-current="true"] { border-color: var(--ink); }
.pdp-gallery__thumb:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) {
  .pdp-gallery__thumb:active:not(:disabled) {
    transform: scale(var(--mono-press-scale));
  }
}
.pdp-gallery__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Info rail */
.pdp__title { margin: 8px 0 0; letter-spacing: -0.015em; line-height: 1.03; }
.pdp-scarcity { font-family: var(--ui-font-sans); font-size: 13px; color: var(--gray); }

/* Condition / size / tag chips (flat Mono corners) */
.pdp-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.pdp-tag {
  font-family: var(--ui-font-sans);
  font-size: 12px;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  padding: 5px 12px;
}

.pdp__actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
.pdp__cta { width: 100%; text-align: center; }

/* Provenance — first-class trust */
.pdp-provenance { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line); }
.pdp-provenance__text { margin: 8px 0 0; font-size: 14px; line-height: 1.5; color: var(--muted); }

/* Spec block (DS4 ledger row) */
.pdp-spec { margin: 28px 0 0; padding-top: 8px; border-top: 1px solid var(--line); }
.pdp-spec__row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.pdp-spec__row dt {
  font-family: var(--ui-font-sans);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gray);
}
.pdp-spec__row dd { margin: 0; font-size: 14px; color: var(--ink); text-align: right; }

/* Seller — a warm credibility card in EVERY variant (grafted from the Flea direction); the
   strongest honest trust lever on a solo-seller marketplace. */
.pdp-seller {
  margin-top: 28px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--paper);
  display: flex;
  align-items: center;
  gap: 14px;
}
.pdp-seller__avatar {
  width: 44px;
  height: 44px;
  border-radius: 999px;
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ui-font-display);
  color: var(--ink);
}
.pdp-seller__name { margin: 0; font-weight: 500; color: var(--ink); }

/* Lower full-width sections */
.pdp__section { margin-top: 56px; padding-top: 32px; border-top: 1px solid var(--line); }
.pdp__description { margin-top: 12px; max-width: 60ch; font-size: 16px; line-height: 1.7; color: var(--muted); }
.pdp-details__item { border-bottom: 1px solid var(--line); padding: 18px 0; }
.pdp-details__item summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  font-size: 16px;
  color: var(--ink);
}
.pdp-details__item summary::-webkit-details-marker { display: none; }
.pdp-details__item summary::after { content: "+"; font-family: var(--ui-font-mono); color: var(--gray); }
.pdp-details__item[open] summary::after { content: "–"; }
.pdp-details__item p { margin: 12px 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); max-width: 60ch; }
.pdp-protection { border: 1px solid var(--line); background: var(--paper); padding: 20px 24px; }
.pdp-protection__title { margin: 0; font-weight: 500; color: var(--ink); }
.pdp-protection__text { margin: 6px 0 0; font-size: 14px; color: var(--muted); }

/* More from this shop */
.pdp-related__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}
.pdp-related__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
@media (min-width: 900px) {
  .pdp-related__grid { grid-template-columns: repeat(4, 1fr); }
}

/* Mobile sticky buy bar — the one action stays reachable on the long phone page */
.pdp__buybar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 40px;
  padding: 12px 16px;
  background: var(--paper);
  border-top: 1px solid var(--line);
}
.pdp__buybar-price { font-size: 18px; }
.pdp__buybar-cta { flex: 0 0 auto; }
@media (min-width: 900px) {
  .pdp__buybar { display: none; }
}

/* ============================================================
   PDP — honest pricing ledger, seller signals, scrim, variants
   ============================================================ */

/* Gallery overlay chips get a REAL background so the counter/plate text is never gray-on-photo
   (the one contrast the page couldn't certify). Now ink-on-paper with a hairline — known ≥ floor. */
.pdp-gallery__counter,
.pdp-catalog-plate {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  padding: 4px 9px;
  color: var(--ink);
}

/* The honest 1-of-1 signal given real (but Mono) presence — an ink catalog tag, not a whisper.
   Never a fake countdown or "N viewing". */
.pdp-scarcity--inline {
  display: inline-block;
  margin-top: 14px;
  font-family: var(--ui-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  border-top: 1px solid var(--ink);
  padding-top: 6px;
}

/* Transparent price ledger — Item + Buyer Protection + You-pay-today, with shipping stated honestly
   as calculated-at-checkout. Same numbers the checkout charges (single source on the model). */
.pdp-ledger { margin-top: 22px; }
.pdp-ledger__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 7px 0;
}
.pdp-ledger__label { font-family: var(--ui-font-sans); font-size: 14px; color: var(--muted); }
.pdp-ledger__value { font-family: var(--ui-font-mono); font-size: 15px; color: var(--ink); }
.pdp-ledger__row .landing-money { font-size: 18px; }
.pdp-ledger__row--total {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.pdp-ledger__row--total .pdp-ledger__label { color: var(--ink); font-weight: 500; }
.pdp-ledger__row--total .landing-money { font-size: 22px; }
.pdp-ledger__note { margin: 10px 0 0; font-family: var(--ui-font-sans); font-size: 12px; line-height: 1.5; color: var(--muted); max-width: 44ch; }
.pdp-ledger__shipping { margin: 10px 0 0; font-family: var(--ui-font-sans); font-size: 13px; color: var(--gray); }

/* Disabled-CTA reason (class avoids the `pdp__cta` substring so a `scan("pdp__cta")` CTA-count
   assertion doesn't double-count this hint). */
.pdp__buy-hint { margin: 8px 0 0; font-size: 13px; color: var(--muted); }

/* Section kicker promoted to a real <h2> — strip the heading default margins */
.pdp__section-head { margin: 0; }

/* Honest seller signals line (member-since · pieces · sold) */
.pdp-seller__signals { margin: 3px 0 4px; font-family: var(--ui-font-sans); font-size: 13px; color: var(--muted); }

/* Breadcrumb — orientation (Marketplace › seller shop › item). Quiet mono row above the title. */
.pdp-breadcrumb { margin-bottom: 18px; }
.pdp-breadcrumb ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-family: var(--ui-font-sans);
  font-size: 13px;
  color: var(--muted);
}
.pdp-breadcrumb a { color: var(--muted); }
.pdp-breadcrumb a:hover { color: var(--ink); }
.pdp-breadcrumb__sep { color: var(--gray); }
.pdp-breadcrumb__current { color: var(--ink); }

/* Buyer order-confirmation END — a calm Mono peak (ink, never green) that sets expectations */
.mkt-confirm { max-width: 460px; margin: 24px auto; text-align: center; }
.mkt-confirm__mark { color: var(--ink); display: flex; justify-content: center; margin-bottom: 20px; }
.mkt-confirm__title { margin: 0; }
.mkt-confirm__lead { margin: 12px auto 0; max-width: 42ch; font-size: 15px; line-height: 1.6; color: var(--muted); }
.mkt-confirm__next { margin-top: 32px; padding: 20px 22px; border: 1px solid var(--line); background: var(--paper); text-align: left; }
.mkt-confirm__steps { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.mkt-confirm__steps li { display: flex; align-items: flex-start; gap: 12px; font-size: 14px; line-height: 1.5; color: var(--text); }
.mkt-confirm__n {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 1px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ui-font-mono);
  font-size: 12px;
  color: var(--ink);
}
.mkt-confirm__cta { display: inline-block; margin-top: 28px; }

/* Buyer auth (passwordless email + code) — Mono, calm, one field at a time */
.mkt-auth { max-width: 400px; margin: 24px auto; }
.mkt-auth__title { margin: 0; }
.mkt-auth__intro { margin: 10px 0 24px; font-size: 14px; line-height: 1.5; color: var(--muted); }
.mkt-auth__form { display: flex; flex-direction: column; gap: 16px; }
.mkt-field { display: flex; flex-direction: column; gap: 6px; }
.mkt-field__label { font-family: var(--ui-font-sans); font-size: 13px; color: var(--text); }
.mkt-field__input {
  width: 100%;
  padding: 10px 12px;
  font-family: var(--ui-font-sans);
  font-size: 15px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
}
.mkt-field__input:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
.mkt-field__input--code { font-family: var(--ui-font-mono); letter-spacing: 0.3em; text-align: center; font-size: 22px; }
.mkt-auth__submit { width: 100%; text-align: center; margin-top: 4px; }
.mkt-auth__alt { margin-top: 18px; font-size: 14px; color: var(--muted); text-align: center; }
.mkt-auth__errors { border: 1px solid var(--brick); background: var(--paper); padding: 12px 14px; border-radius: var(--radius-small); }
.mkt-auth__errors ul { margin: 0; padding-left: 18px; }
.mkt-auth__errors li { font-size: 13px; color: var(--brick); }

/* Marketplace index pagination (kaminari) — Mono */
.marketplace-pagination { margin-top: 48px; display: flex; justify-content: center; }
.marketplace-pagination .pagination { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-family: var(--ui-font-mono); font-size: 13px; }
.marketplace-pagination a,
.marketplace-pagination .current {
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--line);
  color: var(--ink);
  text-decoration: none;
}
.marketplace-pagination .current { background: var(--mist); }
.marketplace-pagination a:hover { background: var(--mist); }

/* ============================================================
   Mono shared primitives — seeded by the design-system sweep.
   New shared classes for components whose CSS had no home yet
   (Kicker, Checkbox, Toggle, Tabs). Token-driven; the Mono values
   resolve inside .app-mono / .landing, with literal fallbacks so
   they degrade sanely on legacy pages.
   ============================================================ */

/* ----- Kicker (eyebrow): Plex Mono, 500, 12px, 0.1em, uppercase, muted ----- */
.app-kicker {
  font-family: var(--ui-font-mono, "IBM Plex Mono", ui-monospace, monospace);
  font-weight: 500;
  font-size: 12px;
  line-height: 1.2;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--subtle);
}

/* ----- Checkbox: 20px ink box, 2px radius, ink fill + white tick on :checked ----- */
.checkbox {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border: 1px solid var(--ink);
  border-radius: 2px;
  background: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 150ms ease, border-color 150ms ease;
}
.checkbox:checked {
  border-width: 0;
  background: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.5 6.2l2.3 2.3L9.5 3.5' stroke='%23FFFFFF' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
}
.checkbox:focus-visible {
  outline: none;
  border-width: 2px;
  border-color: var(--ink);
}
.checkbox:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.checkbox-label {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--ui-font-sans);
  font-size: 16px;
  color: var(--ink);
  cursor: pointer;
}
.checkbox-label:has(.checkbox:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ----- Toggle: pill switch, ink ON track, white sliding knob ----- */
.ds-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-family: var(--ui-font-sans);
  font-size: 16px;
  color: var(--ink);
}
.ds-toggle:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}
.ds-toggle input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.ds-toggle__track {
  position: relative;
  width: 44px;
  height: 26px;
  border-radius: 9999px;
  background: var(--white);
  border: 1px solid var(--line);
  transition: background 150ms ease, border-color 150ms ease;
  flex-shrink: 0;
}
.ds-toggle__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 9999px;
  background: var(--ink);
  transition: left 150ms ease, background 150ms ease;
}
.ds-toggle input:checked + .ds-toggle__track {
  background: var(--ink);
  border-color: var(--ink);
}
.ds-toggle input:checked + .ds-toggle__track .ds-toggle__knob {
  left: 20px;
  background: var(--white);
}
.ds-toggle input:focus-visible + .ds-toggle__track {
  border-width: 2px;
}

/* ----- Tabs (navigation): flat underline bar, no pill ----- */
.ds-tabs {
  display: flex;
  gap: 28px;
  border-bottom: 1px solid var(--line);
}
.ds-tab {
  position: relative;
  padding: 0 0 14px;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--ui-font-sans);
  font-weight: 500;
  font-size: 14px;
  color: var(--muted);
  transition: color 150ms ease;
}
.ds-tab[aria-selected="true"],
.ds-tab.is-active {
  font-weight: 600;
  color: var(--ink);
}
.ds-tab::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: transparent;
}
.ds-tab[aria-selected="true"]::after,
.ds-tab.is-active::after {
  background: var(--ink);
}
.ds-tab:focus-visible {
  outline: none;
  box-shadow: none;
}
.ds-tab:focus-visible::after {
  background: var(--ink);
}

/* ============================================================
   Collections — "playlist" cards, curated-edits shelf, page.
   Authored against canonical Mono tokens (rendered under body.landing).
   Photography is the only colour; everything else is ink on paper.
   One compact shelf — the product grid is the star of /marketplace.
   ============================================================ */

/* Curated-edits shelf (horizontal scroll-snap) — an editorial break INSIDE the item grid flow */
.edits-rail { margin: 40px 0; }
.edits-rail__head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
.edits-rail__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(140px, 160px);
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 8px;
  scrollbar-width: thin;
}
@media (min-width: 720px) {
  .edits-rail__track { grid-auto-columns: minmax(150px, 176px); }
}
.edits-rail__track > * { scroll-snap-align: start; }

/* Collection card (the "playlist" tile) */
.collection-card { display: flex; flex-direction: column; text-decoration: none; }
.collection-card__cover {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--mist);
  border: 1px solid var(--line);
  overflow: hidden;
}
.collection-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 320ms var(--mono-ease);
}
.collection-card:hover .collection-card__img { transform: scale(1.02); }
/* 2×2 member mosaic — hairline grout in --line, cells crop like the single cover.
   Hovers settle exactly like the single-cover img (mosaic is the dominant card type). */
.collection-card__mosaic {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  transition: transform 320ms var(--mono-ease);
}
.collection-card:hover .collection-card__mosaic { transform: scale(1.02); }
.collection-card__cell {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Monogram tile — the imageless fallback for non-homepage shelves (PDP below-fold). */
.collection-card__placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ui-font-display);
  font-size: 40px;
  line-height: 1;
  color: var(--gray);
}
.collection-card__title {
  margin: 8px 0 0;
  font-family: var(--ui-font-display);
  font-size: 15px;
  line-height: 1.25;
  color: var(--ink);
}
.collection-card__meta { margin: 2px 0 0; font-family: var(--ui-font-mono); font-size: 12px; color: var(--gray); }

/* Collection page (the "mini popup shop") */
.collection-page { margin-bottom: 40px; }
.collection-page__head { margin-bottom: 8px; }
.collection-page__headrow { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-top: 8px; }
.collection-page__title { margin: 0; color: var(--ink); }
.collection-page__share { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
.collection-page__meta { margin: 12px 0 0; font-family: var(--ui-font-mono); font-size: 13px; color: var(--gray); }

/* ============================================================
   Mono Motion (mirrored from the design system's handoff/mono-motion +
   handoff/activation-journey → tokens/motion.css). "One spring, five tiers,
   used sparingly, always functional." Namespaced (--mono-* / .mono-*) so it
   coexists with Tailwind; lives here in ui.css (plain Propshaft CSS, already
   loaded app-wide after :app) rather than a separate @import.

   Rules baked in: the visible end-state IS the base style — animations play
   FROM hidden and ONLY under (prefers-reduced-motion: no-preference), so print,
   PDF, and reduced-motion always land on content (never a stuck opacity:0).
   Affirmations run ONCE. Every animating figure still renders its real value
   (JS counts toward it; CSS never hides it). Transform/opacity only.
   ============================================================ */
:root {
  --mono-ease:         cubic-bezier(0.2, 0.8, 0.2, 1);    /* standard quiet ease-out */
  --mono-ease-emph:    cubic-bezier(0.2, 0.9, 0.1, 1);    /* decisive reveal */
  --mono-ease-exit:    cubic-bezier(0.4, 0, 1, 1);        /* dismiss */
  --mono-spring:       cubic-bezier(0.34, 1.42, 0.5, 1);  /* the one overshoot spring */
  --mono-spring-soft:  cubic-bezier(0.34, 1.26, 0.46, 1); /* gentler, for larger surfaces */

  /* Response / reveal / transition */
  --mono-dur-tap:    90ms;   /* tier-0 immediate tap (below --mono-dur-fast) */
  --mono-dur-fast:   120ms;
  --mono-dur-hover:  140ms;
  --mono-dur:        160ms;
  --mono-dur-flash:  200ms;
  --mono-dur-enter:  220ms;
  --mono-dur-settle: 240ms;  /* a figure's settle as a count-up lands */
  --mono-dur-screen: 280ms;
  --mono-dur-sheet:  300ms;
  --mono-dur-spring: 320ms;  /* the success ring stamp (= --mono-dur-ring) */
  --mono-dur-coda:   360ms;  /* an END coda screen settling at the exhale */
  --mono-dur-draw:   560ms;
  --mono-stagger-step: 42ms;

  /* Count-up speeds — the felt money figure */
  --mono-dur-count:      1000ms; /* dashboard money tiles */
  --mono-dur-count-peak: 1400ms; /* first-sale / payout peak figure */

  /* Micro shared values */
  --mono-press-scale: 0.97;
  --mono-hover-lift:  -3px;
  --mono-nudge-from:  2px;
  --mono-overscroll-max: 96px; /* rubber-band: max damped pull past a scroll boundary */
}

/* Physicality — physical-correctness: contain overscroll so a bounded scroller (modal,
   sheet, long list) never chains its scroll to the page or fires an accidental
   pull-to-refresh. Not an animation, so it lives outside the reduced-motion gate.
   The elastic rubber-band on top of this is the overscroll_controller (transform-only). */
.mono-overscroll { overscroll-behavior: contain; }

@keyframes mono-rise        { from { opacity:0; transform:translateY(8px); }  to { opacity:1; transform:none; } }
@keyframes mono-fade        { from { opacity:0; }                              to { opacity:1; } }
@keyframes mono-sheet-up    { from { opacity:0; transform:translateY(16px); } to { opacity:1; transform:none; } }
@keyframes mono-screen-push { from { opacity:0; transform:translateX(24px); } to { opacity:1; transform:none; } }
@keyframes mono-screen-pop  { from { opacity:0; transform:translateX(-24px);} to { opacity:1; transform:none; } }
@keyframes mono-stamp       { from { opacity:0; transform:scale(0.6); }       to { opacity:1; transform:scale(1); } }
@keyframes mono-tick        { from { transform:scale(0); } 70% { transform:scale(1.18); } to { transform:scale(1); } }
@keyframes mono-pulse-once  { 0% { transform:scale(1); } 40% { transform:scale(1.06); } 100% { transform:scale(1); } }
@keyframes mono-draw        { from { stroke-dashoffset: var(--draw-len,1000); } to { stroke-dashoffset:0; } }
/* tier-0 micro */
@keyframes mono-nudge       { from { opacity:0; transform:translateY(var(--mono-nudge-from)); } to { opacity:1; transform:none; } }
@keyframes mono-flash       { from { color: var(--gray, #6B6B66); } to { color: var(--ink, #0E0E0C); } }
@keyframes mono-settle      { from { transform:scale(1.04); } to { transform:scale(1); } }
/* Top-anchored toast: falls in from above the top edge, leaves back out the SAME edge.
   Deliberately NOT mono-sheet-up — that rises from BELOW (right for a bottom sheet, spatially
   backwards for a toast pinned to the top).
   Bare translateY is safe even though the toast is centred with Tailwind's `-translate-x-1/2`:
   v4 compiles that to the independent `translate` property, which COMPOSES with `transform`
   instead of being clobbered by it. (.sale-toast must bake translateX(-50%) into its own
   keyframes precisely because it centres with raw `transform`.) */
@keyframes mono-toast-in    { from { opacity:0; transform:translateY(-16px); } to { opacity:1; transform:none; } }
@keyframes mono-toast-out   { from { opacity:1; transform:none; } to { opacity:0; transform:translateY(-16px); } }
/* Account dropdown: scale(0.97), never scale(0) — from zero it pops open like a balloon instead of
   growing out of its trigger. Enter only; see the class below for why there is no exit. */
@keyframes account-menu-in  { from { opacity:0; transform:scale(0.97); } to { opacity:1; transform:none; } }

/* Success checkmark — mirrored VERBATIM from the Brechó Brand Book Figma Motion (node 378:7):
   the ink ring does a damped ball-drop bounce, then the ink tick draws on via path-trim. Figma
   exports this as a 2.2s infinite loop; the .mono-check__* classes below play it ONCE then hold,
   per the house rule above ("Affirmations run ONCE"). Stops / easings / values are unchanged from
   the export — only Figma's `translate:` shorthand is written as `transform: translateY()` to match
   the codebase idiom. The translateY px resolve in the SVG's 84-unit space, so the bounce scales
   with the rendered size. mono-check-draw needs pathLength="1" on the path (set in the component).

   SANCTIONED EXCEPTION (do not "simplify" to a single overshoot): the multi-stage ball-drop is a
   LOUDER motion than the system's "one overshoot spring" affirmations (mono-tick/stamp). It is the
   brand book's own checkmark, adopted deliberately and approved to ship as-is — the design source
   wins here. If you're tempted to flatten it to mono-stamp/mono-tick, that's a brand-owner decision,
   not a cleanup. */
@keyframes mono-check-ring {
  0%      { animation-timing-function: cubic-bezier(0,0,0.15,1); transform: translateY(0); }
  6.818%  { animation-timing-function: cubic-bezier(0.55,0,1,1); transform: translateY(-28px); }
  15.909% { animation-timing-function: cubic-bezier(0,0,0.15,1); transform: translateY(0); }
  22.727% { animation-timing-function: cubic-bezier(0.55,0,1,1); transform: translateY(-13px); }
  29.545% { animation-timing-function: cubic-bezier(0,0,0.15,1); transform: translateY(0); }
  34.091% { animation-timing-function: ease-out;                 transform: translateY(-5px); }
  38.636% { transform: translateY(0); }
  100%    { transform: translateY(0); }
}
@keyframes mono-check-draw {
  0%      { stroke-dasharray: 0 1; visibility: hidden; }
  45.455% { animation-timing-function: ease-in-out; stroke-dasharray: 0 1; visibility: hidden; }
  86.364% { stroke-dasharray: 1 1; visibility: visible; }
  100%    { stroke-dasharray: 1 1; visibility: visible; }
}

/* Headroom for the bounce. The ring (r=40.75 @ cx/cy 42) sits flush to the 84 viewBox, so the
   default SVG `overflow:hidden` would slice its top into a flat-cut "U" while it hops up — reading
   as a glitch for the ~1s before the tick draws. overflow:visible lets the WHOLE ring bounce inside
   the host's padding (modal p-8 / coda pt-12 absorb the ~19–21px rise at 56/64px). Not a motion
   value, so it lives outside the reduced-motion gate. */
.mono-check { overflow: visible; }

/* Ink-bleed wordmark (OriginKit "inkbleed" → Mono: goo only, no chromatic fringe — ink
   stays ink). The static link is upgraded by ink_bleed_controller.js, which builds
   per-character .ink-bleed__wrap spans holding four layers and drives --mx/--my (char-local
   cursor) + --spot-on (0..~1.6 spot scale) in a rAF loop; the goo filter itself is injected
   and mounted by the controller only while the spot is live. These rules are inert on the
   static markup (no wraps, no vars), so they need no reduced-motion gate — the controller
   never builds the layered DOM under prefers-reduced-motion. */
.ink-bleed {
  /* Lens radius in `em`, so it is GLYPH-RELATIVE, not site-relative. `em` resolves at the USE
     site (the layer span), so each render site scales itself: header 31px type → 9.3px, footer
     26px → 7.8px. Same reasoning as GOO_BLUR_RATIO in the controller (assertion E3) — the melt
     must be sized against the type it melts — but CSS carries it here with no JS at all.
     0.3em ≈ half a glyph. Was 26px: a 52px lens spanning ~3 of our ~17px letters, which fused
     r-e-c into one unreadable slab (reported: "the ink bleed is too strong … individual letters
     should bleed, not the whole word").
     THE USABLE WINDOW IS ~0.24em–0.34em. Both bounds are load-bearing and neither is obvious:
       · floor ~0.24em — below this the melt stops reading at all.
       · ceiling ~0.34em — the lens is multiplied by PRESS_BOOST (1.6) while held, so
         `radius x 1.6` must stay under the ~0.55em letter advance. Past that, holding ONE letter
         melts its NEIGHBOURS: the reported bug, restored.
     The ceiling is invisible from the hover state — 0.36em is clean on hover and damages b (-15%)
     and e (-48%) at press. Measured by sweeping the radius and reading counter area per glyph,
     with `r` (no counter) as the control; at the old 26px, hovering `r` destroyed `b` outright —
     the melt wasn't even under the cursor. Both bounds are pinned by assertion E6.
     `--ib-blur` deliberately stays px even though the same-hook argument above applies to it —
     and it DOES apply: measured, `--ib-radius` is a constant 0.2999em across sites while
     `--ib-blur` spreads 0.1613em–0.1923em, a 19.2% mis-scale (the same relative spread the radius
     had before this change). So "we measured the footer and saw no collateral" is NOT the reason;
     that evidence doesn't distinguish the two values, and it's bounded by the two type sizes that
     happen to exist today.
     The reason that holds is the CLIFF: the radius is multiplied by PRESS_BOOST and must clear the
     letter advance — cross it and letters fuse, discontinuously (see the ceiling above). Blur is
     never boosted and has no such bound, so mis-scaling it degrades gracefully rather than falling
     off a cliff. A bounded, graceful, purely aesthetic drift does not earn a change; the radius's
     cliff did. Revisit if a render site lands at markedly different type.
     NOTE: --ib-choker stays px because its value is DUPLICATED as CHOKER_OFFSET in
     ink_bleed_controller.js (the JS shifts --mx-l/--mx-r by ∓ the same amount to cancel the CSS
     `left` offset). Change one and you must change the other — they are one value in two files. */
  --ib-radius: 0.3em;         /* spot lens radius (original: 35px at 180px type) */
  --ib-blur: 5px;             /* spot-copy gaussian blur (original: 10px) */
  --ib-choker: 6px;           /* choker offsets — the liquid fuse inside the lens (see NOTE above) */
  /* No `display` here: ui.css loads AFTER the Tailwind bundle at equal specificity,
     so a `display` on this shared hook would override the layout utilities the link
     itself carries. The app-header link is `flex items-center gap-[3px]` — forcing
     inline-block silently killed that flex. Each call site owns its own display:
     `.landing-wordmark` (below) sets inline-block; the app header uses Tailwind flex. */
}
.ink-bleed__text { display: inline-block; position: relative; }
.ink-bleed__wrap {
  position: relative;
  display: inline-block;
  white-space: pre;
  overflow: visible;
}
.ink-bleed__layer {
  display: inline-block;
  white-space: pre;
}
/* EVERY mask below is scoped to .ink-bleed--active — the class the controller toggles once
   smooth.on > 0.01. That scoping is the whole fix for the invisible-wordmark bug, so it is
   not cosmetic nesting: at rest --spot-on is 0, which made the radius `calc(26px * 0)` — a
   DEGENERATE zero-radius radial-gradient. Chromium and Playwright's WebKit paint such a
   gradient's last stop; real iOS Safari paints nothing at all. The sharp layer is the ONLY
   layer painting the wordmark once upgraded, so on iOS the brand mark vanished from every
   page of the site while its box kept the space (reported from a real iPhone; confirmed by
   touching the empty spot, which lifts --spot-on and brought the logo back).
   Under .ink-bleed--active the radius is provably non-zero, so the degenerate case is now
   unreachable rather than merely reinterpreted. Rest state = no mask at all, which also
   makes the PRD's "at rest the logo costs nothing" claim true for the first time (24 masked
   layers per wordmark were compositing on every page). */
.ink-bleed--active .ink-bleed__layer--sharp {
  /* hidden INSIDE the spot circle so the blurred ink shows through */
  mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx, -9999px) var(--my, -9999px), transparent 0%, rgb(0 0 0) 100%);
  -webkit-mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx, -9999px) var(--my, -9999px), transparent 0%, rgb(0 0 0) 100%);
}
.ink-bleed__layer--spot,
.ink-bleed__layer--choker-l,
.ink-bleed__layer--choker-r {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  /* Hidden at rest by a REAL rule. These duplicates used to be hidden only as a side effect of
     the same degenerate mask — an accident that happened to agree across engines (a zero-radius
     circle reveals nothing either way), which is why the iOS bug showed up as a missing logo
     rather than as three blurred copies stacked over it. With the rest-state mask gone they
     would otherwise paint unmasked. */
  visibility: hidden;
}
.ink-bleed--active .ink-bleed__layer--spot,
.ink-bleed--active .ink-bleed__layer--choker-l,
.ink-bleed--active .ink-bleed__layer--choker-r {
  visibility: visible;
  /* visible only INSIDE the spot circle */
  mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
}
.ink-bleed__layer--spot { filter: blur(var(--ib-blur)); }
.ink-bleed__layer--choker-l { left: calc(var(--ib-choker) * -1); }
.ink-bleed__layer--choker-r { left: var(--ib-choker); }
.ink-bleed--active .ink-bleed__layer--choker-l {
  mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx-l, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx-l, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
  will-change: mask-image;
}
.ink-bleed--active .ink-bleed__layer--choker-r {
  mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx-r, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle calc(var(--ib-radius) * var(--spot-on, 0)) at var(--mx-r, -9999px) var(--my, -9999px), rgb(0 0 0) 0%, transparent 100%);
  will-change: mask-image;
}

@media (prefers-reduced-motion: no-preference) {
  /* 0 · MICRO */
  .mono-tap { transition: transform var(--mono-dur-tap) var(--mono-spring), background var(--mono-dur-hover) var(--mono-ease), border-color var(--mono-dur-hover) var(--mono-ease); }
  .mono-tap:active { transform: scale(var(--mono-press-scale)); }
  .mono-hover { transition: color var(--mono-dur-hover) var(--mono-ease), background var(--mono-dur-hover) var(--mono-ease), border-color var(--mono-dur-hover) var(--mono-ease); }
  .mono-underline { position: relative; }
  .mono-underline::after { content:""; position:absolute; left:0; right:0; bottom:-2px; height:1px; background:currentColor; transform:scaleX(0); transform-origin:left; transition: transform var(--mono-dur-hover) var(--mono-ease-emph); }
  .mono-underline:hover::after { transform: scaleX(1); }
  .mono-nudge  { animation: mono-nudge var(--mono-dur-fast) var(--mono-spring) both; }
  .mono-flash  { animation: mono-flash var(--mono-dur-flash) var(--mono-ease) 1; }
  /* Physicality — money settles with weight, never bounces: --mono-ease-emph decelerates
     into rest without dipping below 1.0 (the spring would overshoot under target). Resolves
     the count-up overshoot question — on numbers, a bounce reads as untrustworthy. Spring
     overshoot stays for non-money affirmations (.mono-tick / .mono-stamp / .mono-press). */
  .mono-settle { animation: mono-settle var(--mono-dur-settle) var(--mono-ease-emph) 1; }
  .mono-coda   { animation: mono-rise var(--mono-dur-coda) var(--mono-spring-soft) both; }

  /* 1 · RESPONSE */
  .mono-press { transition: transform var(--mono-dur-fast) var(--mono-spring); }
  .mono-press:active { transform: scale(var(--mono-press-scale)); }
  .mono-hover-lift { transition: transform var(--mono-dur) var(--mono-spring), box-shadow var(--mono-dur) var(--mono-ease); }
  .mono-hover-lift:hover { transform: translateY(var(--mono-hover-lift)); box-shadow: var(--shadow-card, 0 8px 24px -12px rgba(14,14,12,.16)); }

  /* 2 · REVEAL */
  .mono-rise { animation: mono-rise var(--mono-dur-enter) var(--mono-ease-emph) both; }
  .mono-fade { animation: mono-fade var(--mono-dur) var(--mono-ease) both; }
  .mono-stagger > * { animation: mono-rise var(--mono-dur-enter) var(--mono-ease-emph) both; }
  .mono-stagger > *:nth-child(1){animation-delay:0ms}
  .mono-stagger > *:nth-child(2){animation-delay:calc(var(--mono-stagger-step)*1)}
  .mono-stagger > *:nth-child(3){animation-delay:calc(var(--mono-stagger-step)*2)}
  .mono-stagger > *:nth-child(4){animation-delay:calc(var(--mono-stagger-step)*3)}
  .mono-stagger > *:nth-child(5){animation-delay:calc(var(--mono-stagger-step)*4)}
  .mono-stagger > *:nth-child(6){animation-delay:calc(var(--mono-stagger-step)*5)}
  .mono-stagger > *:nth-child(7){animation-delay:calc(var(--mono-stagger-step)*6)}
  .mono-stagger > *:nth-child(8){animation-delay:calc(var(--mono-stagger-step)*7)}
  .mono-stagger > *:nth-child(n+9){animation-delay:calc(var(--mono-stagger-step)*8)}

  /* 3 · TRANSITION */
  .mono-sheet       { animation: mono-sheet-up var(--mono-dur-sheet) var(--mono-spring-soft) both; }
  .mono-screen-push { animation: mono-screen-push var(--mono-dur-screen) var(--mono-ease-emph) both; }
  .mono-screen-pop  { animation: mono-screen-pop var(--mono-dur-screen) var(--mono-ease-emph) both; }
  /* Toast: asymmetric on purpose — arrives with an emphatic decelerate, leaves fast and gets out of
     the way. Same edge both ways. NOTE for the JS side: because these live inside the no-preference
     gate, under `reduce` they never run and `animationend` NEVER FIRES — the hide path must take a
     synchronous branch first or the toast is stranded on screen. */
  .mono-toast          { animation: mono-toast-in  var(--mono-dur-enter) var(--mono-ease-emph) both; }
  .mono-toast--exiting { animation: mono-toast-out var(--mono-dur-fast)  var(--mono-ease-exit) both; }

  /* Account dropdown — the FAST tier, on purpose. This is opened many times a day, and the rule for
     that frequency is near-imperceptible motion or none: 120ms is felt as "solid", not as "animated".
     Enter only, deliberately asymmetric: a menu you open all day must never feel like it lags on the
     way out, so close() stays an instant `hidden`. (The symmetric enter/exit rule is for toasts and
     sheets, which are dismissed content — not for high-frequency chrome.) */
  .account-menu__panel:not(.hidden) {
    animation: account-menu-in var(--mono-dur-fast) var(--mono-ease-emph) both;
  }

  /* 4 · AFFIRMATION (runs once) */
  .mono-stamp { animation: mono-stamp var(--mono-dur-spring) var(--mono-spring) both; }
  .mono-tick  { animation: mono-tick var(--mono-dur-spring) var(--mono-spring) both; }
  .mono-pulse { animation: mono-pulse-once 240ms var(--mono-spring) 1; }
  .mono-draw  { animation: mono-draw var(--mono-dur-draw) var(--mono-ease-emph) both; }

  /* The Figma success checkmark, played ONCE on entry (2.2s master clock is linear; the
     per-segment easing lives in the keyframes). Ring bounces, then tick draws; `both` holds the
     finished mark at rest. The resting (un-animated) path has no dasharray, so the BASE style is a
     complete tick — that's what reduced-motion lands on instantly (the reduce block neutralizes the
     animation) and what every paint holds after one 2.2s play. (No-JS still runs this CSS animation;
     a print fired mid-play is covered by the @media print reset below.) */
  .mono-check__ring { animation: mono-check-ring 2.2s linear 1 both; }
  .mono-check__draw { animation: mono-check-draw 2.2s linear 1 both; }
}

/* Print / PDF: snapshot the FINISHED mark, never a mid-draw frame — receipts and payout records
   must show the tick even if printed during the 2.2s play. */
@media print {
  .mono-check__ring,.mono-check__draw{ animation:none!important; visibility:visible!important; transform:none!important; }
}

@media (prefers-reduced-motion: reduce) {
  .mono-press,.mono-hover-lift,.mono-rise,.mono-fade,.mono-stagger>*,
  .mono-sheet,.mono-screen-push,.mono-screen-pop,
  .mono-stamp,.mono-tick,.mono-pulse,.mono-draw,
  .mono-check__ring,.mono-check__draw,
  .mono-toast,.mono-toast--exiting,
  .mono-tap,.mono-hover,.mono-nudge,.mono-flash,.mono-settle,.mono-coda{
    animation:none!important;transition:none!important;transform:none!important;
    opacity:1!important;stroke-dashoffset:0!important;visibility:visible!important;
  }
  .mono-underline::after{ transition:none!important; transform:scaleX(1)!important; }
}

/* ============================================================
   View Transitions (Turbo 8) — the signature shared-element morph between the marketplace
   grid and the PDP. Opted in per-page via <meta name="view-transition" content="same-origin">
   on browse index + show ONLY, so live turbo_stream refreshes elsewhere never crossfade.
   Turbo skips the whole transition under prefers-reduced-motion (so this block is moot then).
   Tuned to the Mono screen tier so the carry feels like the rest of the system, not a UA default.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(*) {
    animation-duration: var(--mono-dur-screen);
    animation-timing-function: var(--mono-ease-emph);
  }
}
