:root {
  --ink: #17211d;
  --muted: #68746e;
  --line: #dce4df;
  --surface: #fbfcfa;
  /* PROMPT-218 (QA-21 H/video #1 — Rail edge-fade magnify glitch):
     transparent companion to --surface for gradient interpolation
     that stays within the surface hue. The pre-PROMPT-218 rail-
     track edge-fade gradient interpolated from var(--surface) to
     rgba(255, 255, 255, 0) — at the midpoint that produced a muddy
     gray/brown stripe (the magnify-sliver artifact QA observed)
     because the gradient crossed through unrelated RGB values.
     Pairing each surface color with a same-hue fully-transparent
     companion makes the gradient smoothly fade WITHIN its own hue,
     eliminating the disorienting artifact. Same pattern applies in
     dark mode below. */
  --surface-fade-transparent: rgba(251, 252, 250, 0);
  /* PROMPT-225 (Dark Mode Google Sign-In Button Fix): brand-intent
     tokens for the .account-google-signin surface. These tokens are
     INTENTIONALLY repeated verbatim inside the dark @media block
     below — the Google sign-in button is a trust-critical brand
     affordance that opts OUT of the dark-mode cascade. The
     duplication makes that decision explicit to future audits
     (rather than being silent / oversight-shaped). PROMPT-120
     invariant preserved: no hardcoded white surface literal in
     CSS; every value flows through a token. */
  --google-signin-bg: #ffffff;
  --google-signin-bg-hover: #f5f8f5;
  --google-signin-ink: #17211d;
  --google-signin-border: rgba(23, 33, 29, 0.18);
  --google-signin-border-hover: rgba(23, 33, 29, 0.3);
  --panel: #ffffff;
  --green: #1f7a4d;
  --green-dark: #135633;
  --pepper: #d7332f;
  --gold: #c8862d;
  --mint: #dff3e7;
  --blue: #245c88;
  --shadow: 0 2px 8px rgba(23, 33, 29, 0.06);

  /* PROMPT-120 (QA-12 §P2): semantic theming aliases. New code should
     prefer these names; legacy selectors continue to read the tokens
     above. The dark-mode block below redefines the legacy names so
     existing usage flips automatically without per-selector overrides. */
  --bg-primary: var(--surface);
  --bg-secondary: #f3f6f3;
  --surface-primary: var(--panel);
  --surface-secondary: #f7f9f7;
  --text-primary: var(--ink);
  --text-secondary: var(--muted);
  --border-primary: var(--line);
  --accent-primary: var(--green);
  --accent-primary-strong: var(--green-dark);

  /* PROMPT-120: sidebar palette. The sidebar is intentionally dark
     even in light mode for visual hierarchy; the dark-mode block
     tunes it slightly so the sidebar still reads as "elevated chrome"
     against the dark surface. */
  --sidebar-bg: #14231b;
  --sidebar-text: #f6fff9;
  --sidebar-muted: #b7c7bd;
  --sidebar-divider: rgba(255, 255, 255, 0.1);
  --sidebar-hover: rgba(255, 255, 255, 0.08);

  /* PROMPT-120: tint tokens for decorative chip / panel surfaces.
     Dark mode redefines these as low-opacity overlays so they read as
     calm tags / soft panels instead of glaring pastel rectangles. */
  --tint-mint: #eef5f1;
  --tint-mint-strong: #f3fbf6;
  --tint-warm: #fff2e0;
  --tint-blue: #f0f4fa;
  --tint-cool: #eef3f8;
  --tint-cream: #fffef5;
  --tint-bg: #f5f8f5;
  --tint-bg-soft: #fbfdfb;

  /* PROMPT-124 (QA-13 §10 + §16): motion + tactile primitives. Used
     by feed cards, buttons, and chips so hover / press feedback feels
     like Arc-soft + Linear-precise + Nintendo-tactile rather than
     "default browser." Calibrated to be perceptible but never
     distracting. Any additional motion needs (drawer slide, page
     transitions, etc.) are out of scope here — a future PROMPT can
     add `--motion-slow` + a transitions token if needed. */
  --motion-fast: 120ms;
  --motion-base: 160ms;
  --motion-slow: 220ms;
  --ease-soft: cubic-bezier(0.32, 0.72, 0, 1);
  --press-scale: 0.985;

  /* PROMPT-134 (QA-13 phase 3): motion tokens for ambient + insert
     interactions. Calibrated for "biological / ambient /
     infrastructural" — never aggressive, never gaming-energy.
     prefers-reduced-motion overrides each. */
  --motion-insert-fast: 120ms;
  --motion-insert-base: 200ms;
  --motion-pulse: 2400ms;
  --ease-breathe: cubic-bezier(0.45, 0, 0.55, 1);
  --freshness-warm: rgba(200, 134, 45, 0.18);

  /* PROMPT-127 (QA-13 §9 + §15): typography tokens. Body keeps the
     existing system stack (no external font, no SRI burden, no
     CSP exposure). Display stack starts with `ui-sans-serif` —
     modern OS-aware mapping (SF Pro Display on macOS/iOS, Segoe UI
     on Windows, Roboto on Android) — so headings feel one tier
     more intentional than the running text without committing to a
     proprietary face. Numerics use the same body face but with
     `tabular-nums` opted in via the per-selector
     `font-variant-numeric` rule established in PROMPT-124. */
  --font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-display: ui-sans-serif, "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  --type-xs: 0.72rem;
  --type-sm: 0.82rem;
  --type-base: 0.95rem;
  --type-md: 1rem;
  --type-lg: 1.18rem;
  --type-xl: 1.4rem;
  --type-2xl: 1.8rem;
  --type-display: clamp(1.4rem, 2.4vw, 2.2rem);

  --tracking-tight: -0.015em;
  --tracking-snug: -0.005em;
  --tracking-normal: 0em;
  --tracking-wide: 0.04em;

  --leading-tight: 1.1;
  --leading-snug: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.65;

  /* PROMPT-142 (QA-13 phase 12 — Shelf Rhythm + Spatial Pacing):
     semantic spacing tokens that drive cinematic vertical cadence
     across rails. Each tier maps to a cadence class consumed by
     .rail-section--<cadence>. The values establish a clear pressure
     hierarchy:
       tight     — utility / dense rails
       normal    — default rail spacing (matches the prior 36px gap)
       breathe   — emotional peak rails (Featured Grails, etc.)
       hero      — storefront window only
     Mobile values override these inside the 768px media query so
     pacing scales down without changing the semantic vocabulary. */
  --rail-gap-tight:   20px;
  --rail-gap-normal:  36px;
  --rail-gap-breathe: 56px;
  --rail-gap-hero:    72px;
}

/* PROMPT-120 (QA-12 §P2): respect OS-level dark-mode preference via
   prefers-color-scheme. The dark palette is calm, infrastructure-grade,
   and deliberately avoids pure black / saturated gradients / neon
   accents / cyberpunk aesthetics — the goal is "early marketplace
   infrastructure at night," not a gaming or crypto UI.

   No JavaScript is involved — the entire theme switch happens via
   CSS variable redefinition so the UI updates without re-rendering
   and without violating the PROMPT-110 CSP (no inline style injection,
   no runtime style mutation, no stylesheet hot-swap).

   Light mode remains the default visual identity. Dark mode is
   foundation-only: a manual theme toggle, persisted preference, or
   per-component dark-only polish are all out of scope (see PROMPT-120
   §out-of-scope). */
@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e6ecea;
    --muted: #9aa6a0;
    --line: #2b3733;
    --surface: #0f1714;
    /* PROMPT-218: dark-mode surface-fade-transparent companion.
       Same hue as --surface above with fully transparent opacity
       so the rail edge-fade gradient stays within dark green tones
       instead of passing through brownish gray-mud at the midpoint. */
    --surface-fade-transparent: rgba(15, 23, 20, 0);
    /* PROMPT-225: Google sign-in surface OPTS OUT of the dark
       cascade. Tokens repeat their :root (light-mode) values
       verbatim here — the duplication is intentional and
       documents the brand-intent theme-independence. Future
       audits SHOULD NOT collapse these into a single :root-only
       declaration; the explicit repeat is what guarantees the
       button stays white when prefers-color-scheme: dark
       activates. */
    --google-signin-bg: #ffffff;
    --google-signin-bg-hover: #f5f8f5;
    --google-signin-ink: #17211d;
    --google-signin-border: rgba(23, 33, 29, 0.18);
    --google-signin-border-hover: rgba(23, 33, 29, 0.3);
    --panel: #1a2420;
    --green: #4ec38a;
    --green-dark: #3aa974;
    --pepper: #ec5d59;
    --gold: #e0a04a;
    --mint: rgba(78, 195, 138, 0.14);
    --blue: #4d8fbe;
    --shadow: 0 2px 12px rgba(0, 0, 0, 0.45);

    --bg-secondary: #0a1311;
    --surface-secondary: #21302a;

    --sidebar-bg: #0a1411;
    --sidebar-text: #e6ecea;
    --sidebar-muted: #8a978f;
    --sidebar-divider: rgba(255, 255, 255, 0.06);
    --sidebar-hover: rgba(255, 255, 255, 0.05);

    --tint-mint: rgba(78, 195, 138, 0.10);
    --tint-mint-strong: rgba(78, 195, 138, 0.16);
    --tint-warm: rgba(224, 160, 74, 0.14);
    --tint-blue: rgba(77, 143, 190, 0.14);
    --tint-cool: rgba(77, 143, 190, 0.10);
    --tint-cream: rgba(224, 160, 74, 0.06);
    --tint-bg: rgba(255, 255, 255, 0.025);
    --tint-bg-soft: rgba(255, 255, 255, 0.018);
  }

  /* Soften the bright pastel status / role chips so they read as
     calm tags on the dark surface instead of glaring tickets. */
  .status-chip.status-proposed,
  .status-chip.status-pending,
  .status-chip.status-posted { background: rgba(77, 143, 190, 0.18); color: #9bcaf0; }
  .status-chip.status-contacted,
  .status-chip.status-matched { background: rgba(224, 160, 74, 0.18); color: #e8b970; }
  .status-chip.status-negotiating { background: rgba(236, 93, 89, 0.16); color: #f29a90; }
  .status-chip.status-failed { background: rgba(236, 93, 89, 0.18); color: #f29a90; }
  .role-badge.role-seller { background: rgba(224, 160, 74, 0.18); color: #e8b970; }
  .role-badge.role-admin { background: rgba(236, 93, 89, 0.18); color: #f29a90; }

  /* Source-label "observed" uses a warm cream tint in light mode;
     dim it for dark so it reads as a tag, not a beacon. */
  .source-label.observed { background: rgba(224, 160, 74, 0.18); color: #e8b970; }

  /* PROMPT-162 (QA-16 — Mobile Dark-Mode Respect): close the
     "page dark, form light" fracture QA-16 found on iOS. PROMPT-120
     redefined the surface tokens but left native form controls
     (input / select / textarea) at their browser defaults, which
     on iOS Safari ignore the page surface and paint white. The
     <meta name="color-scheme" content="light dark"> tag in
     index.html instructs the browser to honor the preference for
     scrollbars + native widgets; these rules harmonize the form
     surfaces themselves so what the user types into is part of
     the same dark room as the page around it.

     All three element types share the same calm dark surface
     (--panel) + ink text + line border, mirroring the listing
     form's existing inline-style frame. Placeholder text uses
     --muted so it reads as the calm hint register, not white-on-
     dark glare. */
  input,
  select,
  textarea {
    background: var(--panel);
    color: var(--ink);
    border-color: var(--line);
  }
  input::placeholder,
  textarea::placeholder {
    color: var(--muted);
  }

  /* Empty-state floor + market quiet surfaces inherit --ink + --muted
     via the PROMPT-154 / PROMPT-150 token-driven copy; verify here
     that the headline tone stays readable on dark by re-asserting
     the ink variable (defensive for browsers that resolve the
     cascaded variable at compute-time differently across the
     media-query boundary). */
  .empty-state-floor-line,
  .market-floor-intro .market-floor-line {
    color: var(--ink);
  }
  .empty-state-floor-sub,
  .market-floor-intro .eyebrow {
    color: var(--muted);
  }
}

/* PROMPT-124 (QA-13 §10): respect prefers-reduced-motion. Users with
   vestibular sensitivity or system-level reduced-motion preference
   get no transforms / no transitions. Functional state changes
   (hover color, focus outline) still apply — only the kinetic
   feedback is suppressed.
   PROMPT-229 §reduced-motion-extends: extended to suppress the
   :active press-scale on every PROMPT-229 surface so reduced-motion
   users get no kinetic compression feedback. The functional press
   (the action still fires) is unchanged — only the visual scale is
   suppressed. */
@media (prefers-reduced-motion: reduce) {
  .want-card,
  .event-card,
  .match-card,
  .primary-button,
  .ghost-button {
    transition: none;
  }
  .item-clickable:hover,
  .item-clickable:focus,
  .item-clickable:active,
  .record-clickable:hover,
  .record-clickable:focus,
  .record-clickable:active,
  .match-clickable:hover,
  .match-clickable:focus,
  .match-clickable:active,
  .primary-button:active,
  .ghost-button:active,
  .nav-item:active,
  .nav-hamburger:active,
  .mobile-signin-chip:active,
  .search-bar button:active,
  .search-suggestion-chip:active,
  .search-result-card:active,
  .collector-card:active,
  .listings-empty-continuation-link:active,
  .homepage-editorial-close-link:active,
  .account-drawer-link:active,
  .rail-scroll-prev:active,
  .rail-scroll-next:active,
  .post-want-dialog-close:active {
    transform: none;
  }
}

* {
  box-sizing: border-box;
}

/* PROMPT-070 (QA-08 §9): keyboard-accessible skip-to-content link.
   Visually hidden until it receives focus, then pinned to the top-left
   above the layout chrome. Activating it (Enter/Space) moves focus into
   the <main id="main"> region. */
.skip-link {
  position: absolute;
  left: 8px;
  top: -40px;
  z-index: 1000;
  padding: 8px 14px;
  background: var(--ink);
  color: #ffffff;
  text-decoration: none;
  font-weight: 700;
  border-radius: 6px;
  transition: top 120ms ease;
}

.skip-link:focus {
  top: 8px;
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

html {
  /* PROMPT-162 (QA-16 — Mobile Dark-Mode Respect): paint the
     root element with the same surface token the body uses so any
     momentary delay before <body> styles apply (or any pull-to-
     refresh overscroll on mobile Safari) shows the dark surface
     instead of the browser-default white. The token swaps under
     @media (prefers-color-scheme: dark) via the PROMPT-120 root-
     variable redefinition — no per-mode rule needed here. */
  background: var(--surface);
  overflow-x: hidden;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  overflow-x: hidden;
}

/* PROMPT-127 (QA-13 §9): display-family + tracking applied to all
   heading levels so the product reads as a considered typographic
   system instead of default-system text on a designed palette. The
   tightening is restrained — `tracking-snug` (-0.005em) on most
   heads, `tracking-tight` (-0.015em) only on the largest display
   sizes. Existing per-selector font-size + margin rhythm is
   preserved; only family + tracking + line-height are touched. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
}

h1 {
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
}

button,
input,
select,
textarea {
  font: inherit;
}

button {
  cursor: pointer;
}

.app-shell {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  padding: 20px;
  background: var(--sidebar-bg);
  color: var(--sidebar-text);
  display: flex;
  flex-direction: column;
  gap: 16px;
  overflow-y: auto;
}

.nav-close {
  display: none;
  align-self: flex-end;
  border: 0;
  background: transparent;
  color: var(--sidebar-muted);
  font-size: 1.1rem;
  padding: 4px 8px;
  line-height: 1;
}

.nav-close:hover { color: var(--sidebar-text); }

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  border-radius: 8px;
  padding: 4px;
  margin: -4px;
  transition: 120ms ease;
}

.brand:hover {
  background: var(--sidebar-hover);
}

.brand-mark {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--green), var(--pepper));
  color: white;
  font-weight: 800;
  font-size: 0.9rem;
}

.brand strong,
.brand span {
  display: block;
}

.brand span {
  margin-top: 2px;
  color: var(--sidebar-muted);
  font-size: 0.78rem;
}

.nav-list {
  display: grid;
  gap: 4px;
}

.nav-item {
  display: block;
  width: 100%;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 7px 10px;
  background: transparent;
  color: var(--sidebar-text);
  text-align: left;
  text-decoration: none;
  font-size: 0.9rem;
  opacity: 0.92;
}

.nav-item.active,
.nav-item:hover {
  background: var(--sidebar-hover);
  border-color: var(--sidebar-divider);
  opacity: 1;
}

.sidebar-panel {
  margin-top: auto;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--sidebar-hover);
  border: 1px solid var(--sidebar-divider);
}

.sidebar-panel strong {
  display: block;
  margin-top: 6px;
  font-size: 1.6rem;
}

.sidebar-panel p {
  margin: 0;
  color: var(--sidebar-muted);
  font-size: 0.82rem;
}

/* --- mobile nav strip (hidden on desktop) --- */
.mobile-nav-strip {
  display: none;
}

/* --- nav overlay (mobile drawer backdrop) --- */
.nav-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 199;
  background: rgba(0, 0, 0, 0.45);
}

body.drawer-open .nav-overlay {
  display: block;
}

main {
  min-width: 0;
  padding: 18px 22px;
}

.topbar,
.section-heading,
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
}

.topbar {
  margin-bottom: 12px;
}

/* PROMPT-251 (QA-22 §B2): persistent partial-degradation banner.
   Calm muted register — does NOT use any panic / alert color. Sits
   directly under the topbar and never crowds the marketplace
   content. Hidden by default via the [hidden] attribute; the
   renderOutageBanner() helper toggles visibility based on the
   loadHealth state. Full-failure styling adds a slightly stronger
   border anchor but no red — calmer than transient toast text. */
.outage-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 12px;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--text);
  font-size: 0.92rem;
}

.outage-banner[hidden] {
  display: none;
}

.outage-banner[data-outage-scope="full"] {
  border-color: var(--ink);
}

.outage-banner-message {
  margin: 0;
  color: var(--muted);
  flex: 1 1 auto;
}

.outage-banner-retry {
  flex: 0 0 auto;
  font-size: 0.85rem;
  padding: 6px 12px;
  touch-action: manipulation;
}

.topbar h1 {
  max-width: 720px;
  margin: 4px 0 0;
  font-size: clamp(1.4rem, 2.8vw, 2.8rem);
  line-height: 0.95;
  letter-spacing: 0;
}

.topbar-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* PROMPT-161 (QA-16 closure — Eyebrow Register Cleanup): the base
   eyebrow is the canonical structural label across the rail-native
   environment — used for ROUTE PAGES (Item Page / Matches /
   Listings / Account / Market Floor / Results / etc.) and SECTION
   HEADERS within routes (Photos / New Listing / Active Wants /
   Marketplace listings / Buyer Side / Seller Side / Notes / etc.).
   It belongs in the calm grey register: structural, navigational,
   informational. No alarm-state energy.

   The pre-PROMPT-161 base color was `var(--pepper)` (red), which
   collided with the destructive register (validation errors,
   sign-out, irreversible confirmations). That misuse made the
   collector floor read as alarm-state by default — every route
   label, every section header, every empty-state observation
   shouted in red. PROMPT-161 reassigns the base to `var(--muted)`
   so the eyebrow speaks in the calm structural register and the
   `--pepper` token is reserved for actual consequence.

   Semantic register (governance contract, not enforced by code):
     `.eyebrow`                    → grey  (route + section labels)
     `.item-pedestal-eyebrow`      → world-accent (editorial framing)
     `.match-lane-eyebrow`         → grey
     `.market-floor-intro .eyebrow`→ grey
     `--pepper`                    → red — destructive ONLY
                                       (.msg-error / .msg-send-error /
                                       .image-uploader-status-error)

   The variant classes above predate PROMPT-161 and already use
   their semantically-correct colors; the only change PROMPT-161
   makes is the base. No new tokens. No new abstractions. */
.eyebrow {
  margin: 0;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-xs);
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* PROMPT-127 (QA-13 §9 + §14): numeric figure utility. Aliases the
   per-selector tabular-nums established in PROMPT-124 into a single
   reusable class. New surfaces can opt in by adding `class="numeric"`
   without restating the font-feature-settings. */
.numeric,
.tabular-nums {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* PROMPT-138 (QA-13 Phase 8 — Cinematic Homepage Conversion): the
   onboarding hero-band, hero-copy, market-status pill, and
   signal-grid explainer tiles were retired with the homepage rail-
   native conversion. The .signal-card primitive remains because it
   is reused inside profile views (account stats). The .intake-panel
   and .feed-panel surfaces remain for non-homepage flows. Surface
   tokens stay shared so dark-mode discipline is unaffected. */
.signal-card,
.intake-panel,
.feed-panel,
.seller-card {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

.signal-card {
  min-height: 72px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.94);
  color: var(--ink);
}

.signal-card span,
.signal-card small {
  display: block;
  color: var(--muted);
  font-size: 0.78rem;
}

.signal-card strong {
  display: block;
  margin: 4px 0 2px;
  font-size: 1.5rem;
}

.view {
  display: none;
}

.view.active {
  display: block;
}

.section-heading {
  margin: 4px 0 12px;
}

.section-heading h2 {
  margin: 4px 0 0;
  font-size: 1.4rem;
}

/* PROMPT-177 (QA-17 §title-dedup — Page Header Title Deduplication):
   standard a11y utility class that visually hides an element while
   keeping it readable by assistive technology. Used on the route-
   header <h2>s of #itemDetail and #recordDetail where the cinematic
   pedestal <h1> and the record-body <h3> already provide the
   primary visible title. The H2s remain in the DOM (so JS continues
   updating textContent for screen readers + future aria-labelledby
   references) but disappear from the visual flow — no duplicate
   title in close proximity.

   Standard clip-rect + absolute-position pattern; no display: none
   because that would also hide from AT. Reusable utility: any
   future deduplication that needs to keep screen-reader context
   alive can apply this class. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.section-heading > p {
  max-width: 560px;
  margin: 0;
  color: var(--muted);
}

.market-layout {
  display: grid;
  grid-template-columns: minmax(280px, 400px) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}

/* PROMPT-138 → PROMPT-149: the .market-layout--rail-native modifier
   was retired with the §H1 listing-flow conversion. The homepage
   no longer hosts the inline Post Want form (it lives in a native
   <dialog> now); the wrapper was the form's only consumer. */

.signals-layout {
  display: grid;
  grid-template-columns: minmax(300px, 0.95fr) minmax(300px, 1.05fr);
  gap: 18px;
  align-items: start;
}

/* PROMPT-150 (QA-15 — Market World Conversion): the .insight-strip
   count-tile dashboard pattern was retired with the /market rail-
   native conversion. Tiles ("MARKET POSTS / 4" + "BUYER WANTS /
   10") are gone; the route now renders editorial shelves through
   renderRailHtml. Calm Market Floor intro replaces the old
   section-heading explainer. */
.market-floor-intro {
  margin: 0 0 18px;
  padding: 0 2px;
}

.market-floor-intro .eyebrow {
  margin: 0;
  letter-spacing: var(--tracking-wide);
  color: var(--muted);
}

.market-floor-intro .market-floor-line {
  margin: 4px 0 0;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

/* PROMPT-238 §market-floor-pulse: calm fact-based summary line that
   renders directly below .market-floor-line on /market + /trending.
   Populated by buildFloorPulseText() — empty string when no record
   is in the 24h freshness window, real number otherwise. The
   :empty selector collapses the paragraph to zero visual presence
   when the text is empty so quiet floors stay quiet. Tabular
   numerics keep digit widths stable across renders. Same muted
   register as .rail-pulse for visual consistency. */
.market-floor-intro .market-floor-pulse {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 0.83rem;
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-snug);
}

.market-floor-intro .market-floor-pulse:empty {
  display: none;
}

.empty-state-market {
  padding: 28px 16px;
  text-align: center;
}

/* PROMPT-154 §6.4 (QA-16 closure): calm editorial fallback for the
   homepage discover floor when no rails resolve at boot. Mirrors
   the .empty-state-market quiet-floor pattern (PROMPT-150) so the
   collector environment never lies — it speaks plainly when there's
   nothing to show. No skeletons, no shimmer, no "for you" copy. */
.empty-state-floor {
  padding: 36px 16px 28px;
  text-align: center;
}

.empty-state-floor-line {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.empty-state-floor-sub {
  margin: 6px 0 0;
  font-size: var(--type-sm);
  color: var(--muted);
}

/* PROMPT-167 (QA-16 M5 — Homepage Editorial Close): restrained
   continuation cue after the homepage's final rail. Lives inside
   #marketplace so the .view-cascade hides it automatically on
   non-homepage routes — no per-route mount logic required. The
   top border + breathe-rhythm margin separate the close from the
   final rail without becoming a heavy footer divider. Calm muted
   text on hover shifts to ink — same gravity tier as the .inline-
   link family, NOT a filled CTA. Token-driven; no new palette. */
.homepage-editorial-close {
  margin-top: var(--rail-gap-breathe);
  padding: 36px 16px 28px;
  border-top: 1px solid var(--line);
  text-align: center;
}

.homepage-editorial-close-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 4px 6px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-base);
  letter-spacing: var(--tracking-snug);
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-soft);
}

.homepage-editorial-close-link:hover,
.homepage-editorial-close-link:focus {
  color: var(--ink);
  outline: none;
}

.homepage-editorial-close-link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 4px;
  border-radius: 4px;
}

.intake-panel,
.feed-panel {
  padding: 12px;
}

.field-group {
  display: grid;
  gap: 6px;
  margin-bottom: 12px;
}

label {
  color: var(--ink);
  font-size: 0.86rem;
  font-weight: 700;
}

input,
select,
textarea {
  width: 100%;
  min-height: 42px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 12px;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--type-base);
}

textarea {
  min-height: 150px;
  resize: vertical;
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--green);
  outline: 3px solid rgba(31, 122, 77, 0.14);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.primary-button,
.ghost-button {
  min-height: 40px;
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0 14px;
  font-weight: 700;
  transition:
    background-color var(--motion-fast) var(--ease-soft),
    border-color var(--motion-fast) var(--ease-soft),
    transform var(--motion-fast) var(--ease-soft);
}

.primary-button {
  background: var(--green);
  color: white;
}

.primary-button:hover {
  background: var(--green-dark);
}

/* PROMPT-124 §2: tactile press feedback. Settled compression on
   click, returns instantly. Mirrors physical button feel without
   bounce. */
.primary-button:active,
.ghost-button:active {
  transform: scale(var(--press-scale));
}

/* PROMPT-229 (Mobile Touch Feedback Pass): extend the calm
   tactile-press pattern to every tappable surface that previously
   only had :hover / :focus states. Pre-PROMPT-229, mobile users
   reported "dead taps" on these surfaces — the user touched the
   element, saw no visual change, then the action fired. Each
   :active here reuses the existing PROMPT-124 --press-scale token
   (0.985) so the compression feel is uniform across the entire
   marketplace. No transition declared on transform — the press
   snaps on touch-down and snaps back on release (instant tactile
   feedback, matching the PROMPT-124 "settled compression, returns
   instantly" pattern). All these :active rules are suppressed
   under prefers-reduced-motion via the extended @media block
   below (PROMPT-229 §reduced-motion-extends). */
.nav-item:active,
.nav-hamburger:active,
.mobile-signin-chip:active,
.search-bar button:active,
.search-suggestion-chip:active,
.search-result-card:active,
.collector-card:active,
.listings-empty-continuation-link:active,
.homepage-editorial-close-link:active,
.account-drawer-link:active,
.rail-scroll-prev:active,
.rail-scroll-next:active,
.post-want-dialog-close:active {
  transform: scale(var(--press-scale));
}

/* PROMPT-229: the rail-scroll prev/next buttons live inside
   .rail-track-wrapper with `transform: translateY(-50%)` for
   vertical centering. The :active scale must compose with that
   translate, otherwise the button jumps to the top of the wrapper
   on press. Override here to preserve the translateY. */
.rail-scroll-prev:active,
.rail-scroll-next:active {
  transform: translateY(-50%) scale(var(--press-scale));
}

.ghost-button {
  background: var(--panel);
  border-color: var(--line);
  color: var(--ink);
}

.ghost-button:hover {
  border-color: var(--green);
}

.full-width {
  width: 100%;
}

.want-list {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

/* PROMPT-124 (QA-13 §16): warmer empty states. Solid border instead of
   dashed reads less "form field," more "intentional pause." Slightly
   more padding so the copy can breathe; unchanged copy semantics. */
.empty-state {
  min-height: 72px;
  display: grid;
  place-items: center;
  gap: 4px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--muted);
  background: var(--tint-bg-soft);
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.5;
}

/* PROMPT-126 (QA-13 §11 + §15 + §16): rich empty-state variant for
   high-impact moments. Adds an inline-SVG illustration above the
   title/body/CTA so sparse marketplace moments feel intentional, not
   silent. The base `.empty-state` styles (border, background, color,
   centered grid) apply; this block layers structure + breathing room.
   Theme-aware: illustration uses currentColor + var(--muted), so dark
   mode flips automatically via PROMPT-120 tokens. */
.empty-state-rich {
  min-height: 0;
  padding: 28px 22px;
  gap: 10px;
  font-size: 1rem;
}

.empty-state-illustration {
  display: grid;
  place-items: center;
  width: 72px;
  height: 56px;
  color: var(--muted);
  opacity: 0.55;
  margin-bottom: 4px;
}

.empty-state-illustration svg {
  width: 100%;
  height: 100%;
  display: block;
}

.empty-state-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-md);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
}

.empty-state-body {
  margin: 0;
  max-width: 38ch;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.55;
}

.empty-state-cta {
  margin-top: 10px;
  font-size: 0.88rem;
}

/* Compact variant — used inside narrower panels (e.g. item-detail
   tab content) where the full vertical breathing room would crowd
   the surrounding layout. */
.empty-state-compact {
  padding: 16px 14px;
  gap: 6px;
}

.empty-state-compact .empty-state-illustration {
  width: 56px;
  height: 42px;
  margin-bottom: 0;
}

.empty-state-compact .empty-state-title {
  font-size: 0.96rem;
}

.empty-state-compact .empty-state-body {
  font-size: 0.86rem;
}

.want-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  transition:
    transform var(--motion-base) var(--ease-soft),
    box-shadow var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft);
  overflow: hidden;
  min-width: 0;
}

/* PROMPT-125 (QA-13 §6 + §15): card media region. When present, it
   spans the full row above the existing 2-column body+price grid so
   the markup stays a single grid (no wrapper required) and cards
   without media render unchanged. The category glyph sits behind
   the image; if the image fails to load, the glyph reveals
   automatically via the .card-img-broken class added by the global
   error listener in app.js. */
.want-card > .card-media,
.event-card > .card-media {
  grid-column: 1 / -1;
}

.card-media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--tint-bg);
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 6px;
  border: 1px solid var(--line);
}

/* Variable visual rhythm — glyph-only cards are more compact so the
   feed's eye travel naturally pauses on cards with real images. */
.card-media-glyph {
  aspect-ratio: 5 / 2;
  background: var(--tint-bg-soft);
}

.card-glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  opacity: 0.55;
}

/* PROMPT-187 §card-atmosphere: cardMediaHtml emits `data-world` on the
   .card-media element, derived from worldForCategory(record.category).
   The middle layer of the fallback hierarchy — category/world-aware
   atmosphere — picks up the existing PROMPT-137 token palette so the
   glyph-only fallback varies by category instead of showing a single
   generic gray. With-image cards still render their <img> on top; the
   world tokens only become visible when the image is missing or fails
   to load (card-img-broken). All values reference EXISTING PROMPT-120
   tints — no new colors introduced. Dark-mode flips automatically via
   the token cascade. */
.card-media[data-world] {
  --world-surface: var(--tint-bg);
  --world-accent:  var(--muted);
}
.card-media[data-world="tcg-world"]         { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.card-media[data-world="sneaker-world"]     { --world-surface: var(--tint-cool);  --world-accent: var(--blue);  }
.card-media[data-world="watch-world"]       { --world-surface: var(--tint-blue);  --world-accent: var(--blue);  }
.card-media[data-world="comic-world"]       { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.card-media[data-world="game-world"]        { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.card-media[data-world="vinyl-world"]       { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.card-media[data-world="coin-world"]        { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.card-media[data-world="funko-world"]       { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.card-media[data-world="figure-world"]      { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.card-media[data-world="bag-world"]         { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.card-media[data-world="jewelry-world"]     { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.card-media[data-world="memorabilia-world"] { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.card-media[data-world="art-world"]         { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.card-media[data-world="general-world"]     { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }

/* The glyph-only fallback variant adopts the world surface so the
   no-image case stops being a single gray neutral. The with-image
   variant keeps the neutral tint-bg behind so real images aren't
   competing with a colored background. */
.card-media-glyph[data-world] {
  background: var(--world-surface);
}
.card-media[data-world] .card-glyph {
  color: var(--world-accent);
}

.card-glyph svg {
  width: 38%;
  max-width: 56px;
  height: auto;
  display: block;
}

.card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* PROMPT-201 (Critical mobile regression fix): defense-in-depth
     bleed containment. The absolute positioning already constrains
     the image to its .card-media parent (which declares overflow:
     hidden); these belt-and-suspenders properties guarantee the
     image cannot exceed its container regardless of intrinsic
     dimensions OR if a future markup change moves the image out of
     absolute positioning. `display: block` removes the inline-image
     baseline gap; `max-width: 100%` caps natural width below
     parent; `object-fit: cover` ensures the aspect-ratio is honored
     by cropping the longer dimension. */
  max-width: 100%;
  display: block;
  object-fit: cover;
  background: transparent;
  transition: opacity var(--motion-base) var(--ease-soft);
  z-index: 1;
  /* PROMPT-228 (Mobile Rail Physics Pass): suppress iOS Safari's
     native image-drag-preview that initialized whenever a swipe
     gesture began on a card's <img>. The drag-preview behavior
     was the primary "drag-lock" / "sticky" feel collectors
     reported on horizontal rails — the OS captured the touch
     for image drag-and-drop instead of routing it to the rail's
     scroll surface. -webkit-user-drag: none disables the native
     drag preview; user-drag: none is the spec'd companion
     (limited browser support today, defensive). user-select:
     none + -webkit-touch-callout: none echo the rail-card
     defenses against text-selection / long-press interference
     during fast horizontal flicks. The image remains tappable
     (cursor: pointer / click handlers on the wrapping element
     still fire) — only the OS-level drag affordance is
     suppressed. */
  -webkit-user-drag: none;
  user-drag: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.card-img.card-img-broken {
  display: none;
}

.item-clickable,
.record-clickable,
.match-clickable {
  cursor: pointer;
}

.item-clickable:hover,
.item-clickable:focus,
.record-clickable:hover,
.record-clickable:focus,
.match-clickable:hover,
.match-clickable:focus {
  border-color: rgba(31, 122, 77, 0.4);
  box-shadow: 0 6px 18px rgba(23, 33, 29, 0.09);
  outline: none;
  transform: translateY(-1px);
}

/* PROMPT-124 §2: tactile press feedback. Tiny scale gives the card
   a "settled" feel under click/tap that mirrors physical pressing —
   no bounce, no toy motion. Returns instantly on release. */
.item-clickable:active,
.record-clickable:active,
.match-clickable:active {
  transform: translateY(0) scale(var(--press-scale));
  box-shadow: 0 2px 6px rgba(23, 33, 29, 0.06);
  transition-duration: var(--motion-fast);
}

.want-card h4 {
  margin: 0 0 4px;
  font-size: 1rem;
  line-height: 1.35;
  letter-spacing: -0.005em;
}

.inline-link {
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  font-weight: 700;
  text-align: left;
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-decoration-color: rgba(36, 92, 136, 0.22);
  text-underline-offset: 3px;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Item-name button on want/offer cards — explicit 44px touch target.
   inline-flex keeps the layout compact on desktop while ensuring tap
   zone meets WCAG 2.5.5 AA on mobile. */
.inline-link.item-name-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 4px 0;
}

.inline-link:hover,
.inline-link:focus {
  color: var(--blue);
  outline: none;
  text-decoration-color: var(--blue);
}

.meta-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  min-width: 0;
}

/* PROMPT-128 (QA-13 §5 + §6): editorial secondary metadata strip.
   Replaces the chip-wall in feed cards with a calmer dot-separated
   inline text row. Primary intent (status-chip / source-label /
   view-affordance / group-count-chip) remains in the .meta-row
   above and keeps its full visual weight; this strip carries the
   lower-priority context — category, condition, urgency, location,
   source platform, age — as quiet inline text.

   The middot separator is rendered via ::after on every child
   except the last, so the markup is just a flat list of <span>s.
   No stranded leading or trailing dots, no JS, no separator nodes
   in the DOM. */
.meta-strip {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0;
  margin-top: 6px;
  font-family: var(--font-body);
  font-size: var(--type-sm);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  line-height: var(--leading-snug);
}

.meta-strip > * {
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
  padding: 0;
  background: transparent;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: var(--tracking-snug);
}

.meta-strip > *:not(:last-child)::after {
  content: "·";
  margin: 0 8px;
  color: var(--line);
  font-weight: 700;
}

/* PROMPT-129 (QA-13 §13 + §15 + §18): calm provenance/source mark for
   feed cards. Sits BETWEEN the primary intent .meta-row and the
   tertiary .meta-strip — emotional grounding context, not metadata
   spam. Token-driven so dark mode flips automatically (PROMPT-120
   invariant); typography reads from PROMPT-127 tokens. No fills,
   no shields, no checkmarks — just a small inline-SVG icon + label.

   Tone is selected via a `data-provenance-tone` attribute set by
   the renderer in src/provenance.js. Each tone uses an existing
   PROMPT-120 tint background + an existing accent text color so the
   palette stays small and theme-coherent. */
.provenance-mark {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: var(--type-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  line-height: 1.2;
  background: var(--tint-bg);
  color: var(--muted);
  transition: opacity var(--motion-fast) var(--ease-soft);
}

.provenance-mark:hover {
  opacity: 0.92;
}

.provenance-mark[data-provenance-tone="native"] {
  background: var(--tint-mint);
  color: var(--green-dark);
}
.provenance-mark[data-provenance-tone="observed"] {
  background: var(--tint-warm);
  color: var(--gold);
}
.provenance-mark[data-provenance-tone="external"] {
  background: var(--tint-blue);
  color: var(--blue);
}
.provenance-mark[data-provenance-tone="auction"] {
  background: var(--tint-cream);
  color: var(--gold);
}
.provenance-mark[data-provenance-tone="estate"] {
  background: var(--tint-bg);
  color: var(--muted);
}

.provenance-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  opacity: 0.85;
}

.provenance-label {
  white-space: nowrap;
}

/* PROMPT-190 (QA-18 §provenance-expansion): inline relative-time
   span appended to the provenance mark when a record carries a usable
   timestamp (observedAt / firstSeenAt / createdAt). Calm subordinate
   typography — the time reads as supporting metadata, never as a
   competing chip. The "·" separator is a CSS pseudo-element so it
   doesn't appear in screen-reader output, where the time chip already
   carries an explicit aria-label. */
.provenance-time {
  white-space: nowrap;
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}
.provenance-time::before {
  content: "·";
  margin: 0 6px 0 0;
  opacity: 0.7;
}

/* PROMPT-190: "Linked account" indicator. Factual: this signals only
   that the record was posted by an authenticated Poblaz account. It
   is NOT a verification, an identity check, or an authenticity
   guarantee. Tone stays subordinate to the parent provenance chip —
   no shield icons, no green checkmarks, no "✓ Verified" theatre. */
.provenance-linked {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: var(--tint-bg);
  color: var(--muted);
  border: 1px solid var(--line);
}
.provenance-linked::before {
  content: "·";
  margin-right: 5px;
  opacity: 0.7;
  color: var(--muted);
  font-weight: 500;
}
/* Native-tone provenance gets a slightly warmer linked chip so the
   "Buyer posted · Linked" / "Listed directly · Linked" pairing reads
   as one coherent unit rather than two competing pills. */
.provenance-mark[data-provenance-tone="native"] .provenance-linked {
  background: var(--tint-mint-strong, var(--tint-mint));
  color: var(--green-dark);
  border-color: transparent;
}

/* PROMPT-124 §4: metadata chips de-emphasized. Drop weight from 700 to
   600 and lean text color toward muted so "primary intent" chips
   (status-chip / source-label / view-affordance) carry the visual
   weight, and these secondary metadata chips read as supporting
   detail rather than competing visual lozenges. */
.tag {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 7px;
  border-radius: 999px;
  background: var(--tint-mint);
  color: var(--text-secondary);
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
  min-width: 0;
}

.source-label {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 7px;
  border-radius: 6px;
  font-size: 0.73rem;
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.source-label.native {
  background: var(--mint);
  color: var(--green-dark);
}

.source-label.observed {
  background: var(--tint-warm);
  color: #8a5318;
}

.source-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--tint-blue);
  color: var(--blue);
  font-size: 0.75rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}

.source-link:hover {
  text-decoration: underline;
}

.view-affordance {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 7px;
  border-radius: 999px;
  background: var(--tint-cool);
  color: var(--blue);
  font-size: 0.73rem;
  font-weight: 700;
}

/* PROMPT-124 §3: prices visually anchor cards. Tabular numerics so
   the digits line up across rows — premium feel, not default text. */
.price-block {
  min-width: 88px;
  text-align: right;
  flex-shrink: 0;
}

.price-block strong {
  display: block;
  font-size: 1.18rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--ink);
}

.price-block span {
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* PROMPT-139 (QA-13 phase 9 — Trending World Conversion): retired
   the .heatmap-toolbar / .pill / .heatmap-grid / .heat-tile rules
   when /trending was converted to a fully rail-native editorial
   discovery surface. The 4 metric pills (volume / pressure /
   momentum / urgency) and the saturated-gradient tile system both
   carried leaderboard / casino energy that conflicts with the
   collector-environment thesis. The new surface reuses
   .discover-rails + .rail-section + .rail-card primitives from
   PROMPT-131 / 132 / 137 / 138 — no parallel design system. */
.empty-state-trending {
  padding: 28px 16px;
  text-align: center;
}

/* PROMPT-140 (QA-13 phase 10 — Item Page Cinematic Conversion):
   item pages render as a cinematic collector pedestal + rail-stack.
   Retired: .item-mobile-snapshot / .item-snapshot-* / .snapshot-stat
   / .snapshot-chip / .item-mobile-tabs / .item-tab-* / .item-overflow
   / .item-show-more / .item-desktop-panels / .item-summary-grid /
   .item-aliases / .item-events-layout — those carried database-row
   / dashboard-detail energy that conflicts with the collector-
   pedestal thesis. The new surface reuses .card-media (PROMPT-125),
   .provenance-mark (PROMPT-129), .rail-section (PROMPT-131), and
   the per-world token cascade (PROMPT-137) — no parallel design
   system. */
.item-detail-shell {
  display: grid;
  gap: 32px;
}

.item-pedestal {
  position: relative;
  display: block;
  padding: 28px 0 12px;
  /* PROMPT-137: pedestal carries data-world so descendant
     atmosphere reads from the world tokens. Same cascade contract
     as .rail-section[data-world]. */
  --world-surface: var(--tint-bg);
  --world-border:  var(--line);
  --world-accent:  var(--muted);
}

.item-pedestal[data-world="tcg-world"]         { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.item-pedestal[data-world="sneaker-world"]     { --world-surface: var(--tint-cool);  --world-accent: var(--blue);  }
.item-pedestal[data-world="watch-world"]       { --world-surface: var(--tint-blue);  --world-accent: var(--blue);  }
.item-pedestal[data-world="comic-world"]       { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.item-pedestal[data-world="game-world"]        { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.item-pedestal[data-world="vinyl-world"]       { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.item-pedestal[data-world="coin-world"]        { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.item-pedestal[data-world="funko-world"]       { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.item-pedestal[data-world="figure-world"]      { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.item-pedestal[data-world="bag-world"]         { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.item-pedestal[data-world="jewelry-world"]     { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.item-pedestal[data-world="memorabilia-world"] { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.item-pedestal[data-world="art-world"]         { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.item-pedestal[data-world="general-world"]     { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }

.item-pedestal-stage {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 32px;
  align-items: start;
}

.item-pedestal-media {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  /* The pedestal's media surface frames a single object — give it
     a generous floor so collector photography reads at gallery
     scale. The .card-media primitive inside handles its own aspect
     ratio + glyph fallback; we only set the minimum height. */
  min-height: 320px;
  background: var(--world-surface);
}

.item-pedestal-media .card-media {
  width: 100%;
  height: 100%;
  min-height: 320px;
  margin: 0;
}

/* PROMPT-239 §pedestal-contain-for-accuracy: the cinematic pedestal
   is the surface where collectors evaluate condition before deciding
   to message a seller. Rails / want cards / search previews keep
   object-fit: cover (PROMPT-201 base rule) for grid rhythm — uniform
   crops, no awkward letterbox bars per thumbnail. The pedestal earns
   a different treatment: switch to object-fit: contain so portrait
   uploads aren't cropped top + bottom and landscape uploads aren't
   cropped left + right. Condition-critical details (card edges,
   comic spines, sneaker stitching, watch crown) stay visible.
   Letterbox bars (when the image's intrinsic ratio doesn't match
   the pedestal's 4/3 desktop / 5/3 mobile aspect-ratio) reveal the
   existing [data-world] atmospheric background from PROMPT-187 —
   so the letterboxing reads as editorial framing, not as a flaw.
   No new CSS filters, no image alteration, no server-side
   processing. The original seller upload is preserved byte-for-byte
   on Supabase storage; only the DISPLAY mode changes at this single
   high-trust surface. Mobile + desktop both inherit the override
   because the .item-pedestal-media .card-img descendant selector
   applies regardless of viewport. PROMPT-201 base rule
   (object-fit: cover on .card-img) stays for every other surface;
   this rule is a scoped override, not a replacement. */
.item-pedestal-media .card-img {
  object-fit: contain;
}

.item-pedestal-meta {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 4px 0;
}

.item-pedestal-eyebrow {
  margin: 0;
  color: var(--world-accent);
  letter-spacing: var(--tracking-wide);
}

.item-pedestal-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-2xl);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.item-pedestal-price {
  margin: 0;
  font-size: var(--type-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--ink);
  letter-spacing: var(--tracking-snug);
}

.item-pedestal-activity {
  margin: 0;
  font-size: var(--type-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: var(--tracking-snug);
}

.item-pedestal .provenance-mark {
  margin: 0;
}

.item-pedestal-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 10px;
}

.item-pedestal-secondary {
  /* Quiet ghost button for the optional "View N listings below"
     pedestal-jump action. Reuses the existing .ghost-button
     surface so no new primitive is introduced. */
  font-size: var(--type-sm);
}

/* §rarity-frame: featured-tier pedestal gets the same calm warm-
   pocket emphasis as PROMPT-133 featured cards — soft warm border,
   no glow, no shimmer. */
.item-pedestal--featured .item-pedestal-media {
  border: 1px solid rgba(200, 134, 45, 0.30);
}

.item-page-rails {
  /* The lower half of the item page IS a rail stack. Reuses the
     existing .discover-rails layout via class composition; this
     selector exists only as a scoping anchor for tests and
     potential future tweaks. */
  margin-top: 8px;
}

@media (max-width: 768px) {
  .item-pedestal {
    padding: 16px 0 8px;
  }

  .item-pedestal-stage {
    grid-template-columns: 1fr;
    gap: 18px;
  }

  .item-pedestal-media {
    min-height: 220px;
  }

  .item-pedestal-media .card-media {
    min-height: 220px;
  }

  .item-pedestal-title {
    font-size: var(--type-xl);
  }
}

.event-list {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

.event-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  transition:
    transform var(--motion-base) var(--ease-soft),
    box-shadow var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft);
}

.event-card strong {
  display: block;
  margin-bottom: 3px;
  overflow-wrap: anywhere;
}

.event-card p {
  margin: 0 0 3px;
  color: var(--muted);
  font-size: 0.83rem;
  overflow-wrap: anywhere;
}

.event-card > span {
  color: var(--muted);
  font-size: 0.76rem;
  font-weight: 700;
  white-space: nowrap;
}

.record-detail-shell {
  display: grid;
  gap: 16px;
}

.match-detail-shell {
  display: grid;
  gap: 14px;
}

.record-hero {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

.record-hero h3 {
  margin: 5px 0 5px;
  font-size: clamp(1.1rem, 2.2vw, 1.6rem);
  line-height: 1.05;
  overflow-wrap: anywhere;
}

.record-hero > strong {
  font-size: 1.3rem;
  white-space: nowrap;
  flex-shrink: 0;
}

.record-spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 10px;
}

.record-spec-grid article {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

.record-spec-grid span {
  display: block;
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 800;
  text-transform: uppercase;
}

.record-spec-grid strong {
  display: block;
  margin-top: 3px;
  overflow-wrap: anywhere;
  font-size: 0.95rem;
}

.record-link-row,
.record-notes {
  margin: 0;
}

.record-notes {
  margin-top: 8px;
  color: var(--muted);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

/* --- match detail compact layout --- */
.match-deal-header {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.match-status-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 8px;
}

.match-deal-name {
  margin: 0 0 6px;
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  line-height: 1.1;
  overflow-wrap: anywhere;
}

.match-price-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.price-chip {
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  min-width: 0;
}

.price-chip span {
  display: block;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 800;
  text-transform: uppercase;
  white-space: nowrap;
}

.price-chip strong {
  display: block;
  font-size: 1.05rem;
  white-space: nowrap;
}

.price-chip-accent {
  background: var(--mint);
  border-color: rgba(31, 122, 77, 0.28);
}

.price-chip-accent strong {
  color: var(--green-dark);
}

.match-sides-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.match-side-card {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  min-width: 0;
}

.match-side-card strong {
  display: block;
  margin: 4px 0 6px;
  overflow-wrap: anywhere;
}

.match-side-card p {
  margin: 0;
  color: var(--muted);
  font-size: 0.83rem;
  overflow-wrap: anywhere;
}

/* PROMPT-236 §match-side-profile-link: the "View @handle's profile →"
   affordance under each match side. Calm muted line spaced just
   under the existing meta-row so it reads as a follow-up trust path
   rather than a primary action. Reuses .inline-link for the button
   tone (no new color tokens). Sits inside a <p class="match-side-
   profile-link"> so the .match-side-card p { color: var(--muted) }
   rule above paints the wrapper if anything outside the inline-link
   leaks color. The button itself inherits inline-link styling. */
.match-side-card .match-side-profile-link {
  margin-top: 8px;
  font-size: 0.83rem;
}

.match-notes-block {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

/* PROMPT-240 §match-detail-guidance: calm muted etiquette line that
   sits below the match-sides-row + optional match-notes-block and
   above the match-meta-row. Centered max-width reading column keeps
   the helper line intimate — it's editorial guidance, not chrome.
   var(--muted) + display-family + smaller font size place it in the
   same visual register as the .match-side-card paragraphs above so
   the eye reads it as continuation, not as a new chrome layer. */
.match-detail-guidance {
  margin: 12px 0 0;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.86rem;
  line-height: var(--leading-snug);
  max-width: 640px;
}

/* PROMPT-256 (QA-24 messaging-ignition): match-detail "Open
   conversation" CTA. Calm primary action sized to the same register
   as the existing match-detail-guidance helper above — the helper
   line + CTA read as one block. No urgency styling. */
.match-conversation-cta {
  margin: 16px 0 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  max-width: 640px;
}
.match-conversation-cta-helper {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}
.match-conversation-cta .primary-button {
  touch-action: manipulation;
}

/* PROMPT-258 (QA-24 follow-up lifecycle-closure): "Mark closed"
   action block. Calm ghost-button — deliberately quieter than the
   primary Open-conversation CTA above it, because closure is a
   terminal action collectors should reach for thoughtfully, not a
   primary call to action. Same muted helper-line register as the
   Open-conversation helper above. */
.match-close-action {
  margin: 16px 0 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  max-width: 640px;
}
.match-close-helper {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}
.match-close-action .ghost-button {
  touch-action: manipulation;
}

/* PROMPT-256: calm presence indicator on the Messages tab button.
   A small muted dot — NOT a red unread badge. Hidden by default;
   loadMessagesPanel unhides it when conversations exist. No
   animation, no pulse, no count. */
/* PROMPT-256 → PROMPT-275 evolution: the calm Messages-tab indicator
   was a 6px muted dot in PROMPT-256 (binary "conversations exist"
   signal). PROMPT-275 turns it into a calm numeric count when the
   needs-attention summary returns > 0; renders nothing at 0.
   Muted color (var(--muted)), normal weight, NOT red, NO animation,
   NO pulse. Ledger awareness register — never dopamine. */
.account-tab-indicator {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  min-width: 18px;
  height: 18px;
  line-height: 18px;
  border-radius: 9px;
  background: var(--line);
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 500;
  text-align: center;
  vertical-align: middle;
}
.account-tab-indicator[hidden] {
  display: none;
}
/* Empty-content fallback: if a future caller leaves textContent
   empty (legacy PROMPT-256 dot mode), collapse back to a 6px dot
   so the visual contract remains backwards-compatible. */
.account-tab-indicator:empty {
  width: 6px;
  min-width: 0;
  height: 6px;
  padding: 0;
  background: var(--muted);
  border-radius: 50%;
}

.match-meta-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

/* --- status chip --- */
.status-chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: var(--tracking-snug);
  text-transform: capitalize;
}

/* PROMPT-104 (QA-11 §M6): grouped-count chip on want / signal cards.
   Renders as `×3 active wants` or `Referenced in 4 reddit posts`
   inline next to the item title. Visually subordinate to status chips
   so it reads as metadata, not status. */
.group-count-chip {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(31, 122, 77, 0.10);
  color: var(--green, #1f7a4d);
  font-size: 0.72rem;
  font-weight: 700;
  vertical-align: middle;
}

/* PROMPT-253 (QA-23): match status-chip CSS collapsed to two
   user-facing tones — Active (calm, in-progress) vs Closed (settled,
   neutral). Pre-PROMPT-253 the chip set carried six tones implying
   verified transaction states (accepted / completed / failed) that
   Poblaz cannot verify. The legacy classes (status-proposed /
   status-contacted / status-negotiating / status-accepted /
   status-completed / status-failed / status-matched) are preserved
   for non-match surfaces that may still emit them (want status,
   conversation status); the new status-active / status-closed
   classes are the canonical match-chip tones going forward. */
.status-chip.status-proposed,
.status-chip.status-pending,
.status-chip.status-posted,
.status-chip.status-active { background: #e6f0fe; color: #1a5fa0; }
.status-chip.status-contacted,
.status-chip.status-matched { background: #fff3dc; color: #7a4200; }
.status-chip.status-negotiating { background: #fdeee8; color: #8a3010; }
.status-chip.status-accepted { background: var(--mint); color: var(--green-dark); }
.status-chip.status-completed,
.status-chip.status-closed { background: var(--surface); color: var(--muted); border: 1px solid var(--line); }
.status-chip.status-failed { background: #fde8e8; color: #7a1a1a; }

/* PROMPT-141 → PROMPT-149: the kanban-era .pipeline + .pipeline-column
   surface was retired with the negotiation-gallery conversion; the
   literal "pipeline" class+id was finally retired by PROMPT-149's
   listing-flow conversion (residue cleanup). The JS injects the
   gallery + continuity markup into .match-gallery-host / #matchGallery.
   The new surface uses .match-gallery / .match-lane / .match-lane-* /
   .match-continuity primitives below. */
.match-gallery-host {
  display: block;
  padding: 0;
}

.signal-feed {
  margin-top: 18px;
}

.signal-list {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

/* --- match gallery (PROMPT-141) --- */

.match-gallery {
  display: flex;
  flex-direction: column;
  gap: 32px;
  margin-bottom: 32px;
}

.match-lane {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.match-lane-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 0 2px;
}

.match-lane-heading-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.match-lane-eyebrow {
  margin: 0;
  color: var(--muted);
  letter-spacing: var(--tracking-wide);
}

.match-lane-title {
  margin: 2px 0 0;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  color: var(--ink);
}

.match-lane-subtitle {
  margin: 2px 0 0;
  font-size: var(--type-sm);
  color: var(--muted);
  line-height: var(--leading-snug);
}

.match-lane-count {
  font-family: var(--font-body);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: var(--tracking-snug);
  align-self: center;
  flex-shrink: 0;
}

.match-lane-stack {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
  align-items: start;
}

.match-lane-empty {
  margin: 0;
  padding: 14px 16px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: var(--panel);
  color: var(--muted);
  font-size: var(--type-sm);
  font-style: italic;
  letter-spacing: var(--tracking-snug);
  grid-column: 1 / -1;
}

/* The .match-card sits inside .match-lane-stack. Existing card
   selector below preserves match-card-name / match-card-meta /
   match-card-price typography; the gallery just lays them out
   in a calm grid instead of a kanban column. */

/* --- match continuity rails (rail-native lower-half) --- */

.match-continuity {
  /* Reuses .discover-rails layout via class composition; this
     selector exists only as a scoping anchor for tests + potential
     future tweaks. */
  margin-top: 8px;
}

@media (max-width: 768px) {
  .match-gallery {
    gap: 24px;
  }
  .match-lane-stack {
    grid-template-columns: 1fr;
  }
}

.match-card {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 6px;
  padding: 8px 10px;
  border-radius: 10px;
  background: var(--panel);
  border: 1px solid var(--line);
  transition:
    transform var(--motion-base) var(--ease-soft),
    box-shadow var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft);
  min-width: 0;
}

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

.match-card-name {
  display: block;
  font-size: 0.88rem;
  font-weight: 700;
  margin-bottom: 4px;
  overflow-wrap: anywhere;
}

.match-card-meta {
  display: flex;
  gap: 5px;
  align-items: center;
  flex-wrap: wrap;
  font-size: 0.78rem;
}

.match-card-seller {
  color: var(--muted);
  overflow-wrap: anywhere;
  min-width: 0;
}

.match-card-price {
  color: var(--ink);
  font-weight: 800;
  white-space: nowrap;
  letter-spacing: -0.005em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.seller-layout {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}

.seller-card {
  padding: 14px;
}

.seller-card h3 {
  margin: 5px 0;
  font-size: 1.4rem;
}

.seller-card p {
  min-height: 40px;
  color: var(--muted);
  font-size: 0.88rem;
}

.seller-card.featured {
  border-color: rgba(31, 122, 77, 0.45);
  background: var(--tint-mint-strong);
}

.toast {
  position: fixed;
  right: 20px;
  bottom: 20px;
  max-width: min(400px, calc(100vw - 40px));
  padding: 12px 16px;
  border-radius: 8px;
  background: #17211d;
  color: white;
  transform: translateY(16px);
  opacity: 0;
  pointer-events: none;
  transition: 180ms ease;
  font-size: 0.9rem;
  z-index: 300;
}

.toast.show {
  transform: translateY(0);
  opacity: 1;
}

/* --- responsive: tablet 1040px --- */
@media (max-width: 1040px) {
  .app-shell {
    grid-template-columns: 1fr;
  }

  .sidebar {
    position: static;
    height: auto;
    padding: 14px 16px;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    overflow-y: visible;
  }

  .brand {
    flex: 0 0 auto;
  }

  .nav-list {
    flex: 1 1 auto;
    grid-auto-flow: column;
    grid-template-columns: repeat(4, auto);
    gap: 4px;
  }

  .nav-item {
    font-size: 0.82rem;
    padding: 6px 10px;
  }

  .sidebar-panel {
    display: none;
  }

  .account-bar {
    flex: 0 0 auto;
    margin-top: 0;
  }

  .market-layout,
  .signals-layout,
  .record-spec-grid,
  .seller-layout {
    grid-template-columns: 1fr;
  }

  .match-sides-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --- responsive: mobile 768px --- */
@media (max-width: 768px) {
  /* --- compact floating command sheet --- */
  .sidebar {
    position: fixed;
    top: 12px;
    left: 10px;
    transform: translateX(calc(-100% - 20px));
    width: min(220px, 78vw);
    max-width: 220px;
    height: auto;
    max-height: calc(100dvh - 24px);
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 6px;
    padding: 10px;
    border-radius: 14px;
    box-shadow: none;
    z-index: 200;
    overflow-x: hidden;
    overflow-y: auto;
    transition: transform 220ms ease;
  }

  body.drawer-open .sidebar {
    transform: translateX(0);
    box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28);
  }

  /* close button floats top-right inside the sheet */
  .nav-close {
    display: block;
    position: absolute;
    top: 8px;
    right: 8px;
    margin: 0;
    padding: 2px 6px;
    font-size: 0.9rem;
  }

  .brand {
    padding: 4px 6px;
    margin: 0 24px 4px 0; /* leave room for abs-positioned close btn */
  }

  .brand-mark {
    width: 28px;
    height: 28px;
    font-size: 0.75rem;
  }

  .brand strong {
    font-size: 0.9rem;
  }

  .brand span {
    display: none;
  }

  .nav-list {
    display: grid;
    grid-auto-flow: row;
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .nav-item {
    min-height: 34px;
    padding: 6px 8px;
    font-size: 0.84rem;
    border-radius: 8px;
  }

  .account-bar {
    display: block;
    margin-top: 6px;
    padding: 6px;
    border-radius: 10px;
    font-size: 0.8rem;
  }

  .account-google-signin {
    min-height: 34px;
    padding: 6px 8px;
    font-size: 0.78rem;
  }

  .sidebar-panel {
    display: none !important;
  }

  .nav-overlay {
    backdrop-filter: blur(2px);
  }

  .mobile-nav-strip {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 0 12px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--line);
    min-width: 0;
  }

  main {
    padding: 14px 16px;
  }

  .topbar {
    display: grid;
    gap: 10px;
  }

  .topbar h1 {
    font-size: 1.5rem;
  }

  .topbar-actions {
    justify-content: stretch;
    gap: 6px;
  }

  .topbar-actions button {
    flex: 1;
  }

  /* PROMPT-163 (QA-16 §M1 — Mobile Topbar Simplification): hide
     the Refresh (#seedData) button on mobile so the topbar
     compresses from three controls (Search / Refresh / Post Want)
     to two (Search / Post Want). At ≤402px the third button
     created cramped utility/admin energy; removing it gives the
     search input + Post Want CTA the breathing room the
     collector floor expects. The button stays in the DOM (its JS
     listener at app.js#seedData wires unconditionally on boot —
     removal would throw on querySelector(null)). Desktop ≥769px
     remains unchanged: Refresh visible + functional. */
  #seedData {
    display: none;
  }

  /* PROMPT-164 (QA-16 §M2 — Mobile Back-Affordance Cleanup): hide
     the in-page Back pill on the item-page section heading at
     mobile widths. iOS Safari already provides a back button + a
     swipe-back gesture; the duplicate in-page chrome added
     tool-energy to the cinematic pedestal QA-16 flagged. We hide
     the parent `.topbar-actions` container (not just the button)
     so the section-heading grid collapses cleanly without leaving
     a 6px ghost gap. The button stays in the DOM because
     app.js boot reads `const itemBack = document.querySelector(
     "#itemBack")` and later does `itemBack.addEventListener(...)`
     non-null-safely — removal would throw on null.

     Scoped to #itemDetail so other views' Back buttons (record
     detail / match detail / user profile) — which legitimately
     need multiple back-target choices on every viewport — are
     untouched. Desktop ≥769px keeps the Back pill visible. */
  #itemDetail .section-heading .topbar-actions {
    display: none;
  }

  /* PROMPT-163 §6.5 — Mobile tap-target safety. The search input
     + Search button live inside the .search-bar pill. Bumping
     both to 44px on mobile satisfies the WCAG AAA minimum tap
     target while keeping the pill compact in the topbar row. */
  .search-bar input[type="search"],
  .search-bar button {
    min-height: 44px;
  }

  /* PROMPT-166 (QA-16 §M6 — Mobile Tap-Target Floor): raise every
     interactive surface on mobile to the iOS HIG / WCAG 2.5.5 AA
     comfort floor of 44px. Where the visual control is intentionally
     small (calm density), the size bump is invisible — text remains
     the same weight, the hit zone widens. This consolidates the
     ad-hoc floors PROMPT-163 (.search-bar) + PROMPT-152
     (.account-drawer-link) introduced into one mobile pass.

     Scope is limited to INTERACTIVE controls: buttons, hyperlinks,
     pill chips that fire navigation. Non-interactive surfaces
     (.view-affordance — a decorative span inside an interactive
     <article role="button"> match-card — and metadata strips /
     provenance tags / status chips) are intentionally untouched.

     Visual restraint: no filled CTA conversion, no heavy borders,
     no font-size bumps, no oversized padding. The strategy is
     `min-height: 44px` (often paired with `display: inline-flex`
     + `align-items: center` for inline links) so the actual hit
     zone reaches comfort floor without bloating the visible
     control. The .inline-link.item-name-link variant (PROMPT-163
     precedent) already follows this pattern; PROMPT-166 extends
     the floor to the rest of the inline-link family + buttons +
     drawer + source-links. */
  .primary-button,
  .ghost-button {
    min-height: 44px;
  }

  /* Section-heading compact ghost rule (desktop default 28px)
     would otherwise win over the .ghost-button bump above due to
     selector specificity. Override it for mobile so the
     record-detail / match-detail / user-profile Back buttons
     (PROMPT-164 preserved at all viewports for multi-target nav)
     reach the comfort floor. */
  .section-heading .topbar-actions .ghost-button {
    min-height: 44px;
  }

  /* Inline-link family: "Open →" / "Rate →" / "View →" /
     reputation chips / source-URL displays / "End" actions / etc.
     The inline-flex + min-height pattern expands the hit zone
     without changing the visible text size — same approach as
     the existing .inline-link.item-name-link variant. The 4px
     padding-inline keeps a comfortable touch buffer on each
     side without making the link visually feel like a button. */
  .inline-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 4px;
  }

  /* Account drawer menu items (PROMPT-152 set 9px vertical
     padding ≈ 34px tall). Bump to 44px floor so the
     authenticated identity menu hits comfortable thumb height. */
  .account-drawer-link {
    min-height: 44px;
  }

  /* Source link pill ("View source ↗" → external collector
     forum). This is an <a href="..."> — actually interactive,
     not decorative. The 24px chip is below thumb safety; the
     44px floor adds visual height to the chip but keeps it
     within the meta-row rhythm without becoming a filled button. */
  .source-link {
    min-height: 44px;
  }

  .section-heading,
  .panel-header {
    display: grid;
    gap: 6px;
  }

  .want-card,
  .event-card,
  .record-hero {
    display: grid;
  }

  .price-block {
    text-align: left;
  }

  .form-grid,
  .match-sides-row {
    grid-template-columns: 1fr;
  }

  .match-price-chips {
    gap: 6px;
  }

  .price-chip {
    padding: 5px 10px;
  }
}

/* --- responsive: narrow 600px --- */
@media (max-width: 600px) {
  .search-result-card {
    grid-template-columns: 1fr;
  }

  .seller-layout {
    grid-template-columns: 1fr;
  }
}

/* --- account bar (sidebar) --- */
.account-bar {
  padding: 10px 12px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.09);
  font-size: 0.84rem;
  color: #d8eadf;
}

.account-name-btn {
  display: block;
  width: 100%;
  border: 0;
  padding: 0 0 5px;
  background: transparent;
  color: #f0f8f3;
  font-weight: 700;
  font-size: 0.86rem;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, 0.18);
  text-underline-offset: 2px;
}

.account-name-btn:hover {
  color: white;
  text-decoration-color: rgba(255, 255, 255, 0.55);
}

.account-meta {
  display: flex;
  gap: 5px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 8px;
  font-size: 0.76rem;
  color: #b7c7bd;
}

.role-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.role-badge.role-buyer { background: var(--mint); color: var(--green-dark); }
.role-badge.role-seller { background: #fff3dc; color: #7a4200; }
.role-badge.role-admin { background: #fde8e8; color: #7a1a1a; }

.account-signout,
.account-signin-link {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 700;
  text-align: center;
  cursor: pointer;
}

.account-signout {
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.13);
  color: #d8eadf;
}

.account-signout:hover { background: rgba(255, 255, 255, 0.16); }

/* ─── PROMPT-108 (QA-11 §IA-2): account drawer + profile navigation ───────
   Replaces the static account-name-btn + always-visible Sign out button
   with a calmer, infrastructure-grade chip+drawer. The .account-name-btn
   and .account-signout rules above remain for legacy compatibility but
   are no longer wired by the renderer. */

.account-bar {
  position: relative; /* anchor for the absolutely-positioned drawer */
}

.account-chip {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  border: 1px solid rgba(255, 255, 255, 0.13);
  background: rgba(255, 255, 255, 0.04);
  color: #f0f8f3;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  position: relative;
  transition: background 120ms ease, border-color 120ms ease;
}

.account-chip:hover {
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(255, 255, 255, 0.22);
}

.account-chip[aria-expanded="true"] {
  background: rgba(255, 255, 255, 0.11);
  border-color: rgba(255, 255, 255, 0.28);
}

.account-chip-name {
  font-weight: 700;
  font-size: 0.86rem;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-right: 18px; /* room for caret */
}

.account-chip-meta {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 4px;
  font-size: 0.74rem;
  color: #b7c7bd;
}

.account-chip-handle {
  font-size: 0.74rem;
  color: #8aa899;
}

.account-chip-caret {
  position: absolute;
  top: 8px;
  right: 10px;
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.55);
  transition: transform 120ms ease;
}

.account-chip[aria-expanded="true"] .account-chip-caret {
  transform: rotate(180deg);
}

/* Drawer panel — anchored to the chip in the sidebar's stacking
   context. Calm, restrained styling per PROMPT-108 §4 (no spring
   animations, no oversized avatars, infrastructure-grade tone).

   PROMPT-152 (QA-15 §G1+G2+G3 — Account + Overlay Gravity Refinement):
   the drawer now unfolds DOWNWARD instead of upward so the reveal
   grammar matches the rest of the environment (rails, dialogs, and
   the chip caret ▾ all drop downward). Viewport-clipping safety is
   provided via `max-height` + `overflow-y: auto`. The mobile branch
   below remains `position: static` so the drawer pushes content
   instead of escaping over the sidebar overlay.

   §G2 tone refinement: section padding loosened, dividers softened
   from rgba .06 → rgba .04, link rows get a touch more vertical
   breathing room. The drawer reads as a calm identity surface
   rather than a flat tool list.

   §G3 overlay coherence: motion + ease tokens (`--motion-fast` +
   `--ease-soft`) shared with .post-want-dialog. Both surfaces share
   the same z-index tier (drawer on top of sidebar = 30; dialog
   uses native top-layer = above everything), the same surface
   contrast model (drawer continues sidebar tone, dialog matches
   main canvas), and the same calm reveal motion. */
.account-drawer {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 30;
  background: #1a2620;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 10px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
  padding: 6px 0;
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  transition: opacity var(--motion-fast) var(--ease-soft);
}

.account-drawer[hidden] { display: none; }

.account-drawer-section {
  padding: 6px 6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}

.account-drawer-section:last-child {
  border-bottom: 0;
}

.account-drawer-section-end {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-bottom: 0;
  padding-top: 6px;
}

.account-drawer-identity {
  padding: 10px 12px 8px;
}

.account-drawer-eyebrow {
  display: block;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  margin-bottom: 2px;
}

.account-drawer-display {
  display: block;
  font-weight: 700;
  font-size: 0.88rem;
  color: #f0f8f3;
}

.account-drawer-subtle {
  display: block;
  font-size: 0.74rem;
  color: #8aa899;
  margin-top: 2px;
}

.account-drawer-link {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border: 0;
  background: transparent;
  color: #d8eadf;
  font-size: 0.84rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  border-radius: 6px;
  font-family: inherit;
  transition: background var(--motion-fast) var(--ease-soft),
              color var(--motion-fast) var(--ease-soft);
}

.account-drawer-link:hover {
  background: rgba(255, 255, 255, 0.07);
  color: #ffffff;
}

.account-drawer-link:focus {
  outline: 2px solid rgba(255, 255, 255, 0.32);
  outline-offset: -2px;
}

.account-drawer-link-soft {
  color: #a3e4bf; /* highlight handle-setup nudge for accounts without a handle */
}

.account-drawer-signout {
  color: #f3c5c5;
}

.account-drawer-signout:hover {
  background: rgba(220, 80, 80, 0.18);
  color: #ffe4e4;
}

@media (max-width: 768px) {
  /* On mobile the sidebar opens as an overlay. Keep the drawer
     within the sidebar instead of escaping over the page.
     PROMPT-152 §mobile-safety: with the desktop drawer now
     downward-anchored (top: calc(100% + 6px)), the static layout
     here continues to push the live-signal panel below it without
     overlap. max-height is intentionally not constrained on mobile
     because the parent sidebar already scrolls. */
  .account-drawer {
    position: static;
    margin-top: 6px;
    box-shadow: none;
    max-height: none;
    overflow-y: visible;
  }
}

.account-signin-link {
  background: rgba(31, 122, 77, 0.22);
  border: 1px solid rgba(31, 122, 77, 0.35);
  color: #a3e4bf;
}

.account-signin-link:hover { background: rgba(31, 122, 77, 0.38); }

/* PROMPT-225 (Dark Mode Google Sign-In Button Fix): Google sign-in
   is a trust-critical action. Pre-PROMPT-225, the .account-google-signin
   button used `background: var(--panel)` + `color: #17211d` — in dark
   mode, --panel resolves to #1a2420 (dark), producing dark text on a
   dark surface that read as disabled / poisoned. Border was also
   `rgba(255, 255, 255, 0.18)` (semi-transparent white, invisible on a
   light surface in light mode).
   PROMPT-225 reads the Google sign-in surface from dedicated brand-
   intent tokens (--google-signin-bg / --google-signin-ink /
   --google-signin-border / --google-signin-bg-hover) declared in
   :root AND repeated verbatim in @media (prefers-color-scheme: dark).
   The duplication is intentional: it documents that the Google sign-
   in button is a brand-intent affordance that OPTS OUT of the dark
   cascade, so future audits see the theme-independence as a
   deliberate design decision (not an oversight). The PROMPT-120
   "no-hardcoded-white-surface-literal" invariant in styles.css
   is preserved — every value here flows through a token. */
.account-google-signin {
  display: block;
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--google-signin-border);
  border-radius: 6px;
  background: var(--google-signin-bg);
  color: var(--google-signin-ink);
  font-family: var(--font-display);
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
  text-align: center;
  transition: background var(--motion-base) var(--ease-soft),
              border-color var(--motion-base) var(--ease-soft),
              box-shadow var(--motion-base) var(--ease-soft);
}

.account-google-signin:hover {
  background: var(--google-signin-bg-hover);
  border-color: var(--google-signin-border-hover);
  box-shadow: 0 2px 6px rgba(23, 33, 29, 0.08);
}

.account-google-signin:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.account-google-signin:active {
  transform: scale(var(--press-scale));
}

/* --- mobile nav elements --- */
.nav-hamburger {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 10px;
  background: var(--panel);
  color: var(--ink);
  font-size: 0.82rem;
  font-weight: 700;
}

.nav-hamburger:hover { border-color: var(--green); }

.mobile-brand-text {
  border: 0;
  padding: 0;
  background: transparent;
  font-weight: 800;
  font-size: 0.95rem;
  color: var(--ink);
  flex: 1 1 0;
  min-width: 0;
  text-align: left;
  cursor: pointer;
}

.mobile-brand-text:hover {
  color: var(--green);
}

.mobile-auth-chip {
  flex-shrink: 0;
}

.mobile-user-chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--mint);
  color: var(--green-dark);
  font-size: 0.78rem;
  font-weight: 700;
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}

.mobile-user-chip:hover { border-color: var(--green); }

.mobile-signin-chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  border: 1px solid var(--green);
  border-radius: 999px;
  background: var(--green);
  color: white;
  font-size: 0.78rem;
  font-weight: 700;
  cursor: pointer;
}

.mobile-signin-chip:hover { background: var(--green-dark); }

/* --- account view (main) --- */
.account-profile-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 10px;
}

.account-profile-grid article {
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

.account-profile-grid span {
  display: block;
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 800;
  text-transform: uppercase;
}

.account-profile-grid strong {
  display: block;
  margin-top: 5px;
  font-size: 1rem;
  overflow-wrap: anywhere;
}

.account-section {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  margin-bottom: 10px;
}

.handle-display-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.handle-chip {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--ink);
}

.account-placeholder {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: 0.88rem;
}

.account-signed-out > p {
  color: var(--muted);
  margin: 0 0 10px;
}

.linked-account-row {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  padding: 6px 0;
}

.dev-login-panel {
  margin-top: 18px;
  max-width: 540px;
  padding: 16px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  background: var(--tint-cream);
}

.dev-login-panel p { margin: 0 0 10px; }

.account-oauth-panel {
  margin-bottom: 14px;
}

.account-wants-list {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.account-want-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}

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

.account-want-name {
  font-size: 0.9rem;
  flex: 1 1 0;
  min-width: 0;
}

.account-want-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
  color: var(--muted);
  flex-shrink: 0;
}

.ownership-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 20px;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--mint);
  color: var(--green-dark);
  letter-spacing: 0.01em;
}

@media (max-width: 1040px) {
  .account-bar {
    display: none;
  }
  .account-profile-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 768px) {
  .account-bar {
    display: block;
  }
  .account-profile-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 480px) {
  .account-profile-grid {
    grid-template-columns: 1fr;
  }
}

/* --- search bar --- */
.search-bar {
  display: flex;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel);
  /* PROMPT-229 (Mobile Touch Feedback Pass): smooth transition on
     border-color so the :focus-within ring fades in calmly rather
     than snapping. The input itself has `outline: none` (line below)
     so the :focus-within border on the wrapper is the only visible
     keyboard-focus indicator — without this rule, keyboard users
     received zero feedback when tabbing into the search bar.
     transition is on border-color only — no transform pollution. */
  transition: border-color var(--motion-base) var(--ease-soft);
}

/* PROMPT-229: :focus-within fires when the input INSIDE the wrapper
   has focus. Border darkens to var(--green) (the same brand accent
   used on .rail-track:focus-visible + .account-google-signin:focus-
   visible — keyboard-focus parity across the marketplace).
   Accessibility win: WCAG 2.4.7 (visible focus) was previously
   regressed because the input's own outline was suppressed without
   a replacement. */
.search-bar:focus-within {
  border-color: var(--green);
}

.search-bar input[type="search"] {
  border: 0;
  border-radius: 0;
  min-height: 40px;
  width: 210px;
}

.search-bar input[type="search"]:focus {
  border: 0;
  outline: none;
  box-shadow: none;
}

.search-bar button {
  border: 0;
  border-left: 1px solid var(--line);
  border-radius: 0;
  min-height: 40px;
  padding: 0 12px;
  background: var(--panel);
  color: var(--ink);
  font-weight: 700;
  font-size: 0.88rem;
}

.search-bar button:hover {
  background: var(--surface);
}

/* --- search results --- */
.search-results-shell {
  display: grid;
  gap: 22px;
}

/* PROMPT-245 (Search Expressiveness Pass): inline filter-chip row
   rendered ABOVE the result groups when the user has an active
   search query. Three dimensions (category / intent / freshness)
   exposed as small toggleable chips. Calm muted register; reuses
   the existing pill-chip pattern from PROMPT-195 .search-suggestion-
   chip + PROMPT-241 .collector-world-chip so the visual family
   stays converged. No new color tokens, no new animations, no
   sidebar filter panel, no faceted-search dashboard chrome.

   Wrap is intentional — chips spill onto multiple lines on narrow
   viewports rather than scrolling horizontally (avoids hijacking
   the rail-physics scroll surface from PROMPT-228). The
   "Clear filters" affordance renders only when at least one filter
   is active; honest about state. */
.search-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

.search-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.86rem;
  font-weight: 500;
  cursor: pointer;
  transition:
    background var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft);
  /* PROMPT-232 §touch-action-manipulation parity: chips earn the
     same instant-tap treatment as other tappable surfaces. */
  touch-action: manipulation;
  /* PROMPT-229 :active scale already cascades via the consolidated
     selector list — no per-chip transform needed here. */
}

.search-filter-chip:hover {
  background: var(--tint-bg);
  border-color: var(--green);
}

.search-filter-chip:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

/* Active state via aria-pressed — accessible + selector-clean.
   Filled green tint signals selection without animation or icon. */
.search-filter-chip[aria-pressed="true"] {
  background: var(--tint-mint);
  border-color: var(--green);
  color: var(--ink);
  font-weight: 600;
}

/* "Clear filters" inline link — text-tier affordance, deliberately
   smaller than the chips so it reads as a release valve, not a
   competing CTA. Only present when at least one chip is active. */
.search-filter-clear {
  display: inline-flex;
  align-items: center;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-body);
  font-size: 0.82rem;
  text-decoration: underline;
  cursor: pointer;
  padding: 5px 4px;
  margin-left: 4px;
  touch-action: manipulation;
}

.search-filter-clear:hover {
  color: var(--ink);
}

.search-filter-clear:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  border-radius: 4px;
}

/* PROMPT-195 (QA-18 §intelligent-search-surface): grounded
   suggestions panel rendered when the query is empty OR when a
   non-empty query returns zero results. Suggestions are drawn from
   actual category labels + locally-loaded item names; never invented.
   Chips reuse existing token palette — no new colors. */
.search-suggestions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--tint-bg);
}

.search-suggestions-eyebrow {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.search-suggestions-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.search-suggestion-chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  font-size: var(--type-sm);
  font-weight: 600;
  cursor: pointer;
  transition:
    border-color var(--motion-base) var(--ease-soft),
    background var(--motion-base) var(--ease-soft);
}
.search-suggestion-chip:hover,
.search-suggestion-chip:focus-visible {
  border-color: rgba(31, 122, 77, 0.4);
  background: var(--tint-mint);
  outline: none;
}
.search-suggestion-chip[data-suggestion-kind="category"] {
  /* Category chips read as the strongest grounded surface; tint
     slightly warmer so they read distinctly from item suggestions. */
  background: var(--tint-cream);
}

.search-suggestions-foot {
  margin: 0;
  font-size: 0.78rem;
  color: var(--muted);
  letter-spacing: var(--tracking-snug);
}

/* PROMPT-195 §intent-banner: a small calm chip strip above the
   result groups that surfaces query intent (category world / buyer-
   side / listing-side). The default item_like intent emits no
   banner — silence beats clutter when the query is a plain item
   search. */
.search-intent-banner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

.search-intent-chip {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--tint-bg);
  color: var(--text);
  border: 1px solid var(--line);
  font-size: 0.78rem;
  font-weight: 600;
}
.search-intent-chip[data-search-intent="category"] {
  background: var(--tint-warm);
  color: var(--gold);
}
.search-intent-chip[data-search-intent="want"] {
  background: var(--tint-mint);
  color: var(--green-dark);
}
.search-intent-chip[data-search-intent="listing"] {
  background: var(--tint-blue);
  color: var(--blue);
}

.search-group h3 {
  margin: 0 0 8px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.search-group-count {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--line);
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
}

.search-group-list {
  display: grid;
  gap: 8px;
}

.search-result-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  cursor: pointer;
  transition: 140ms ease;
}

.search-result-card:hover,
.search-result-card:focus {
  border-color: rgba(31, 122, 77, 0.4);
  box-shadow: 0 6px 16px rgba(23, 33, 29, 0.08);
  outline: none;
  transform: translateY(-1px);
}

.search-result-body .meta-row {
  margin-bottom: 5px;
}

.search-result-card strong {
  display: block;
  margin-bottom: 3px;
  overflow-wrap: anywhere;
}

.search-result-card p {
  margin: 0;
  color: var(--muted);
  font-size: 0.83rem;
  overflow-wrap: anywhere;
}

@media (max-width: 768px) {
  .search-bar input[type="search"] { width: 150px; }
  .search-result-card { grid-template-columns: 1fr; }
  .match-sides-row { grid-template-columns: 1fr; }
}

/* --- compact back/utility nav buttons (section heading topbar-actions) --- */
.section-heading .topbar-actions .ghost-button {
  min-height: 28px;
  padding: 0 10px;
  font-size: 0.78rem;
  font-weight: 600;
}

/* --- topbar minimal (non-home views) --- */
.topbar-minimal {
  margin-bottom: 4px;
}

/* PROMPT-217 (QA-21 C2 — Search Bar Position Parity): the
   pre-PROMPT-217 rule `.topbar-minimal .topbar-actions { width:
   100% }` was retired here. Combined with the default
   `.topbar-actions { justify-content: flex-end }` (line 499-504),
   width: 100% pushed the search bar from x=283 on `/` to x=990
   on /market / /trending / /listings / /matches / /items /
   /search (a 707px shift measured by QA-21). The topbar's parent
   `.topbar { justify-content: space-between }` (line 478-485)
   handles topbar-actions positioning correctly on its own — a
   single-child topbar lays out at flex-start, matching the
   homepage layout exactly. Mobile (≤768px) uses display: grid
   for the topbar, so this rule's removal has no mobile impact
   (the mobile @media block at line 2245-2252 still applies its
   own `justify-content: stretch` + `flex: 1` button sizing). */

/* --- record detail compact action layout --- */
.record-deal-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.record-deal-title {
  min-width: 0;
}

.record-deal-meta-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 8px;
}

.record-deal-name {
  margin: 0 0 6px;
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  line-height: 1.1;
  overflow-wrap: anywhere;
}

.record-deal-price {
  font-size: 1.5rem;
  white-space: nowrap;
  flex-shrink: 0;
}

.record-chips-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.record-action-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.record-demand-col,
.record-contact-col {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  min-width: 0;
}

.record-field-list {
  margin-top: 8px;
  display: grid;
  gap: 5px;
}

.record-field {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

.record-field span {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 800;
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
  min-width: 72px;
}

.record-field strong {
  overflow-wrap: anywhere;
  min-width: 0;
  font-size: 0.95rem;
}

.record-cta-block {
  margin-top: 14px;
}

/* PROMPT-263 (QA-26 follow-up open-market-outreach Phase 2): calm
   muted helper line above the want-detail "Offer to source this" CTA.
   Mirrors .match-conversation-cta-helper register — non-urgent,
   source-anchored, no payment / completion language. */
.record-cta-helper {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}

/* PROMPT-264 (QA-26 follow-up open-market-outreach Phase 3): listing
   outreach CTA cluster inside .offer-card. Sits below the meta strip
   and above the price column. Same calm muted helper-line register
   as .record-cta-helper / .match-conversation-cta-helper — non-
   urgent, source-anchored, marketplace-native. The primary-button
   stays at default size (the surrounding card constrains width).
   Anonymous viewers + owners + inactive listings never see this
   block — gating happens server-side via offer.viewerIsOwner +
   client-side via the allowOutreach opt + active-listing predicate. */
.offer-card-outreach {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: flex-start;
}
.offer-card-outreach-helper {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}
.offer-card-outreach-btn {
  font-size: 0.85rem;
  padding: 6px 12px;
}

.record-source-link-block {
  margin-top: 10px;
}

.record-source-cta {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 0.84rem;
  font-weight: 700;
  background: var(--tint-blue);
  color: var(--blue);
  text-decoration: none;
  border: 1px solid rgba(36, 92, 136, 0.2);
}

.record-source-cta:hover {
  background: var(--tint-blue);
  text-decoration: underline;
}

.record-notes-block {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}

@media (max-width: 768px) {
  .record-deal-header {
    flex-direction: column;
    gap: 8px;
  }
  .record-deal-price {
    font-size: 1.3rem;
  }
  .record-action-layout {
    grid-template-columns: 1fr;
  }
}

/* --- account tabs --- */
.account-tab-bar {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 10px;
  background: var(--panel);
}

.account-tab-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 0 8px;
  border: 0;
  border-right: 1px solid var(--line);
  border-radius: 0;
  background: var(--panel);
  color: var(--muted);
  font-weight: 700;
  font-size: 0.82rem;
  cursor: pointer;
}

.account-tab-btn:last-child { border-right: 0; }

.account-tab-btn.active {
  background: var(--ink);
  color: white;
}

.account-tab-panel { padding: 4px 0 8px; }

.account-tab-hidden { display: none; }

.account-ratings-summary {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 1.1rem;
}

@media (max-width: 480px) {
  .account-tab-bar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .account-tab-btn:nth-child(2) { border-right: 0; }
  .account-tab-btn:nth-child(3) { border-top: 1px solid var(--line); }
  .account-tab-btn:nth-child(4) { border-top: 1px solid var(--line); border-right: 0; }
}

/* --- buyer name link in want cards --- */
.buyer-name-link {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--tint-mint);
  color: var(--green);
  font-size: 0.78rem;
  font-weight: 700;
  border: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.buyer-name-link:hover { background: var(--tint-mint-strong); }

/* --- user profile view --- */
.user-profile-shell { display: grid; gap: 24px; }

/* PROMPT-188 (QA-18 §collector-profile): the public profile is a
   collector identity surface, not an account/dashboard. The legacy
   .user-profile-grid + .user-profile-stats 4-cell signal grid was
   retired because it framed the collector as a row of metrics rather
   than a person with worlds, wants, and a shelf. The .collector-*
   primitives below render the same data in collector-floor language.
   Mobile-first; viewport-aware widths; no new colors; reuses the
   existing PROMPT-120 token palette. */
.collector-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-bottom: 4px;
}

.collector-identity {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.collector-identity-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.collector-display-name {
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  color: var(--text);
}

.collector-identity-chips {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.collector-meta-line {
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
}

.account-linked-badge {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  border-radius: 999px;
  background: #1f7a4d;
  color: white;
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.profile-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }

.collector-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.collector-worlds-section .collector-worlds {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* PROMPT-241 §collector-worlds-tagline: calm muted one-sentence
   identity anchor above the world chips. Sits between the
   "Collector Worlds" eyebrow and the chip strip so the eye reads
   eyebrow → tagline → chips → on to the next section. var(--muted)
   color + display family + smaller font place it in the same
   calm helper-text register as the rest of the collector header. */
.collector-worlds-section .collector-worlds-tagline {
  margin: 0 0 8px;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.86rem;
  line-height: var(--leading-snug);
  max-width: 560px;
}

/* PROMPT-241 §collector-worlds-empty: muted single line that names
   the mechanism (worlds surface from posted wants + listings) for
   collectors who haven't activated yet. Pure copy on existing
   markup — the chip strip simply doesn't render in this branch.
   Same calm register as the tagline above. */
.collector-worlds-section .collector-worlds-empty {
  margin: 0;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.86rem;
  line-height: var(--leading-snug);
  max-width: 560px;
}

/* The collector-world chip carries data-world so the existing
   PROMPT-137 / PROMPT-187 cascade applies the per-world atmospheric
   surface + accent tokens. No new color tokens introduced — the
   chip's background reads var(--world-surface) and the glyph color
   reads var(--world-accent). Calm: chips do not animate, do not pulse,
   and never carry loud category-branding accents. */
.collector-world-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--world-surface, var(--tint-bg));
  font-size: 0.82rem;
  color: var(--text);
  --world-surface: var(--tint-bg);
  --world-accent: var(--muted);
}
.collector-world-chip[data-world="tcg-world"]         { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.collector-world-chip[data-world="sneaker-world"]     { --world-surface: var(--tint-cool);  --world-accent: var(--blue);  }
.collector-world-chip[data-world="watch-world"]       { --world-surface: var(--tint-blue);  --world-accent: var(--blue);  }
.collector-world-chip[data-world="comic-world"]       { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.collector-world-chip[data-world="game-world"]        { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.collector-world-chip[data-world="vinyl-world"]       { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.collector-world-chip[data-world="coin-world"]        { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.collector-world-chip[data-world="funko-world"]       { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.collector-world-chip[data-world="figure-world"]      { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.collector-world-chip[data-world="bag-world"]         { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.collector-world-chip[data-world="jewelry-world"]     { --world-surface: var(--tint-warm);  --world-accent: var(--gold);  }
.collector-world-chip[data-world="memorabilia-world"] { --world-surface: var(--tint-cream); --world-accent: var(--gold);  }
.collector-world-chip[data-world="art-world"]         { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.collector-world-chip[data-world="general-world"]     { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }

.collector-world-chip-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  color: var(--world-accent);
  opacity: 0.85;
}
.collector-world-chip-glyph svg {
  width: 100%;
  height: 100%;
}

.collector-world-chip-label {
  font-weight: 600;
}

.collector-world-chip-count {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
}

/* Collector card grid for Looking For / On the Shelf. Two columns at
   desktop, single column on mobile so cards retain visible image area
   without horizontal cramming. .card-media inside each card carries
   data-world from the cardMediaHtml helper, so PROMPT-187 atmosphere
   travels card-by-card across mixed-category profiles. */
.collector-card-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.collector-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  cursor: pointer;
  transition:
    transform var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft),
    box-shadow var(--motion-base) var(--ease-soft);
  /* PROMPT-201 (Critical mobile regression fix): collector-card
     grid uses minmax(0, 1fr) tracks, but a child with intrinsic
     width > the track's column-width could still bleed (esp. on
     narrow viewports < 360px). min-width: 0 lets flex/grid
     shrink the card; overflow: hidden clips any rogue child.
     Both are defense-in-depth — current children all respect
     parent width. */
  min-width: 0;
  overflow: hidden;
}
.collector-card:not([data-record-type]):not([data-item-slug]) {
  cursor: default;
}
.collector-card:hover,
.collector-card:focus-visible {
  border-color: rgba(31, 122, 77, 0.4);
  box-shadow: 0 6px 18px rgba(23, 33, 29, 0.09);
  outline: none;
}
.collector-card:not([data-record-type]):not([data-item-slug]):hover,
.collector-card:not([data-record-type]):not([data-item-slug]):focus-visible {
  border-color: var(--line);
  box-shadow: none;
}

.collector-card-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.collector-card-title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: var(--leading-snug);
  color: var(--text);
  word-break: break-word;
}

.collector-card-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--muted);
}

.collector-card-price {
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.collector-activity-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.collector-activity-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--tint-bg);
}

.collector-activity-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.collector-activity-stars {
  color: var(--gold);
  letter-spacing: 0.02em;
}

.collector-activity-time {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.78rem;
}

.collector-activity-comment {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text);
  line-height: var(--leading-snug);
}

@media (max-width: 600px) {
  .collector-card-grid { grid-template-columns: 1fr; }
  .collector-display-name { font-size: 1.1rem; }
}

.loading-state {
  color: var(--muted);
  font-size: 0.9rem;
  padding: 20px 0;
}

/* ─── messaging UI ───────────────────────────────────────────────────────────── */
.msg-toolbar {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 10px;
}

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

.msg-conv-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.msg-conv-row:last-child { border-bottom: 0; }

.msg-conv-main {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.msg-conv-label {
  font-size: 0.88rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.msg-conv-meta {
  font-size: 0.79rem;
  color: var(--muted);
}

/* PROMPT-272 (QA-29 inbox-ordering-and-stale-surfacing): calm stale
   label inside the inbox-row meta line. Slightly more muted than the
   surrounding metadata to read as a soft signal — not an unread
   badge, not a notification chip, not urgent. Italic on hover-free
   surfaces would land too "soft" so we keep upright + dim color. */
.msg-conv-stale {
  color: var(--muted);
  font-size: 0.78rem;
  opacity: 0.85;
}

/* PROMPT-278 (QA-32 match-archival-semantics): row-level "Archived"
   muted label. Same calm visual weight as .msg-conv-stale — archive
   is a soft visibility signal, NOT a status / lifecycle chip / urgent
   indicator. Italic to differentiate from .msg-conv-stale at a
   glance (stale = observational; archived = explicit user action). */
.msg-conv-archived-label {
  color: var(--muted);
  font-size: 0.78rem;
  font-style: italic;
  opacity: 0.85;
}

/* PROMPT-278: archived rows get a lighter opacity in the inbox so
   the eye knows they're surfaced explicitly (via "Show archived")
   and not part of the day-to-day active feed. No strikethrough, no
   red, no "removed" framing — the thread is still fully accessible. */
.msg-conv-row-archived {
  opacity: 0.7;
}

/* PROMPT-278: per-row action cluster so the inline "Archive"/"Restore"
   button sits next to the "Open →" link without crowding the meta
   line. Flex row keeps both buttons aligned to the right rail. */
.msg-conv-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.msg-id-code {
  font-size: 0.78rem;
  background: var(--line);
  padding: 1px 5px;
  border-radius: 3px;
  font-family: monospace;
}

.msg-conv-info {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

/* PROMPT-270 (QA-28 follow-up conversation-context-rails): calm
   context rail in the thread header. Replaces the pre-PROMPT-270
   raw-match-id code + active/closed conversation-status chip with
   a contextual header that names the item, source, price anchor,
   and lifecycle state. No profile cards, no online state, no
   payment claims. Visual weight: small + muted, NOT a dashboard
   surface — the thread itself remains the focal point. */
.msg-conv-context {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
}
.msg-conv-context-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.msg-conv-context-source {
  font-size: 0.72rem;
}
.msg-conv-context-item {
  margin: 0;
  font-size: 1rem;
  line-height: 1.3;
}
.msg-conv-context-item .item-name-link {
  font-size: inherit;
  font-weight: inherit;
}
.msg-conv-context-origin {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}
.msg-conv-context-price {
  margin: 0;
  font-size: 0.86rem;
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.msg-conv-context-price .eyebrow {
  font-size: 0.7rem;
}
.msg-conv-context-price strong {
  font-size: 0.95rem;
}
.msg-conv-context-links {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 4px;
}
.msg-conv-context-link {
  font-size: 0.8rem;
  padding: 4px 10px;
}
/* Minimal empty-state fallback when no context resolves. */
.msg-conv-context.msg-conv-context-empty {
  background: transparent;
  border: none;
  padding: 0;
}

.msg-thread {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 10px;
}

.msg-bubble {
  background: var(--surface);
  border-radius: 6px;
  padding: 8px 10px;
}

.msg-bubble-empty { list-style: none; }

/* PROMPT-240 §thread-empty-first-helper: muted etiquette line that
   appears below the "No messages yet — say hello." placeholder.
   Calm collector-native checklist (item name + condition + photos
   + coordination) without scripted message text. Smaller font than
   the placeholder above so the visual hierarchy reads orientation
   → guidance, not as a competing message. */
.msg-bubble-empty .msg-first-helper {
  margin: 6px 0 0;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.82rem;
  line-height: var(--leading-snug);
}

.msg-sender {
  display: block;
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 3px;
}

.msg-body {
  margin: 0;
  font-size: 0.9rem;
  word-break: break-word;
  white-space: pre-wrap;
}

.msg-time {
  display: block;
  font-size: 0.73rem;
  color: var(--muted);
  margin-top: 4px;
}

.msg-compose {
  border-top: 1px solid var(--line);
  padding-top: 10px;
}

.msg-input {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.9rem;
  font-family: inherit;
  resize: vertical;
  box-sizing: border-box;
  margin-bottom: 6px;
  background: var(--panel);
}

.msg-input:focus {
  outline: none;
  border-color: var(--green);
}

.msg-compose-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.msg-char-count {
  font-size: 0.78rem;
  color: var(--muted);
}

.msg-send-error {
  margin: 5px 0 0;
  min-height: 1.1em;
  font-size: 0.83rem;
  color: var(--pepper);
}

.msg-error {
  color: var(--pepper) !important;
}

/* ─── ratings UI ─────────────────────────────────────────────────────────────── */
.ratings-section {
  margin-bottom: 16px;
}

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

.ratings-eligible-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}

.ratings-eligible-row:last-child { border-bottom: 0; }

.ratings-eligible-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.ratings-eligible-name {
  font-size: 0.88rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ratings-eligible-price {
  font-size: 0.78rem;
  color: var(--muted);
}

.ratings-form {
  background: var(--mint);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 12px 14px;
  margin-bottom: 14px;
}

.ratings-star-row {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 8px;
}

.ratings-star-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.5rem;
  color: var(--muted);
  padding: 2px;
  line-height: 1;
  transition: color 0.1s, transform 0.1s;
}

.ratings-star-btn:hover,
.ratings-star-btn.ratings-star-selected {
  color: var(--gold);
}

.ratings-star-hint {
  font-size: 0.82rem;
  color: var(--muted);
  margin-left: 4px;
}

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

.ratings-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.ratings-row:last-child { border-bottom: 0; }

.ratings-stars {
  font-size: 1rem;
  color: var(--gold);
  white-space: nowrap;
  flex-shrink: 0;
  padding-top: 2px;
}

.ratings-row-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.ratings-from {
  font-size: 0.85rem;
}

.ratings-comment {
  font-size: 0.85rem;
  color: var(--ink);
  margin: 2px 0 0;
}

.ratings-meta {
  font-size: 0.76rem;
  color: var(--muted);
}

.account-ratings-summary {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 12px;
  font-size: 1.1rem;
}

.account-ratings-summary strong {
  font-size: 1.6rem;
  color: var(--gold);
}

/* PR #53 — dual-intent marketplace + offer cards */

.intent-tabs {
  display: flex;
  gap: 4px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 3px;
  margin-bottom: 12px;
}

.intent-tab {
  flex: 1;
  padding: 6px 10px;
  border: none;
  border-radius: 6px;
  background: transparent;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
  white-space: nowrap;
}

.intent-tab.active {
  background: var(--ink);
  color: #ffffff;
}

.intent-panel {
  display: block;
}

.intent-panel.hidden {
  display: none;
}

.intent-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  flex-wrap: wrap;
  gap: 8px;
}

.intent-panel-header .eyebrow {
  margin: 0;
}

.offer-card {
  border-left: 3px solid var(--accent, #3a7bd5);
}

.want-seller-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 4px;
  font-size: 0.84rem;
}

.seller-cta-band {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 0.88rem;
}

.spread-stat strong {
  font-size: 0.88rem;
}

@media (max-width: 600px) {
  .intent-tab {
    padding: 5px 6px;
    font-size: 0.78rem;
  }
}

/* PR #52 — account UX */
.want-auth-notice {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--tint-mint);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 0.9rem;
  color: #2d6a2d;
  margin-bottom: 4px;
}

.account-empty-cta {
  padding: 20px 0 8px;
}

.account-empty-cta p {
  margin: 4px 0;
}

/* PROMPT-168 (QA-16 §M4 — Listings Empty-State Continuation):
   calm secondary continuation below the /listings empty-state
   primary CTA. The 1px top-border + soft margin separate the
   continuation from the "+ Create your first listing" / "Be the
   first seller" copy without becoming a heavy banner. The link
   rests in var(--muted) and shifts to var(--ink) on hover/focus
   — same calm gravity tier as the PROMPT-167 homepage editorial
   close, NOT a filled CTA. min-height: 44px honors the PROMPT-166
   tap-target contract. Token-driven; no new palette. */
.listings-empty-continuation {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.listings-empty-continuation-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 4px 6px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-base);
  letter-spacing: var(--tracking-snug);
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-soft);
}

.listings-empty-continuation-link:hover,
.listings-empty-continuation-link:focus {
  color: var(--ink);
  outline: none;
}

.listings-empty-continuation-link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 4px;
  border-radius: 4px;
}

/* PROMPT-209 (QA-19 M4 — Desktop Listings Empty-State Expansion):
   wrapper for the buyer-demand continuation rail that sits below the
   anonymous /listings empty state. Provides a calm separator from
   the empty-state CTA block (sibling rhythm to
   .listings-empty-continuation), then defers all rail layout to the
   existing .rail-section primitive — no new visual language, no new
   palette, no mobile redesign (the inner rail collapses to a
   horizontal scroller via existing rail CSS, the PROMPT-201
   touch-action: pan-x pan-y contract is preserved). Token-driven. */
.listings-empty-continuation-surface {
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--line);
}

.account-reputation-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.image-upload-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px dashed var(--line);
  border-radius: 8px;
  padding: 18px 12px;
  color: var(--muted);
  font-size: 0.88rem;
  background: var(--surface);
  margin-bottom: 12px;
}

/* PROMPT-130 (QA-13 §6 + §12 + §15 + §17): image uploader.
   Calm Muji / Apple Files tone — no cropper, no filters, no rotation.
   Token-driven so dark mode flips automatically. */
.image-uploader {
  margin: 12px 0;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--tint-bg-soft);
}

.image-uploader-help {
  margin: 4px 0 10px;
  color: var(--muted);
  font-size: var(--type-sm);
  line-height: var(--leading-snug);
}

/* PROMPT-246 §image-uploader-intro: calm collector-native framing
   line above the technical constraints. Same muted register as
   .image-uploader-help so the eye reads them as one helper block;
   the intro sits slightly higher (slightly larger font) to anchor
   WHY photos matter before the WHAT/HOW constraint enumerates. */
.image-uploader-intro {
  margin: 4px 0 2px;
  color: var(--muted);
  font-size: var(--type-base);
  line-height: var(--leading-snug);
}

/* PROMPT-246 §field-help: small calm helper line that sits below
   a form field's input control inside the parent <label>. Pinned
   to the muted register + smaller font-size so the eye reads
   label → input → helper as a continuous information layer, not
   as a new chrome block. Letter-spacing slightly looser to match
   the editorial register of .homepage-intro + .market-floor-line.
   Display: block + margin-top: 4px gives it a clear separation
   from the input it explains without adding vertical chrome. */
.field-help {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: var(--leading-snug);
}

.image-preview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  margin-bottom: 10px;
}

.image-preview-grid:empty {
  display: none;
}

.image-preview {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  overflow: hidden;
  background: var(--tint-bg);
  border: 1px solid var(--line);
}

.image-preview-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.image-preview-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 0;
  /* Hard-coded dark scrim — must remain dark in BOTH themes so the
     × is legible over arbitrary photo content; theme tokens would
     invert in dark mode and lose contrast. */
  background: rgba(15, 23, 20, 0.72);
  color: #ffffff;
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease-soft);
}

.image-preview-remove:hover,
.image-preview-remove:focus {
  background: rgba(15, 23, 20, 0.88);
  outline: none;
}

.image-uploader-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.image-uploader-input {
  display: none;
}

.image-uploader-button {
  font-size: var(--type-sm);
  padding: 6px 12px;
  min-height: 36px;
}

.image-uploader-status {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: var(--type-xs);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-snug);
  min-height: 1em;
}

.image-uploader-status-error {
  color: var(--pepper);
}

/* PROMPT-131 (QA-13 §6 + §12 + §14 + §15 + §18): bucket-oriented
   horizontal discovery rails. Editorial shelf browsing — Netflix /
   App Store / Apple TV / Steam adapted to collector psychology.
   No carousel library, no Swiper.js, no virtualization, no Framer
   Motion — pure CSS scroll-snap with thumb-friendly card sizing
   and momentum-aware soft snapping. */
.discover-rails {
  display: flex;
  flex-direction: column;
  /* PROMPT-142 §rhythm: the base gap establishes the rest beat
     between shelves. Cadence classes additionally apply margin-top
     pressure for hero/breathe peaks and margin-bottom relaxation
     for quiet decompression beats. */
  gap: var(--rail-gap-normal);
  margin-bottom: 24px;
}

.discover-rails:empty {
  display: none;
}

.rail-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.rail-subtitle {
  margin: 2px 0 0;
  font-size: var(--type-sm);
  color: var(--muted);
  line-height: var(--leading-snug);
}

/* PROMPT-189 (QA-18 §editorial-density): data-grounded pulse strip
   under the rail subtitle. Surfaces real freshness + world breadth
   counts from the sanitized records (see buildRailPulse in app.js).
   Calm: muted color, smaller than the subtitle, tabular numerics for
   stable digit width across renders. Fits on a single line at
   402px viewport because the longest expected pulse —
   "9 fresh today · Across 9 worlds" — measures under 240px. */
.rail-pulse {
  margin: 3px 0 0;
  font-size: 0.78rem;
  color: var(--muted);
  letter-spacing: var(--tracking-snug);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-snug);
  opacity: 0.85;
}

/* PROMPT-132 §edge-fade: cinematic edge gradient via ::before /
   ::after on a wrapper element so the gradient overlays the
   scrollable track without participating in pointer/scroll events.
   The gradient uses var(--surface) (theme-aware) → transparent so
   dark mode flips automatically. Opacity stays under 12% per
   §edge-fade — calm, never neon. */
.rail-track-wrapper {
  position: relative;
  isolation: isolate;
}

.rail-track-wrapper::before,
.rail-track-wrapper::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 12px;
  width: 32px;
  pointer-events: none;
  z-index: 2;
  transition: opacity var(--motion-base) var(--ease-soft);
}

.rail-track-wrapper::before {
  left: 0;
  /* PROMPT-218: gradient end-stop now uses --surface-fade-transparent
     (same hue as --surface, fully transparent) instead of
     rgba(255, 255, 255, 0). Eliminates the muddy mid-gradient stripe
     that QA-21 observed as a magnify-sliver artifact, especially
     visible in dark mode where the pre-PROMPT-218 gradient crossed
     through brownish gray between the dark surface and the
     transparent-white end-stop. */
  background: linear-gradient(to right, var(--surface) 0%, var(--surface-fade-transparent) 100%);
}

.rail-track-wrapper::after {
  right: 0;
  background: linear-gradient(to left, var(--surface) 0%, var(--surface-fade-transparent) 100%);
}

.rail-track {
  /* PROMPT-228 (Mobile Rail Physics Pass) audit summary: the
     existing PROMPT-131 / 132 / 172 / 201 / 216 scroll surface
     was reviewed for touch-physics legitimacy and confirmed
     correct. The swipe-stickiness collectors reported was
     traced to the card-level surfaces (text-selection-on-drag
     + iOS image-drag preview), not to this track. All track
     properties below are intentional and pinned by tests —
     do not modify without re-running real-hardware verification
     on iPhone Safari + Android Chrome. */
  display: flex;
  flex-wrap: nowrap;
  gap: 12px;
  padding: 4px 4px 12px;
  margin: 0 -4px;
  overflow-x: auto;
  overflow-y: hidden;
  /* PROMPT-172 (QA-17 §touch-pan — Rail Touch-Pan Velocity Restoration):
     scroll-snap was `x mandatory` from PROMPT-131 onward. On iOS Safari,
     mandatory snap captures the velocity of a flick gesture but then
     forces the viewport to snap to the nearest card boundary at flick
     end — on short rails this rubber-bands the fling into a position the
     user didn't aim at, which feels like "the flick didn't work." Slow
     drags worked because the natural drag endpoint is already close to
     a snap point. Proximity snap preserves snap behavior when the user
     has naturally scrolled close to a card boundary (slow drag UX
     unchanged) but does NOT intercept fling momentum (fast flick UX
     restored). The collector-floor rail-native interaction grammar
     keeps its snap rhythm without sacrificing iOS-native flick physics.

     Do NOT revert to `mandatory` without re-running the manual QA-17
     mobile flick verification on real iPhone Safari. */
  scroll-snap-type: x proximity;
  scroll-padding-left: 4px;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* PROMPT-132 §motion: prevent horizontal scroll bleeding to the
     ancestor — keeps the page's vertical scroll uninterrupted while
     the user explores a single rail. */
  overscroll-behavior-x: contain;
  /* PROMPT-201 (Critical mobile regression fix): the original
     PROMPT-132 declaration was `touch-action: pan-x` with a comment
     claiming "vertical drift bubbles to the page so the user can
     still scroll past a rail." That claim is incorrect — `pan-x`
     EXPLICITLY blocks the browser from interpreting a vertical
     gesture as a page scroll when the touch begins inside the
     rail. Real iPhone Safari testing surfaced this as a regression:
     touching inside any rail and dragging down trapped the gesture
     in the (horizontally-only) rail. The fix is `pan-x pan-y`:
     both axes permitted, browser arbitrates direction once gesture
     starts (horizontal swipe → rail scroll; vertical swipe → page
     scroll). Fast horizontal flick still works (PROMPT-172 contract
     preserved); slow vertical drag now correctly bubbles to the
     page. Do NOT revert to `pan-x` alone without re-running real-
     hardware verification on iPhone Safari. */
  touch-action: pan-x pan-y;
}

.rail-track::-webkit-scrollbar {
  display: none;
}

.rail-track:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  border-radius: 8px;
}

/* PROMPT-216 (QA-21 C1 — Rail Visible Scroll Affordances):
   prev/next buttons that make horizontally scrollable rails
   discoverable for mouse-only desktop users. Calm, neutral,
   contextual:
   - hidden by default (display:none) so non-scrolling rails
     never show inert affordances;
   - visible (calm 0.55 opacity) when the wrapper carries
     data-has-overflow="1" (set by wireRailScrollAffordances
     after detecting scrollWidth > clientWidth);
   - 1.0 opacity on .rail-track-wrapper:hover or :focus-within
     for the standard active-state lift;
   - prev individually hidden when wrapper has data-at-start
     (similarly next when data-at-end) so the user never sees
     an inert button at the scroll endpoints;
   - hidden entirely on touch devices via @media (hover: none)
     since touch users have the native flick gesture as their
     discovery cue (PROMPT-201 contract preserved).
   Buttons live INSIDE .rail-track-wrapper which has
   position:relative (PROMPT-131); they layer above the existing
   edge-fade ::before / ::after pseudo-elements via z-index:3
   (those use z-index:2). Token-driven; no new palette. */
.rail-scroll-prev,
.rail-scroll-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.55;
  z-index: 3;
  box-shadow: 0 2px 6px rgba(23, 33, 29, 0.08);
  transition: opacity var(--motion-base) var(--ease-soft),
              transform var(--motion-base) var(--ease-soft);
}

.rail-scroll-prev { left: 6px; }
.rail-scroll-next { right: 6px; }

.rail-track-wrapper[data-has-overflow="1"] .rail-scroll-prev,
.rail-track-wrapper[data-has-overflow="1"] .rail-scroll-next {
  display: inline-flex;
}

.rail-track-wrapper[data-has-overflow="1"][data-at-start] .rail-scroll-prev {
  display: none;
}

.rail-track-wrapper[data-has-overflow="1"][data-at-end] .rail-scroll-next {
  display: none;
}

.rail-track-wrapper:hover .rail-scroll-prev,
.rail-track-wrapper:hover .rail-scroll-next,
.rail-track-wrapper:focus-within .rail-scroll-prev,
.rail-track-wrapper:focus-within .rail-scroll-next {
  opacity: 1;
}

.rail-scroll-prev:hover,
.rail-scroll-next:hover {
  transform: translateY(-50%) scale(1.04);
  border-color: rgba(31, 122, 77, 0.4);
}

.rail-scroll-prev:focus-visible,
.rail-scroll-next:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
  opacity: 1;
}

@media (hover: none), (pointer: coarse) {
  /* Touch / coarse-pointer devices: the native flick gesture is
     the canonical scroll affordance. Suppress the prev/next
     buttons to keep the touch surface clean. */
  .rail-scroll-prev,
  .rail-scroll-next {
    display: none !important;
  }
}

/* PROMPT-165 (QA-16 R3 — Single-Card Rail Variant): when a rail
   resolves to exactly one renderable record after sanitize +
   dedupe, the renderer emits `.rail-single-card` INSTEAD of the
   horizontal `.rail-track-wrapper > .rail-track` primitive. Goal:
   the card reads as an intentional editorial placement, not a
   half-empty carousel implying "more to the right." We center
   the card with a max-width cap so a single object doesn't
   stretch full-bleed, but on mobile the card uses the available
   width naturally so collectors see the full presentation. The
   `.rail-card flex: 0 0 240px` rule from the multi-card track
   would lock the card at 240px even alone — we override that
   here so single-card presentation fills the calm featured
   width gracefully. No carousel indicators, no pagination dots,
   no swipe affordance — single-card rails ARE NOT carousels. */
.rail-single-card {
  display: flex;
  justify-content: center;
  padding: 4px 4px 12px;
}

.rail-single-card > .rail-card {
  /* Override the multi-card flex-basis so the centered featured
     card can grow up to a calm reading width on desktop while
     still respecting the mobile container. */
  flex: 0 1 auto;
  width: 100%;
  max-width: 360px;
}

@media (max-width: 768px) {
  /* On mobile the single card uses the full available width
     (minus the small padding the wrapper provides) so collectors
     see the full presentation without a stranded narrow column.
     The flex-basis override + max-width cap from desktop are
     intentionally raised here. */
  .rail-single-card {
    padding: 4px 0 12px;
  }

  /* PROMPT-184 (QA-18 N1 — Storefront Window Mobile Overflow Fix):
     replaces PROMPT-165's `max-width: none` mobile override with a
     viewport-aware ceiling. QA-18 observed the curated Storefront
     Window card bleeding past the right edge at 402px on iPhone 17
     simulator. The single-card hero shelf combines
     .rail-section--single (this rule) with .rail-section--hero
     (whose mobile cascade sets `flex-basis: 240px` below at
     line ~4530), and depending on which width directive wins for
     the rendered main-axis size, the card could occupy the full
     container OR 240px. Without a hard ceiling, any future
     cascade change could let a card exceed the viewport.

     `calc(100vw - 32px)` = mobile viewport width minus main's
     horizontal padding (16px × 2 from styles.css mobile main
     rule). With `box-sizing: border-box` (global) the cap counts
     padding + border INSIDE the max-width, so the card's outer
     box always fits comfortably within the visible area on a
     402px viewport (cap = ~370px).

     This is defense-in-depth: even if a future PR changes
     flex-basis / width rules, the card cannot overflow the
     viewport's main content area. */
  .rail-single-card > .rail-card {
    max-width: calc(100vw - 32px);
  }
}

/* PROMPT-273 (QA-29 follow-up item-page-outreach-surfacing): calm
   outreach footer on item-page rail cards (offers + wants). Compact
   ghost-button sized to subordinate to the card's title/price/
   provenance — NOT a primary CTA, NOT urgent, NOT animated. Reserved
   for item-page rails only (opts.allowOutreach gate in renderRailHtml
   keeps homepage / market / trending rails strictly navigational per
   PROMPT-261). Mobile-reachable — visible action, no hover dependency. */
.rail-card-outreach {
  margin-top: 6px;
}
.rail-card-outreach-btn {
  width: 100%;
  font-size: 0.78rem;
  padding: 4px 8px;
}

.rail-card {
  flex: 0 0 240px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  cursor: pointer;
  transition:
    transform var(--motion-base) var(--ease-soft),
    box-shadow var(--motion-base) var(--ease-soft),
    border-color var(--motion-base) var(--ease-soft);
  min-width: 0;
  /* PROMPT-201 (Critical mobile regression fix): defense-in-depth
     bleed containment. The card-media child already declares
     overflow: hidden, but a future child (provenance line, freshness
     chip, etc.) with intrinsic wider content could overflow the
     rail card's content area and visually bleed past the right
     edge on narrow mobile viewports. Clipping at the .rail-card
     boundary guarantees the rounded corners stay rendered and the
     scroll-snap target box never exceeds its declared width. */
  overflow: hidden;
  /* PROMPT-228 (Mobile Rail Physics Pass): swipe ergonomics.
     Pre-PROMPT-228, the absence of user-select / -webkit-touch-callout
     let two iOS-Safari heuristics interfere with horizontal flicks:
     (a) text-selection-on-drag — the browser interpreted a fast
         horizontal swipe across card text as a drag-select gesture
         and the swipe "stuck"; (b) long-press context menu — the
         system menu could initialize on a quick touch-down + hold
         pattern that's normal during a flick. Suppressing both at
         the .rail-card boundary lets the native scroll physics
         take over. Scope intentionally narrow: text-selection
         outside rail cards (account drawer copy, item-pedestal
         body, search results) is unchanged. The trade-off is
         accepting that users can't double-tap to select a rail-
         card title — primary interaction here is tap-to-navigate,
         not copy-text. -webkit-tap-highlight-color: transparent
         removes the gray flash overlay iOS draws on tap so the
         press-scale (PROMPT-201 :active transform) is the only
         visible feedback. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

.rail-card:hover,
.rail-card:focus {
  border-color: rgba(31, 122, 77, 0.4);
  box-shadow: 0 6px 18px rgba(23, 33, 29, 0.09);
  outline: none;
  transform: translateY(-1px);
}

.rail-card:active {
  transform: translateY(0) scale(var(--press-scale));
  box-shadow: 0 2px 6px rgba(23, 33, 29, 0.06);
  transition-duration: var(--motion-fast);
}

.rail-card .card-media {
  margin-bottom: 0;
}

.rail-card-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.rail-card-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-base);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* PROMPT-214 (QA-20 M1 — Rail Card Title Left-Edge Fix): the
     combination of (a) negative letter-spacing (var(--tracking-snug)
     = -0.005em) and (b) `overflow: hidden` (required by the
     -webkit-line-clamp mechanism above) was clipping the leftmost
     pixel of bold display-font glyphs whose left side-bearing
     extends past the text origin — V / U / M / W and their kin.
     Reproductions: "Van Gogh Pikachu", "Umbreon VMAX Alt Art",
     "MTG Revised Underground Sea". A 2px logical inline-start
     padding gives the negative bearing room to render without
     touching the typography (letter-spacing / weight / family
     all preserved), without changing the card's outer width
     (PROMPT-184 mobile viewport cap preserved), and without
     altering the ellipsis behavior (the line-clamp still
     truncates at the title's right edge — only the left edge
     gains 2px of safe-area). The price/meta rows below have no
     equivalent left-bearing risk (tabular-nums numerals + small
     light meta text), so the 2px asymmetry is visually inert. */
  padding-inline-start: 2px;
}

.rail-card-price {
  margin: 0;
  font-size: var(--type-md);
  font-weight: 800;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
}

.rail-card-meta {
  font-size: var(--type-xs);
  font-weight: 600;
  color: var(--muted);
  letter-spacing: var(--tracking-snug);
  margin-left: 4px;
}

/* PROMPT-132 §rail-identity-header-polish: optional small category
   glyph in the rail title, plus a tabular-nums item-count badge.
   The glyph picks up theme tone via currentColor; the count stays
   muted so it reads as supporting context, not a notification. */
.rail-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 0 2px;
}

.rail-heading-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.rail-title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

.rail-heading-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: var(--muted);
  opacity: 0.75;
  flex-shrink: 0;
}

.rail-heading-glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}

.rail-count {
  font-family: var(--font-body);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: var(--tracking-snug);
  align-self: center;
  flex-shrink: 0;
}

/* PROMPT-133 (QA-13 collector-emotional resonance): warm-pocket
   featured card. Subtle museum-label treatment — faint warm border,
   parchment-cream surface, small "Featured" / "Hard to find" /
   "Collector spotlight" tag at the top-left. NO metallic gradients,
   NO glow, NO sparkle, NO pulse / shimmer / breathing animations.
   The treatment reuses existing PROMPT-120 tokens (--tint-cream
   for surface, --gold for accent text) so dark mode flips
   automatically. Featured cards in regular rails create occasional
   emotional spikes; the Featured Finds rail forces the treatment
   on every card via the renderer's `featured: true` option. */
.featured-card {
  background: var(--tint-cream);
  border-color: rgba(200, 134, 45, 0.30);
  position: relative;
  /* Subtle slow-easing override so the hover transition feels a tier
     more deliberate than regular cards — museum-label calm, not
     ecommerce hover hype. PROMPT-124 motion tokens reused. */
  transition:
    transform var(--motion-slow) var(--ease-soft),
    box-shadow var(--motion-slow) var(--ease-soft),
    border-color var(--motion-slow) var(--ease-soft);
}

.featured-card:hover,
.featured-card:focus {
  border-color: rgba(200, 134, 45, 0.55);
}

.featured-mark {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(200, 134, 45, 0.16);
  color: var(--gold);
  font-family: var(--font-display);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  pointer-events: none;
}

/* Featured Finds rail heading gets a tiny warm accent on the title
   eyebrow so collectors recognise the shelf as curated. The accent
   stays subtle — no neon, no metallic, no rainbow. */
.rail-section[data-rail-id="featured-finds"] .rail-title {
  color: var(--ink);
}

.rail-section[data-rail-id="featured-finds"] .rail-subtitle {
  color: var(--gold);
  letter-spacing: var(--tracking-snug);
}

/* PROMPT-135 (QA-13 phase 4): editorial category worlds. Each world
   gets a quiet atmospheric tint behind the heading/track region —
   intentionally subtle (border-top accent only, ~30% opacity of the
   chosen tint token). Reuses PROMPT-120 tokens so dark mode flips
   automatically; no new colors introduced; no saturated branding.
   Intent rails (Recently Listed / Buyer Wants / Market Signals /
   Featured Finds) carry no `data-world` attribute and remain on the
   neutral surface — the worlds are category-specific. */
.rail-section[data-world] {
  position: relative;
  /* PROMPT-137: per-world token defaults. Each world's id-specific
     rule below overrides --world-surface / --world-border /
     --world-accent. Descendant rules (rail-heading-glyph etc.) read
     these via var() so atmospheric inheritance happens through the
     CSS variable cascade — no per-world duplicate rules required. */
  --world-surface: var(--tint-bg);
  --world-border:  var(--line);
  --world-accent:  var(--muted);
}

.rail-section[data-world]::before {
  content: "";
  position: absolute;
  top: 0;
  left: 2px;
  width: 28px;
  height: 2px;
  border-radius: 999px;
  pointer-events: none;
  opacity: 0.5;
  /* PROMPT-137: read from the per-world token; fallback to the
     neutral tint so the accent line is never invisible. */
  background: var(--world-surface, var(--tint-bg));
}

/* PROMPT-137: per-world token declarations. Each block sets the
   3-token palette established in src/categoryWorlds.js. All values
   reference existing PROMPT-120 tokens — no new colors. */
.rail-section[data-world="tcg-world"]         { --world-surface: var(--tint-warm);  --world-accent: var(--gold); }
.rail-section[data-world="sneaker-world"]     { --world-surface: var(--tint-cool);  --world-accent: var(--blue); }
.rail-section[data-world="watch-world"]       { --world-surface: var(--tint-blue);  --world-accent: var(--blue); }
.rail-section[data-world="comic-world"]       { --world-surface: var(--tint-cream); --world-accent: var(--gold); }
.rail-section[data-world="game-world"]        { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.rail-section[data-world="vinyl-world"]       { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.rail-section[data-world="coin-world"]        { --world-surface: var(--tint-warm);  --world-accent: var(--gold); }
.rail-section[data-world="funko-world"]       { --world-surface: var(--tint-mint);  --world-accent: var(--green); }
.rail-section[data-world="figure-world"]      { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.rail-section[data-world="bag-world"]         { --world-surface: var(--tint-cream); --world-accent: var(--gold); }
.rail-section[data-world="jewelry-world"]     { --world-surface: var(--tint-warm);  --world-accent: var(--gold); }
.rail-section[data-world="memorabilia-world"] { --world-surface: var(--tint-cream); --world-accent: var(--gold); }
.rail-section[data-world="art-world"]         { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }
.rail-section[data-world="general-world"]     { --world-surface: var(--tint-bg);    --world-accent: var(--muted); }

/* PROMPT-137 §card-atmosphere: descendant rules opt into the
   per-world tokens. The visual delta is intentionally minimal —
   only the small rail-heading glyph picks up --world-accent. The
   ::before accent line already reads --world-surface above. If a
   user consciously notices "this world has a strong theme," it is
   already too strong. */
.rail-section[data-world] .rail-heading-glyph {
  color: var(--world-accent);
}

/* PROMPT-135: profile identity descriptor. Calm one-line bio used in
   account drawer + future profile views. Theme-token-driven; no
   gamification colors. */
.profile-identity {
  margin: 6px 0 0;
  font-family: var(--font-body);
  font-size: var(--type-sm);
  font-weight: 500;
  color: var(--muted);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-snug);
}

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

/* PROMPT-132 §skeleton: calm placeholder card. Inherits the rail-card
   shell so layout stays stable when real cards replace skeletons.
   No shimmer / no pulse / no animation — just quiet matte lines. */
.rail-card-skeleton {
  cursor: default;
  pointer-events: none;
  opacity: 0.7;
}

.rail-card-skeleton:hover,
.rail-card-skeleton:focus {
  transform: none;
  box-shadow: none;
}

.rail-card-title-skeleton,
.rail-card-price-skeleton {
  display: block;
  height: 0.95em;
  border-radius: 4px;
  background: var(--tint-bg);
  margin: 0;
}

.rail-card-title-skeleton {
  width: 78%;
}

.rail-card-price-skeleton {
  width: 38%;
  height: 1.1em;
  margin-top: 4px;
}

.rail-skeleton-host {
  display: contents;
}

@media (max-width: 600px) {
  .rail-card {
    flex: 0 0 200px;
  }
  .discover-rails {
    gap: 22px;
  }
  /* Tighten the edge fade on narrow viewports so it doesn't
     consume too much of the visible card. */
  .rail-track-wrapper::before,
  .rail-track-wrapper::after {
    width: 20px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rail-track {
    scroll-behavior: auto;
  }
  .rail-card {
    transition: none;
  }
  .rail-card:hover,
  .rail-card:focus,
  .rail-card:active {
    transform: none;
  }
  .rail-track-wrapper::before,
  .rail-track-wrapper::after {
    transition: none;
  }
}

/* PROMPT-134 (QA-13 phase 3): LIVE SIGNAL ambient pulse + rail card
   insert + freshness whisper.

   The pulse is calibrated for "biological / ambient / infrastructural"
   — soft opacity breathing + tiny scale drift over a slow cycle.
   It is NOT a notification, NOT a gaming XP pulse, NOT a blink. The
   insert animation is a soft fade-up that arrives in 200ms and stops
   — no bounce, no spring, no elastic motion. The freshness whisper
   is a calm warm border + tiny "Just added" text label, applied to
   AT MOST one card per rail.

   prefers-reduced-motion disables ALL of these. */
@keyframes live-breathe {
  0%, 100% {
    opacity: 0.55;
    transform: scale(1);
  }
  50% {
    opacity: 0.95;
    transform: scale(1.04);
  }
}

.live-signal-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.live-signal-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--green);
  animation: live-breathe var(--motion-pulse) var(--ease-breathe) infinite;
}

/* PROMPT-138: the .market-status hero pill was retired with the
   onboarding band. Only the sidebar .live-signal-dot now drives the
   ambient marketplace heartbeat — single source of truth. */

@keyframes rail-card-enter {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.rail-card-entering {
  animation: rail-card-enter var(--motion-insert-base) var(--ease-soft) both;
}

.rail-card-fresh {
  position: relative;
  border-color: var(--freshness-warm);
}

.rail-fresh-whisper {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--freshness-warm);
  color: var(--gold);
  font-family: var(--font-display);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  pointer-events: none;
}

/* PROMPT-192 (QA-18 §market-freshness-signals): type-aware freshness
   chip. Replaces the legacy PROMPT-134 "Just added" generic whisper
   with per-record-type framing ("Recently listed" / "Recently wanted"
   / "Recently observed" / "Recently matched" / "Recently added").
   Same corner position as the legacy whisper so the visual rhythm is
   preserved; chip text is now semantic. All values reference EXISTING
   tokens — no new colors. Subtle per-tone modulation (offer/want →
   native mint; signal → observed warm; match → neutral) layers
   gently on the calm base. */
.freshness-chip {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--tint-bg);
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  pointer-events: none;
  max-width: calc(100% - 16px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.freshness-chip[data-freshness-tone="offer"],
.freshness-chip[data-freshness-tone="want"] {
  background: var(--tint-mint);
  color: var(--green-dark);
}
.freshness-chip[data-freshness-tone="signal"] {
  background: var(--tint-warm);
  color: var(--gold);
}
.freshness-chip[data-freshness-tone="match"] {
  background: var(--tint-blue);
  color: var(--blue);
}
.freshness-chip[data-freshness-tone="generic"] {
  background: var(--tint-bg);
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .live-signal-dot {
    animation: none;
    opacity: 0.85;
  }
  .rail-card-entering {
    animation: none;
  }
}

/* PROMPT-138 (QA-13 Phase 8 — Cinematic Homepage Conversion): the
   homepage is now a stack of editorial shelves. The discover-rails
   gap is amplified for cinematic vertical pacing — luxury retail
   spacing, not feed-density. The hero shelf variant gets larger
   cards, more breathing, and a slightly brighter title scale so it
   reads as the storefront window without crossing into landing-
   page bombast. NO new card primitives — .rail-section--hero scopes
   into the existing .rail-card so all card behavior (hover, focus,
   featured-mark, fresh whisper) is reused unchanged. NO new
   animations — the hero shelf inherits the existing tactile stack.
   NO heavy library — pure CSS scoping. */
.topbar-rail-native {
  margin-bottom: 8px;
}

/* PROMPT-142 (QA-13 phase 12 — Shelf Rhythm + Spatial Pacing):
   cadence classes modulate vertical pressure per rail role. Each
   class scopes ONLY spacing + heading opacity — no animations, no
   colors, no card-primitive changes. The base .discover-rails gap
   is the rest beat; cadence classes ADD margin pressure on top.

   Cadence ladder (ascending pressure):
     dense   → tight gap, slight heading mute
     quiet   → normal gap, slight heading mute
     normal  → no override (default)
     breathe → extra top margin (emotional peak)
     hero    → most top margin (storefront window) + larger title +
               larger card (PROMPT-138 contract preserved) */

/* §dense: utility / secondary rails (Buyer Wants / Notable Wants /
   Market Signals). Tightens the gap above so they don't compete
   with editorial peaks; calmer heading opacity so they read as
   supporting material. */
.rail-section--dense {
  margin-top: calc(var(--rail-gap-tight) - var(--rail-gap-normal));
}

.rail-section--dense .rail-heading {
  opacity: 0.85;
}

/* §quiet: decompression rails between intense peaks (Recently
   Surfaced after Featured Grails). Normal gap, slightly muted
   heading so the rail reads as a "rest" beat instead of competing
   for attention. */
.rail-section--quiet .rail-heading {
  opacity: 0.92;
}

/* §breathe: emotional peak rails (Featured Grails). Extra top
   margin so the peak isn't crushed against neighbors. */
.rail-section--breathe {
  margin-top: calc(var(--rail-gap-breathe) - var(--rail-gap-normal));
}

/* §hero: storefront window. PROMPT-138's contract preserved — the
   hero rail gets larger media, larger title, larger card. PROMPT-142
   adds spacing pressure via the cadence ladder. */
.rail-section--hero {
  margin-top: calc(var(--rail-gap-hero) - var(--rail-gap-normal));
  padding: 4px 0 10px;
}

.rail-section--hero .rail-title {
  font-size: var(--type-xl);
  letter-spacing: var(--tracking-tight);
}

.rail-section--hero .rail-subtitle {
  margin-top: 4px;
  font-size: var(--type-base);
  color: var(--muted);
}

.rail-section--hero .rail-track {
  /* A touch more inter-card gap so the storefront window feels
     more spatial than the regular shelves. */
  gap: 18px;
}

.rail-section--hero .rail-card {
  /* Larger cinematic card. Width reuses the existing scroll-snap
     primitive so touch + keyboard behavior is unchanged — only
     the basis grows. */
  flex-basis: 320px;
  padding: 14px;
  border-radius: 14px;
}

.rail-section--hero .rail-card .card-media {
  /* Larger media surface so collector photography reads at
     storefront-window scale. The existing .card-media handles its
     own aspect-ratio + fallback glyph; we only adjust the floor. */
  min-height: 220px;
}

.rail-section--hero .rail-card-title {
  font-size: var(--type-lg);
  -webkit-line-clamp: 2;
}

.rail-section--hero .rail-card-price {
  font-size: var(--type-lg);
}

/* §first-rail-no-margin: when the first rail in a stack carries
   a hero/breathe cadence the calc'd margin-top would push the
   whole gallery downward unnecessarily. Strip it on the first
   child so the page-level header still anchors the start. */
.discover-rails > .rail-section:first-child.rail-section--hero,
.discover-rails > .rail-section:first-child.rail-section--breathe {
  margin-top: 0;
}

@media (max-width: 768px) {
  /* PROMPT-142 §mobile-pacing: amplified gaps on small screens so
     one shelf at a time feels intentional. Cadence math stays
     identical — only the base tokens narrow. */
  :root {
    --rail-gap-tight:   16px;
    --rail-gap-normal:  32px;
    --rail-gap-breathe: 44px;
    --rail-gap-hero:    52px;
  }

  /* On mobile the hero card stays larger than the regular rails
     but doesn't dominate the viewport — collectors still need to
     swipe through.

     PROMPT-184 (QA-18 N1 — Storefront Window Mobile Overflow Fix):
     adds `max-width: calc(100vw - 32px)` so even if a future
     cascade change pushes flex-basis larger, the card cannot
     bleed past the right edge of the viewport. The 32px subtract
     matches main's mobile horizontal padding (16px × 2). With
     box-sizing: border-box globally, the cap counts padding +
     border inside the max-width — outer box fits in the visible
     area. Defense-in-depth alongside the matching cap on
     .rail-single-card > .rail-card above. */
  .rail-section--hero .rail-card {
    flex-basis: 240px;
    max-width: calc(100vw - 32px);
  }

  .rail-section--hero .rail-card .card-media {
    min-height: 160px;
  }
}

/* PROMPT-149 §H1: native HTML5 <dialog> Post Want host. The
   homepage no longer mounts the inline form — collectors open it
   via the topbar / item-pedestal / item-not-found / account-page
   CTAs. The browser handles modal stacking via the top layer +
   the ::backdrop pseudo-element; we only style the surface.
   Reuses existing surface / line / ink / muted / motion tokens —
   no new colors. The PROMPT-138 .market-layout--rail-native
   wrapper is also retired (it only existed to host the form). */
.post-want-dialog {
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 24px;
  background: var(--surface);
  color: var(--ink);
  max-width: 720px;
  width: calc(100vw - 32px);
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  /* Reset the default <dialog> margin: 0 so showModal() centers
     the dialog correctly on the top layer. */
  margin: auto;
}

.post-want-dialog::backdrop {
  /* Calm dim instead of pure black — collector environment tone. */
  background: rgba(20, 28, 25, 0.55);
}

.post-want-dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
  gap: 12px;
}

.post-want-dialog-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-xl);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

/* PROMPT-226 (Human-Centered UI Warmth Pass): short human subtitle
   below the dialog title. Sits in the calm muted register —
   var(--muted) color, no font-weight bump, modest line-height. Used
   by both .post-want-dialog and .add-listing-dialog (the listing
   dialog extends .post-want-dialog so it inherits this rule). */
.post-want-dialog-subtitle {
  margin: 6px 0 14px;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-base);
  line-height: var(--leading-snug);
}

/* PROMPT-226: small plain-English intro line above the homepage
   discover rails. Calm muted register — a curator's note, not a
   marketing hero. Reading width capped so the sentence stays
   intimate. Preserves the PROMPT-138 "rails ARE the homepage"
   design intent — this is a single quiet paragraph, never a
   chrome block. */
.homepage-intro {
  margin: 0 0 18px;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-base);
  line-height: var(--leading-snug);
  max-width: 540px;
}

/* PROMPT-237 (Session Continuity + Return Experience): one-shot
   return acknowledgment that appears above the discover rails when
   local browse memory holds a recognized category world. Calm muted
   register, slightly smaller than .homepage-intro so the visual
   hierarchy reads: continuity acknowledgment → curator's intro →
   rails. The line is built by buildHomepageContinuityHtml() and is
   suppressed entirely (empty string returned) when memory is absent
   or the inferred world is unrecognized, so first-time visitors see
   no chrome here. Title attribute on the <p> names the privacy
   posture explicitly (remembered on this device only). */
.homepage-continuity {
  margin: 0 0 12px;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--type-sm);
  line-height: var(--leading-snug);
  max-width: 540px;
}

/* PROMPT-226: short helper text above the sidebar Google sign-in
   button. Mirrors the explainer copy already present on the
   /account anonymous panel so the surfaces feel like the same
   affordance. Calm muted register; no font-weight bump. */
.account-signin-explainer {
  margin: 0 0 8px;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.78rem;
  line-height: var(--leading-snug);
}

.post-want-dialog-close {
  /* The close affordance reuses .ghost-button surface but
     compresses the padding so it sits as a quiet × in the corner. */
  min-height: 32px;
  padding: 0 10px;
  font-size: 1rem;
  font-weight: 500;
}

@media (max-width: 768px) {
  .post-want-dialog {
    padding: 18px;
    border-radius: 12px;
    width: calc(100vw - 16px);
    max-height: calc(100vh - 32px);
  }
  .post-want-dialog-title {
    font-size: var(--type-lg);
  }
}

/* PROMPT-160 §6.2 (QA-15 §G3 + QA-16 §A1 closure): Add Listing
   dialog shares the .post-want-dialog surface (same radius / same
   backdrop tone / same close positioning / same overlay gravity).
   The .add-listing-dialog modifier exists so the host can
   neutralize the inline form's legacy border + padding (the
   listing form was originally designed as an inline panel inside
   the /listings route and carries its own visual frame). When
   mounted in the dialog body, that inner frame would double-up
   the visual chrome — so we collapse it here without rewriting
   buildListingFormHtml. The .add-listing-dialog-body element is
   the host that openAddListingDialog populates on open. */
.add-listing-dialog .add-listing-dialog-body {
  /* Container for the dynamically-mounted listing form. */
}

.add-listing-dialog .add-listing-dialog-body > [id$="AddListingForm"] {
  /* Neutralize the inline-panel chrome that buildListingFormHtml
     ships with — the dialog itself already provides border /
     radius / padding via .post-want-dialog. The form's own frame
     would render as a nested box inside the dialog otherwise. */
  display: block !important;
  border: 0;
  padding: 0;
  margin: 0;
}

/* PROMPT-230 (Card Density + Aspect Ratio Compression): mobile-only
   density refinements. Collectors on mobile reported oversized
   image zones, repeated empty-placeholder dominance, and vertical
   dead space across rails — each rail consumed ~285px of vertical
   space, so only 2-3 rails were visible per scroll. Compressing
   the card-media aspect ratio + tightening rail-card padding +
   gap reclaims ~40px of vertical space per card with NO content
   change: the same provenance / freshness / title / price all
   render, just with less air above each.
   Mobile-only — every change wraps inside @media (max-width: 768px).
   Desktop card proportions are unchanged. Hero (Storefront Window)
   keeps its existing min-height: 160px floor on mobile so it
   stays subtly taller than regular rails (hierarchy preserved). */
@media (max-width: 768px) {
  /* PROMPT-230 §card-media-aspect: 4/3 (180px tall on a 240px card)
     compressed to 5/3 (144px tall) on mobile. -36px per card.
     Image still legible at this size for typical sports-card /
     trading-card photography; collector identity / canonical name
     readable. Desktop aspect-ratio: 4/3 (line ~1080) preserved
     entirely — wider viewports keep the larger media surface. */
  .card-media {
    aspect-ratio: 5 / 3;
  }

  /* PROMPT-230 §card-media-glyph-aspect: placeholder cards (no
     imageUrl in the record OR skeleton-loading state) used a 5/2
     aspect ratio that read as dead air on mobile. Tightened to
     3/1 on mobile (80px tall on a 240px card) — the glyph remains
     centered and atmospheric (PROMPT-187 data-world cascade
     unchanged) but no longer dominates the card. -16px per
     glyph-only card. */
  .card-media-glyph {
    aspect-ratio: 3 / 1;
  }

  /* PROMPT-230 §rail-card-padding: 10px → 8px on mobile. Saves
     4px (2px top + 2px bottom — minus the shared) per card.
     Combined with the gap reduction below, each card body's
     content-area gains ~6px of breathing room without any text
     reflow. Desktop 10px padding preserved. PROMPT-201 bleed
     containment (overflow: hidden) preserved by virtue of being
     declared inside the base .rail-card rule, not here. */
  .rail-card {
    padding: 8px;
    gap: 6px;
  }
}

/* PROMPT-232 (Native Scroll + Safari Polish Pass): mobile-browser
   polish targeting iPhone Safari + Android Chrome friction. Six
   concrete CSS-native fixes — no JS scroll managers, no body-lock
   libraries, no PWA assumptions, no user-scalable=no, no
   maximum-scale. Accessibility zoom preserved end-to-end. PROMPT-228
   rail physics, PROMPT-229 touch feedback, PROMPT-230 mobile
   density, and PROMPT-231 hierarchy all preserved.

   Audit findings + remediations:
   (1) iOS Safari auto-zooms when input font-size < 16px. Base inputs
       used var(--type-base) = 0.95rem = 15.2px → every form field
       triggered focus-zoom. Mobile-only bump to 16px keeps desktop
       visual unchanged.
   (2) Native <dialog> max-height: calc(100vh - ...) was clipped by
       the iOS soft keyboard (vh doesn't shrink with the keyboard).
       Switched to 100dvh (dynamic viewport height) which contracts
       when the keyboard opens — submit button stays visible.
   (3) Account drawer max-height same fix.
   (4) safe-area-inset env() applied to mobile-nav-strip top padding
       (landscape notch clearance), main bottom padding (home
       indicator clearance), toast bottom positioning, and the
       mobile-only floating sidebar so content never sits under the
       system chrome on iPhone with notch / dynamic island.
   (5) touch-action: manipulation on .primary-button / .ghost-button
       / item/record/match-clickable suppresses iOS double-tap zoom
       + 300ms tap delay on those specific surfaces. Pinch zoom is
       UNTOUCHED (page-level accessibility zoom preserved). Pan
       touch-action on .rail-track (PROMPT-201) preserved verbatim.
   (6) scroll-padding-bottom on .post-want-dialog ensures inputs
       focused deep in a long form scroll above the soft keyboard. */

/* PROMPT-232 §input-no-zoom: bump form-control font-size to 16px on
   mobile so iOS Safari doesn't auto-zoom on input focus. Desktop
   visual is unaffected (the @media gate keeps the base var(--type-base)
   = 0.95rem on widths > 768px). The bump applies to every form
   control type so wants form / listing form / search bar / profile
   edit / dev login / message compose all benefit consistently. */
@media (max-width: 768px) {
  input,
  select,
  textarea,
  .search-bar input[type="search"] {
    font-size: 16px;
  }
}

/* PROMPT-232 §dialog-dvh: switch dialog + drawer max-height from
   100vh to 100dvh so the soft keyboard doesn't clip the dialog's
   submit button on iPhone Safari. 100dvh contracts when the
   keyboard opens; 100vh does not (vh is anchored to the largest
   viewport including the address bar + excluding the keyboard). */
.account-drawer {
  max-height: calc(100dvh - 24px);
}

.post-want-dialog {
  max-height: calc(100dvh - 64px);
  /* PROMPT-232 §keyboard-aware-scroll: when an input deep in a long
     form is focused, the dialog should scroll the input above the
     soft keyboard. scroll-padding-bottom on the scroll container
     reserves space at the bottom of the scrollport so focused
     inputs land above (rather than behind) the keyboard. 96px is
     the visible-keyboard reservation on the most common iPhone
     viewports. */
  scroll-padding-bottom: 96px;
}

@media (max-width: 768px) {
  .post-want-dialog {
    max-height: calc(100dvh - 32px);
  }
}

/* PROMPT-232 §safe-area-inset: clear iPhone notch / dynamic island
   in landscape and the home indicator in portrait. env(safe-area-
   inset-*) returns 0px on devices without insets (and on Android),
   so the rule is a no-op outside iOS — no desktop regression. The
   mobile-nav-strip is the topmost visible element on mobile; main
   ends at the bottom of the page; the toast lives bottom-right.
   All three earn safe-area clearance.

   Note on iPhone landscape: the notch occupies horizontal space on
   the left in landscape-left orientation and on the right in
   landscape-right. Padding left + right on .mobile-nav-strip covers
   both. */
@media (max-width: 768px) {
  .mobile-nav-strip {
    padding-top: max(10px, env(safe-area-inset-top));
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
  }

  main {
    padding-bottom: max(14px, env(safe-area-inset-bottom));
  }

  /* The floating mobile sidebar already uses 100dvh (PROMPT-?
     earlier fix), but its top offset can collide with the notch
     in landscape. Add safe-area-inset-top to the top offset. */
  .sidebar {
    top: max(12px, env(safe-area-inset-top));
  }
}

.toast {
  bottom: max(20px, env(safe-area-inset-bottom));
  right: max(20px, env(safe-area-inset-right));
}

/* PROMPT-232 §touch-action-manipulation: suppress iOS Safari's
   300ms tap delay + double-tap-zoom on interactive surfaces that
   don't need pan-x/pan-y discrimination. .rail-track keeps its
   PROMPT-201 `touch-action: pan-x pan-y` because it MUST allow
   the browser to discriminate horizontal-card-scroll from
   vertical-page-scroll. Buttons + clickable cards don't have
   that need — `manipulation` is the right call there.

   IMPORTANT: this is NOT a global suppression — page-level pinch
   zoom (accessibility zoom) is preserved because touch-action on
   a CHILD element only affects that element's gestures, not the
   document. The viewport meta tag does NOT carry user-scalable=no
   or maximum-scale=1 — accessibility zoom remains available. */
.primary-button,
.ghost-button,
.item-clickable,
.record-clickable,
.match-clickable,
.nav-item,
.nav-hamburger,
.mobile-brand-text,
.account-google-signin {
  touch-action: manipulation;
}

/* PROMPT-233 (Visual Rhythm Humanization Pass): subtle editorial
   variations layered on top of the PROMPT-142 cadence vocabulary.
   The cadence ladder (hero / breathe / normal / quiet / dense) was
   established as a SPACING system; this pass adds TYPOGRAPHIC and
   SETTLING variation within the same vocabulary so the eye reads
   editorial pacing instead of a perfect mathematical ladder.

   Anti-pattern rejections (re-pinned by tests): no random spacing
   chaos (every override is targeted to an existing cadence class),
   no decorative dividers (no new ::before / ::after lines, no
   horizontal-rule injections), no neumorphism / gradients /
   glassmorphism (no shadows, no backdrop-filter, no gradient
   backgrounds), no masonry (.rail-track stays flex; .rail-card
   stays uniform width), no artificial asymmetry (no nth-child
   pseudo-pattern), no animation-heavy work (zero new @keyframes,
   zero new transitions).

   Six surgical changes:
   (1) .section-heading bottom margin 12px → 18px: route headers
       get an editorial settling beat before content begins.
   (2) .homepage-intro bottom margin 18px → 22px: curator's intro
       gets a slightly longer beat before the rails open.
   (3) .discover-rails bottom margin 24px → 36px: rail stack ends
       with editorial settling before the close footer.
   (4) .rail-section--breathe .rail-title letter-spacing nudged
       from --tracking-tight to --tracking-snug: emotional peaks
       read as curated editorial choice rather than mechanical
       duplication of normal-cadence titles.
   (5) .rail-section--quiet .rail-subtitle italicized: decompression
       rails read as a curator's whisper (italics are an editorial
       convention, not decoration).
   (6) .rail-section--dense .rail-subtitle compressed to --type-xs:
       utility-cadence rails (Buyer Wants / Market Signals) feel
       intentionally smaller — hierarchy compression that reads
       as an editorial choice rather than identical-shelf stacking.

   PROMPT-142 contracts ALL preserved: cadence rules still apply
   margin-top via --rail-gap-* tokens; quiet/dense .rail-heading
   still mute opacity; first-child override still strips top
   margin on hero/breathe. PROMPT-228/229/232 untouched. */

.section-heading {
  /* PROMPT-233 §section-heading-settling: was margin: 4px 0 12px;
     editorial breathing before content. The bottom bump is +6px
     across every route header — calm, not redesigned. */
  margin: 4px 0 18px;
}

.homepage-intro {
  /* PROMPT-233 §intro-to-rails-beat: was margin: 0 0 18px. +4px
     gives the curator's intro a settling pause before the rails
     begin. Subtle — still single-paragraph, still calm. */
  margin: 0 0 22px;
}

.discover-rails {
  /* PROMPT-233 §rail-stack-settling: was margin-bottom: 24px.
     +12px gives the rail stack an editorial bottom beat before the
     close footer. Still calm — the close footer's own internal
     spacing handles the inverse beat. */
  margin-bottom: 36px;
}

/* PROMPT-233 §breathe-typographic-variation: emotional-peak rails
   (Featured Grails) get slightly looser title tracking so the peak
   reads as a curated editorial choice rather than a mechanical
   duplicate of normal-cadence titles. Same font, same size, same
   weight — only letter-spacing changes from --tracking-tight to
   --tracking-snug. The hero rail (PROMPT-138) keeps its larger
   cinematic title unchanged. */
.rail-section--breathe .rail-title {
  letter-spacing: var(--tracking-snug);
}

/* PROMPT-233 §quiet-subtitle-italic: decompression rails (Recently
   Surfaced, market-wants, etc.) get an italic subtitle so the rail
   reads as a curator's whisper. Italics are an editorial typography
   convention (not decoration) — they signal "this is a quieter
   editorial aside" without changing color, size, or weight. */
.rail-section--quiet .rail-subtitle {
  font-style: italic;
}

/* PROMPT-233 §dense-subtitle-compress: utility-cadence rails get a
   slightly smaller subtitle (--type-xs instead of --type-sm) so the
   strip reads as intentionally compressed — hierarchy signal that
   says "this is supporting material, not a feature." Pairs with
   the PROMPT-142 opacity mute on .rail-section--dense .rail-heading
   to compound the secondary-tier visual register. */
.rail-section--dense .rail-subtitle {
  font-size: var(--type-xs);
}
