/* ============================================================================
   Marketplace grid — the movie grid.

   Two columns, ~6 items a screen, scroll for more. One tile in each group is
   visibly the biggest, and the biggest tile is the one playing. Size and
   playback go together — that pairing is what makes it read like a wall of
   movie posters rather than a uniform sheet of thumbnails.

   Hero positions are FIXED BY POSITION, not reassigned as you scroll. A hero
   that travelled would resize tiles mid-scroll, which is exactly the layout
   shift we are trying to avoid.

   Flat and 2D. A tile is lifted by a hairline, never a shadow.
   ============================================================================ */

.grid {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  scrollbar-width: none;

  padding: var(--s-3);
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--s-3);

  /* Masonry by row span.
     A fine 8px row unit gives enough resolution to pick real height variety,
     and `dense` lets a short card slot into the gap beside a tall one so the
     columns interlock instead of forming rigid rows.

     Row gap is 0 on purpose: the visual gap comes from each card's bottom
     margin, which sits INSIDE its grid area. Using row-gap instead would
     multiply the gap by the span count and blow the rhythm apart. */
  grid-auto-rows: 8px;
  row-gap: 0;
  grid-auto-flow: row dense;
  align-content: start;
}

.grid::-webkit-scrollbar { display: none; }

/* The marketplace column: a fixed filter bar above a scrolling grid. */
.market {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ---------------------------------------------------------------- search -- */

.searchbar {
  flex: 0 0 auto;
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-3) var(--s-3) 0;
  padding: 0 var(--s-3);
  height: 40px;
  border-radius: var(--r-md);
  background: var(--c-raised);
  border: var(--border-hairline);
}

.searchbar:focus-within { border-color: var(--c-line-strong); }

.searchbar-icon { width: 17px; height: 17px; color: var(--c-text-faint); flex-shrink: 0; }

.searchbar input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  background: none;
  border: none;
  color: var(--c-text);
  font-size: var(--t-subhead);
}

.searchbar input::placeholder { color: var(--c-text-faint); }
.searchbar input::-webkit-search-cancel-button { display: none; }

.searchbar-clear[hidden] { display: none; }

.searchbar-clear {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  min-height: 0;
  min-width: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  background: var(--c-line);
  color: var(--c-text-muted);
}

/* The result count. Inside the bar, in space the bar already holds open, so it
   cannot shift the feed no matter how fast someone types. It was previously a
   block above the grid, which made the whole page jump on every keystroke. */
.searchbar-count {
  flex-shrink: 0;
  min-width: 20px;
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  background: var(--c-line);
  color: var(--c-text-muted);
  font-size: var(--t-caption-2);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.searchbar-count.is-empty { color: var(--c-warning); }
.searchbar-count[hidden] { display: none; }

/* ------------------------------------------------------------- filters -- */

.grid-chips {
  flex: 0 0 auto;
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  scrollbar-width: none;
  padding: var(--s-3) var(--s-3) var(--s-2);
  background: var(--c-ground);
  border-bottom: var(--border-faint);
}

.grid-chips::-webkit-scrollbar { display: none; }

.chip {
  flex-shrink: 0;
  height: 32px;
  padding: 0 var(--s-3);
  min-height: 0;
  border-radius: var(--r-pill);
  border: var(--border-hairline);
  background: transparent;
  color: var(--c-text-muted);
  font-size: var(--t-footnote);
  font-weight: var(--fw-semibold);
  display: inline-flex;
  align-items: center;
}

.chip[aria-pressed="true"] {
  background: var(--c-gold);
  border-color: var(--c-gold);
  color: var(--c-text-on-gold);
  font-weight: var(--fw-bold);
}

.chip:active { opacity: 0.65; }

/* ---------------------------------------------------------------- tile -- */

.gtile {
  position: relative;
  height: auto;
  min-height: 0;
  /* Margin lives inside the grid area, so it draws the row gap without
     inflating the span arithmetic. */
  margin-bottom: var(--s-3);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--c-raised);
  border: var(--border-hairline);

  -webkit-touch-callout: none;
  touch-action: manipulation;
}

/* ---------------------------------------------------------- card sizes --
   Four deliberate heights against a ~197px column. Assigned by position in
   grid.js, never reassigned on scroll, so the composition is stable and reads
   as intentional rather than random.

     short   ~196px   roughly square
     medium  ~236px
     tall    ~292px
     featured ~356px  the one that plays
*/

.gtile.s-short    { grid-row: span 26; }
.gtile.s-medium   { grid-row: span 31; }
.gtile.s-tall     { grid-row: span 38; }
.gtile.s-featured { grid-row: span 46; }

/* Featured earns a touch more presence without shouting. */
.gtile.s-featured .gtile-price { font-size: var(--t-callout); }
.gtile.s-featured .gtile-name  { font-size: var(--t-footnote); }

.gtile-poster,
.gtile-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 42%;   /* rooms read better weighted above centre */
  display: block;
}

.gtile-video {
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}

.gtile.is-live .gtile-video,
.gtile.is-held .gtile-video { opacity: 1; }

.gtile.is-live { border-color: var(--c-gold-edge); }
.gtile.is-held { border-color: var(--c-gold); }

.gtile.is-loading .gtile-poster { opacity: 0; }

@media (hover: hover) {
  .gtile { transition: border-color var(--dur-fast) var(--ease-out); }
  .gtile:hover { border-color: var(--c-line-strong); }
  .gtile:hover .gtile-poster { transform: scale(1.02); }
  .gtile-poster { transition: transform var(--dur-slow) var(--ease-out); }
}

/* --- scrim --- */

.gtile::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 46%;
  background: linear-gradient(to top,
    rgba(0,0,0,0.86) 0%, rgba(0,0,0,0.38) 46%, transparent 100%);
  pointer-events: none;
}

/* --- content --- */

.gtile-body {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 2;
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: 3px;
  pointer-events: none;
}

/* Line 1: what it is. */
.gtile-title {
  font-size: var(--t-footnote);
  font-weight: var(--fw-semibold);
  color: var(--c-text);
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gtile.s-featured .gtile-title { font-size: var(--t-callout); }

/* Line 2: who is selling it, and how much interest it has. One muted row so
   the card carries four facts without reading as four things. */
.gtile-meta {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  min-width: 0;
  font-size: var(--t-caption-2);
  color: rgba(255, 255, 255, 0.62);
}

.gtile-avatar {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.18);
  color: var(--c-text);
  font-size: 9px;
  font-weight: var(--fw-bold);
  line-height: 1;
}

.gtile-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gtile-seal {
  flex-shrink: 0;
  width: 11px;
  height: 11px;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--c-gold);
  color: var(--c-text-on-gold);
  font-size: 7px;
  font-weight: var(--fw-bold);
  line-height: 1;
}

.gtile-views {
  flex-shrink: 0;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-variant-numeric: tabular-nums;
}

.gtile-views svg { width: 11px; height: 11px; opacity: 0.8; }

/* --- one status pill, top left ------------------------------------------
   Exactly one, ever. Priority: SOLD OUT > BUMPED > NEW. Stacking a "bumped"
   and a "new" pill on the same card made it look busy and said nothing extra —
   a bumped listing is almost always new anyway. Listings are cleared every
   week or two, so NEW means recently posted, not "unused item". */

.gtile-status {
  position: absolute;
  top: var(--s-2);
  left: var(--s-2);
  z-index: 3;
  font-size: var(--t-caption-2);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1;
  padding: 5px var(--s-2);
  border-radius: var(--r-pill);
  pointer-events: none;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.gtile-status:empty { display: none; }

.gtile-status[data-s="new"] {
  color: var(--c-text);
  background: rgba(255, 255, 255, 0.16);
}

.gtile-status[data-s="bumped"] {
  color: var(--c-text-on-gold);
  background: var(--c-gold);
}

.gtile-status[data-s="sold"] {
  color: var(--c-text);
  background: rgba(0, 0, 0, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.35);
}

/* --- price pill, top right ----------------------------------------------
   Price is the single most important fact for a buyer, so it gets the other
   corner. This replaced the mute control: nobody buying a mattress needs
   audio, and the corner is worth more than the feature. */

.gtile-price {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  z-index: 3;
  font-family: var(--font-num);
  font-size: var(--t-caption);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  padding: 5px var(--s-2);
  border-radius: var(--r-pill);
  color: var(--c-text);
  background: rgba(0, 0, 0, 0.66);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  pointer-events: none;
}

.gtile.s-featured .gtile-price,
.gtile.s-tall .gtile-price { font-size: var(--t-footnote); }

/* --- sold --- */

.gtile.is-sold .gtile-poster,
.gtile.is-sold .gtile-video { filter: grayscale(1) brightness(0.5); }

/* --- saved heart --- */

.gtile-heart {
  position: absolute;
  right: var(--s-2);
  bottom: var(--s-2);
  z-index: 3;
  width: 22px;
  height: 22px;
  display: none;
  place-items: center;
  color: var(--c-danger);
  pointer-events: none;
}

.gtile.is-saved .gtile-heart { display: grid; }
.gtile-heart svg { width: 16px; height: 16px; }

/* --------------------------------------------------------------- rails -- */

.grid-end {
  grid-column: 1 / -1;
  align-self: start;
  padding: var(--s-8) var(--s-4) var(--s-12);
  text-align: center;
  color: var(--c-text-muted);
  font-size: var(--t-footnote);

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
}

/* The one invitation to post a request left in the feed, and it only appears
   where the catalogue has just failed the buyer: a search with no results, or
   the very end of the listings. Gold because at that moment it IS the primary
   action on screen — there is nothing else here to do. */
.grid-end-ask {
  min-height: 44px;
  padding: 0 var(--s-5);
  border-radius: var(--r-pill);
  border: 1px solid var(--c-gold);
  background: var(--c-gold);
  color: var(--c-text-on-gold);
  font-size: var(--t-footnote);
  font-weight: var(--fw-bold);
}

.grid-end-ask:active { opacity: 0.7; }

/* A pinned pill keeps its place at the front of the rail. The dot is the only
   marker it needs — a label would cost more room than theinformation is worth. */
.chip.is-pinned::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--c-gold);
  margin-right: var(--s-2);
  flex-shrink: 0;
}

.chip.is-pinned[aria-pressed="true"]::before { background: var(--c-text-on-gold); }

/* The animated preview is gone.

   Tiles used to flip a thirty-frame sprite sheet with CSS `steps()` — 38 KB a
   tile, 230 KB a screen, to imply motion that could never turn into the actual
   video. The grid now shows the seller's chosen cover frame at 5 KB and spends
   the difference warming the real clip's opening segment, so hovering hands
   over to genuine playback instead of to a different fake. See js/feed/warm.js.

   Nothing replaces the rules that were here: a poster and a <video> is the
   whole stack now. */

/* ============================================================================
   The Magic Cursor
   ============================================================================ */

/* The cursor belongs to the feed and to nothing else.
   With a listing or the compose sheet open it was still floating over the top,
   parked in the middle of the spec table — pointing at a grid the user can no
   longer see. It comes back the moment the sheet closes. */
/* The puck belongs to the feed. Whenever anything is stacked over the feed it
   has nothing to point at, and left visible it floats on top of the sheet —
   over the demand board's own header, in the case that caught this. */
body:has(.lb.is-open) .magic,
body:has(.ask.is-open) .magic,
body:has(.bd.is-open) .magic,
body:has(.panel.is-open) .magic,
body:has(.pf.is-open) .magic,
body:has(.studio.is-open) .magic {
  opacity: 0;
  pointer-events: none;
}

.magic {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-toast);
  pointer-events: none;          /* must not block elementFromPoint */
}

.magic[hidden] { display: none; }

.magic-dot,
.magic-ring {
  position: fixed;
  border-radius: var(--r-pill);
  transform: translate(-50%, -50%);
}

/* The dot tracks the pointer exactly. */
.magic-dot {
  left: var(--x, 0);
  top: var(--y, 0);
  width: 7px;
  height: 7px;
  background: var(--c-gold);
}

/* The ring lags behind, which is what makes it read as one object with weight
   rather than two things that happen to move together. */
.magic-ring {
  left: var(--rx, 0);
  top: var(--ry, 0);
  width: 30px;
  height: 30px;
  border: 1.5px solid var(--c-gold-edge);
  transition: width var(--dur-fast) var(--ease-out),
              height var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

/* Locked on a tile: the ring tightens onto the dot. */
.magic.is-locked .magic-ring {
  width: 16px;
  height: 16px;
  border-color: var(--c-gold);
}

/* --- touch: a puck you drag --- */

.magic.is-puck {
  pointer-events: auto;          /* this one IS grabbable */
  touch-action: none;
}

.magic.is-puck .magic-ring {
  width: 46px;
  height: 46px;
  border-width: 2px;
  border-color: var(--c-gold);
  background: var(--c-scrim);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.magic.is-puck .magic-dot { width: 9px; height: 9px; }

.magic.is-puck.is-dragging .magic-ring {
  width: 56px;
  height: 56px;
  border-color: var(--c-text);
}

/* Hide the native arrow over the grid; the dot is the cursor now. */
@media (hover: hover) {
  .grid, .grid * { cursor: none; }
}

/* --- the tile the cursor has claimed ------------------------------------
   A solid ring, not a glow. It has to be obvious which card is live without
   breaking the flat, 2D language the rest of the app keeps. */

.gtile.is-cursor {
  border-color: var(--c-gold);
  box-shadow: inset 0 0 0 2px var(--c-gold);
}

.gtile.is-live {
  border-color: var(--c-success);
  box-shadow: inset 0 0 0 2px var(--c-success);
}



.gtile.is-cursor::before { background: var(--c-gold); }

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

/* ============================================================================
   Request cards used to live here — flat text blocks spanning both columns,
   injected among the listings. They are gone: buyer demand now has its own
   board behind the Sell tab, where the people who can act on it actually are.
   The styles for that board live in `panel.css` with the rest of the sheets.
   ============================================================================ */

/* --- ask sheet ------------------------------------------------------------ */

.ask {
  position: absolute;
  inset: 0;
  z-index: var(--z-sheet);
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  padding: var(--s-4);
}

.ask[hidden] { display: none; }

.ask-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.62);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.ask.is-open .ask-backdrop { opacity: 1; }

.ask-card {
  position: relative;
  z-index: 1;
  width: min(380px, 100%);
  max-height: 100%;
  overflow-y: auto;
  scrollbar-width: none;

  background: var(--c-surface);
  border: var(--border-hairline);
  border-radius: var(--r-xl);
  padding: var(--s-6) var(--s-5) var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);

  opacity: 0;
  transform: scale(0.95);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-spring);
}

.ask-card::-webkit-scrollbar { display: none; }
.ask.is-open .ask-card { opacity: 1; transform: scale(1); }

.ask-x {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  width: 30px;
  height: 30px;
  min-height: 0;
  min-width: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  color: var(--c-text-muted);
}

.ask-title {
  font-size: var(--t-title-2);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-heading);
}

.ask-sub { font-size: var(--t-footnote); color: var(--c-text-muted); line-height: var(--lh-body); }

.ask-field { display: flex; flex-direction: column; gap: var(--s-2); }

.ask-label {
  font-size: var(--t-caption-2);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-text-muted);
}

.ask-input {
  height: var(--tap-min);
  padding: 0 var(--s-3);
  border-radius: var(--r-md);
  background: var(--c-raised);
  border: var(--border-hairline);
  color: var(--c-text);
  font-size: var(--t-callout);
}

.ask-input:focus { border-color: var(--c-line-strong); }

.ask-count {
  align-self: flex-end;
  font-size: var(--t-caption);
  color: var(--c-text-faint);
  margin-top: calc(var(--s-2) * -1);
}

.ask-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.ask-chip {
  height: 32px;
  min-height: 0;
  padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  border: var(--border-hairline);
  color: var(--c-text-muted);
  font-size: var(--t-footnote);
  font-weight: var(--fw-semibold);
}

.ask-chip.is-on {
  background: var(--c-gold);
  border-color: var(--c-gold);
  color: var(--c-text-on-gold);
}

.ask-go {
  height: var(--tap-min);
  border-radius: var(--r-md);
  background: var(--c-gold);
  color: var(--c-text-on-gold);
  font-size: var(--t-callout);
  font-weight: var(--fw-bold);
  margin-top: var(--s-1);
}

.ask-go:disabled { opacity: 0.6; }
.ask-go:active { opacity: 0.8; }

.ask-error { font-size: var(--t-footnote); color: var(--c-danger); text-align: center; }

/* ============================================================ partner ====
   The billboard slot.

   Deliberately looks like a listing card in shape and size — it sits in the
   same masonry column and must not break the rhythm — but is unmistakably
   labelled and carries its own button. An ad dressed as a student listing
   poisons the one thing this marketplace has, which is that everything in it
   is real.
   ========================================================================= */

.ptile {
  /* The champagne edge is the only place gold appears in the feed, so a
     billboard reads as different-but-ours rather than as an intrusion. */
  border-color: var(--c-gold-edge);
}

.ptile-badge {
  position: absolute;
  top: var(--s-2);
  left: var(--s-2);
  z-index: 3;
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--c-gold-edge);
  background: rgba(0, 0, 0, 0.55);
  color: var(--c-gold);
  font-size: var(--t-caption-2);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.4px;
}

.ptile-body { gap: var(--s-2); }

.ptile-tag {
  font-size: var(--t-caption);
  color: var(--c-text-muted);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The body is pointer-events: none so the tile stays tappable; the button has
   to opt back in or it cannot be pressed. */
.ptile-cta {
  pointer-events: auto;
  align-self: flex-start;
  margin-top: var(--s-1);
  min-height: 32px;
  padding: 0 var(--s-3);
  display: inline-flex;
  align-items: center;
  border-radius: var(--r-pill);
  background: var(--c-gold);
  color: var(--c-text-on-gold);
  font-size: var(--t-caption);
  font-weight: var(--fw-bold);
  text-decoration: none;
}

.ptile-cta:active { opacity: 0.75; }
