/* ==========================================================================
   AppMates landing page

   Loaded after styles.css and borrows every token from it, so light/dark,
   the accent and shared components behave here exactly as in the app.

   The shape is a marketplace front page, and the rule that shape follows is
   that the cards are the content. Section headings are therefore small and
   muted — a big display heading above a row of listings competes with the
   listings, which are the thing worth looking at. The only large type on
   the page is the hero, which has nothing to compete with.

   Rows scroll horizontally with snap points rather than wrapping into a
   grid: a row that runs off the edge reads as a catalogue that continues,
   where a tidy grid of six reads as everything there is.
   ========================================================================== */

/* `height: auto` (not the `100%` `styles.css` sets for the app shell) lets
   this page grow past the viewport when its content needs it — but a short
   view (`#browseView` with one or two listings, say) must not leave the
   footer stranded right under the content with a slab of bare viewport
   beneath it. `min-height: 100vh` plus a column flex pins it to the
   viewport's bottom edge either way: flush when content is short, pushed
   down normally once content actually needs the scroll. */
.landing-body {
  height: auto;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
main.landing { flex: 1 0 auto; }
.landing-footer { flex-shrink: 0; }
.landing,
.landing-footer { --landing-max: 1120px; }

/* ============================ promoted rails ============================ */

/* Two columns of promoted slots flanking the content, genuinely fixed to
   the viewport — not sticky. A sticky sidebar shorter than the page it
   scrolls beside disengages once its own tail catches up with the row's
   end, which reads as the cards suddenly lurching near the top or the
   bottom of the page. Fixed positioning has no such edge case: the box
   never moves regardless of scroll position, which is the actual property
   being asked for. `landing.js` fills the remaining height with extra
   empty slots at runtime, since how many fit depends on the real rendered
   height of a card, not a number this file can know in advance. */
.rail {
  display: none;
  position: fixed;
  top: 1.25rem;
  height: calc(100vh - 2.5rem);
  width: 190px;
  overflow: hidden;
  flex-direction: column;
  gap: .6rem;
  /* The top card's hover lift (`translateY(-2px)` plus a shadow that blurs
     upward past the element) needs room above it, or `overflow: hidden`
     clips the card's own top edge mid-hover. Padding-bottom would just
     shrink the last card for no reason — hovering never pushes a card
     down into the rail's bottom edge. */
  padding-top: 1rem;
  z-index: 5;
}
.rail-left { left: 1.25rem; }
.rail-right { right: 1.25rem; }

/* Content only makes room for the rails once there is width to spare.
   `--landing-max` shrinks smoothly from the two fixed 190px columns (each
   with its own padding and a breathing gap) up to the page's normal
   1120px, so the rails start appearing well before the viewport is wide
   enough to fit both at full content width. Lives here, not next to
   `.landing`'s own rule, only because this is where the rail geometry it
   depends on is defined. */
@media (min-width: 1180px) {
  .rail { display: flex; }
  .landing,
  .landing-footer {
    --landing-max: min(1120px, calc(100vw - 470px));
  }
}

/* Below the rails' own width, the same slots run as the compact tape
   instead — full-bleed top and bottom of the page rather than flanking
   columns, since there's no width to flank with here. `position: fixed`
   for the same reason the rails use it above: the strip must stay put
   regardless of scroll, not disengage once a shorter page catches up with
   it. Body padding reserves the space so the tapes don't sit over the
   hero on load or the footer at the bottom. */
@media (max-width: 1179px) {
  .landing-tape { display: block; position: fixed; left: 0; right: 0; z-index: 60; }
  .landing-tape.tape-top { top: 0; border-bottom: 1px solid var(--border); }
  .landing-tape.tape-bottom { bottom: 0; border-top: 1px solid var(--border); }
  .landing-body { padding-top: 42px; padding-bottom: 42px; }
}

.rail-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: .4rem;
  padding: .9rem .8rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  color: inherit;
  transition: border-color .12s, transform .1s, box-shadow .12s;
  flex: 1 1 0;
  min-height: 108px;
}
.rail-card:hover {
  text-decoration: none;
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.rail-icon { width: 42px; height: 42px; border-radius: 10px; display: block; object-fit: cover; }
.rail-name {
  font-weight: 700;
  font-size: .82rem;
  line-height: 1.25;
  letter-spacing: -0.01em;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* Only once a name has actually wrapped (see `.name-wrap` below) does it
   get a guaranteed two-line floor — reserving that unconditionally would
   eat into the fixed-height card's budget for every one-line name too,
   squeezing `.rail-genre` down toward nothing. `.rail-card` keeps its
   fixed equal-share height (`flex: 1 1 0`) and must not grow just because
   a name wraps, so this floor is what stops a tight share's shrink from
   landing on the name and clipping its second line mid-glyph; the shrink
   lands on the genre instead, which can't visibly "cut" a single line the
   way an overflow-hidden 2-line box can. */
.rail-card.name-wrap .rail-name {
  flex-shrink: 0;
  min-height: 2.05rem;
}
.rail-genre {
  font-size: .68rem;
  color: var(--muted);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-desc { font-size: .74rem; color: var(--text-2); line-height: 1.4; }
.rail-tag {
  font-size: .56rem;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* `landing.js` adds this once it measures a card's name wrapping to a
   second line — with the icon and genre centered around it via the card's
   flex `gap`, a two-line name otherwise pushes both right up against the
   card's top and bottom padding. Pulling the gap in only for that case
   keeps the common one-line card at its normal, more open spacing. */
.rail-card.name-wrap { gap: .25rem; }

/* The offer, not a placeholder to be styled away later: a dashed slot reads
   as available, which is the whole point of showing it before it sells. */
.rail-card.empty {
  border-style: dashed;
  border-color: var(--border-strong);
  background: none;
  gap: .3rem;
}
.rail-card.empty:hover { border-color: var(--accent); background: var(--surface-2); }
.rail-card.empty .rail-name { color: var(--text-2); font-weight: 650; }

/* A promoted card's colour, picked at request time (see the "Feature your
   app here" dialog below) — a deep tint mixed into the existing
   surface/border tokens rather than a flat hue, so it reads correctly in
   both themes (a rich, moody card in dark mode; a lighter tinted one in
   light mode) without ever fighting the card's own text colours. The base
   hues are the muted, jewel-tone kind a marketplace like trustmrr.com uses
   for its sponsor cards, not a bright candy palette. Kept in sync by hand
   with `RAIL_COLORS` in `landing.js`. */
.rail-card--blue   { background: color-mix(in srgb, #2f5fa8 55%, var(--surface)); border-color: color-mix(in srgb, #2f5fa8 75%, var(--border)); }
.rail-card--green  { background: color-mix(in srgb, #1f7a4d 55%, var(--surface)); border-color: color-mix(in srgb, #1f7a4d 75%, var(--border)); }
.rail-card--violet { background: color-mix(in srgb, #6d3aa8 55%, var(--surface)); border-color: color-mix(in srgb, #6d3aa8 75%, var(--border)); }
.rail-card--orange { background: color-mix(in srgb, #b5502a 55%, var(--surface)); border-color: color-mix(in srgb, #b5502a 75%, var(--border)); }
.rail-card--pink    { background: color-mix(in srgb, #8a2f4a 55%, var(--surface)); border-color: color-mix(in srgb, #8a2f4a 75%, var(--border)); }
.rail-card--teal    { background: color-mix(in srgb, #2c6470 55%, var(--surface)); border-color: color-mix(in srgb, #2c6470 75%, var(--border)); }
.rail-card--red     { background: color-mix(in srgb, #a8342f 55%, var(--surface)); border-color: color-mix(in srgb, #a8342f 75%, var(--border)); }
.rail-card--amber   { background: color-mix(in srgb, #a87e1f 55%, var(--surface)); border-color: color-mix(in srgb, #a87e1f 75%, var(--border)); }
.rail-card--indigo  { background: color-mix(in srgb, #4340a0 55%, var(--surface)); border-color: color-mix(in srgb, #4340a0 75%, var(--border)); }
.rail-card--slate   { background: color-mix(in srgb, #4a5568 55%, var(--surface)); border-color: color-mix(in srgb, #4a5568 75%, var(--border)); }

/* Hovering a coloured card brightens its own border instead of switching
   to the generic purple `.rail-card:hover` above — same specificity (one
   class + :hover each), so it's source order, not weight, that makes
   these win. Full-strength hue rather than the muted `color-mix` the base
   variant uses, so the hover state reads as "lit up", not just "still
   blue". */
.rail-card--blue:hover   { border-color: #2f5fa8; }
.rail-card--green:hover  { border-color: #1f7a4d; }
.rail-card--violet:hover { border-color: #6d3aa8; }
.rail-card--orange:hover { border-color: #b5502a; }
.rail-card--pink:hover   { border-color: #8a2f4a; }
.rail-card--teal:hover   { border-color: #2c6470; }
.rail-card--red:hover    { border-color: #a8342f; }
.rail-card--amber:hover  { border-color: #a87e1f; }
.rail-card--indigo:hover { border-color: #4340a0; }
.rail-card--slate:hover  { border-color: #4a5568; }

/* ============================ promo dialog ============================ */

/* Reuses the app's own `.modal-overlay`/`.modal`/`.field` (see styles.css)
   for everything generic; only the parts unique to this dialog — the
   preview, the app lookup, colour swatches, price — get their own rules
   here. */
.promo-price { display: flex; align-items: baseline; gap: .6rem; margin: 1rem 0 .2rem; }
.promo-price-old { font-size: 1.05rem; color: var(--muted); text-decoration: line-through; }
.promo-price-new { font-size: 1.5rem; font-weight: 700; color: var(--accent); }

.promo-app-status { font-size: .76rem; color: var(--muted); min-height: 1.1em; }
.promo-app-status.error { color: var(--bad); }

.promo-swatches { display: flex; flex-wrap: wrap; gap: .55rem; margin-top: .2rem; }
.promo-swatch {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--swatch);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
  padding: 0;
  cursor: pointer;
  transition: transform .1s, box-shadow .12s;
}
/* `.promo-swatch` is a `<button>`, so it also matches styles.css's generic
   `button:hover:not(:disabled)` — which, thanks to the extra element
   selector, outranks a plain `.promo-swatch:hover` on specificity and wins
   regardless of stylesheet order, repainting the swatch grey and masking
   the very colour it's meant to preview. `button.promo-swatch:hover`
   matches that specificity so this rule's later position in the cascade
   decides it instead — the same fix as `.landing-cta`'s button case. */
.promo-swatch:hover,
button.promo-swatch:hover {
  transform: scale(1.1);
  background: var(--swatch);
  border-color: var(--surface);
}
.promo-swatch.selected { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--swatch); }

.promo-preview-wrap { display: flex; justify-content: center; margin-bottom: 1rem; }
.promo-preview-wrap .rail-card {
  width: 190px;
  flex: none;
  cursor: default;
}
.promo-preview-wrap .rail-card:hover { transform: none; box-shadow: none; }
.promo-preview-icon { font-size: 1.6rem; line-height: 1; }

/* The confirmation step's final card gets the same glint a token-featured
   listing gets (`.listing-tile.featured::after`, `@keyframes tile-streak`
   above) — reused rather than redefined, since it's the same "look how
   nice this could be" moment on a different card shape. Needs its own
   `position`/`overflow` because `.rail-card` itself doesn't carry them —
   the sidebar cards never need to clip anything. */
.rail-card.promo-shine {
  position: relative;
  overflow: hidden;
}
.rail-card.promo-shine::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(
    120deg,
    transparent 0%, transparent 30%,
    rgb(255 255 255 / 38%) 44%,
    rgb(255 255 255 / 52%) 50%,
    rgb(255 255 255 / 38%) 56%,
    transparent 70%, transparent 100%
  ) 0 0 / 200% 100% no-repeat;
  animation: tile-streak 5s ease-in-out infinite;
}
@media (prefers-color-scheme: dark) {
  .rail-card.promo-shine::after {
    background-image: linear-gradient(
      120deg,
      transparent 0%, transparent 30%,
      rgb(255 255 255 / 9%) 44%,
      rgb(255 255 255 / 16%) 50%,
      rgb(255 255 255 / 9%) 56%,
      transparent 70%, transparent 100%
    );
  }
}
@media (prefers-reduced-motion: reduce) {
  .rail-card.promo-shine::after { animation: none; opacity: 0; }
}

/* ============================ sponsor page ============================ */

/* `#sponsorView`'s own sections — an in-page view swapped in for
   `#landingHome` by a same-document hash (see landing.js's `boot()`), not
   a separate page, so the `.rail`/`.rail-card` styles above are shared,
   not duplicated. Reuses `.promo-*`/`.field`/`.status` wholesale for
   anything generic; only the layout unique to this view (a fixed 5-a-side
   grid instead of the rails' elastic viewport-fill, the stats strip, the
   live panel, the numbered steps) gets rules here. */

.sponsor-page[hidden] { display: none; }

/* Nothing below hardcodes a colour: every hue is either a token or a
   `color-mix` onto one, and the globe reads its palette off the same tokens
   at runtime — so dark mode needs no override block anywhere in this
   section. */
.sponsor-microlabel {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- hero stat band ---------- */

/* The four measured numbers, as one row wherever they fit. The 128px floor
   is deliberate rather than round: the landing shell reserves the rails, so
   this band is ~688px at a 1280px viewport, and anything above ~135px tips
   `auto-fit` into three columns and leaves the fourth stat stranded on its
   own line. Below roughly 620px it wraps to a tidy 2x2 instead. */
.sponsor-statband {
  margin: 1.6rem 0 0;
  display: grid;
  gap: 1.4rem 2rem;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
}
.sponsor-stat { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.sponsor-stat dt {
  font-size: clamp(1.7rem, 4vw, 2.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.sponsor-stat dd {
  margin: 0;
  font-size: .72rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: 1.35;
}
/* The one stat a prospective sponsor is actually shopping for. */
.sponsor-stat--ok dt { color: var(--ok); }

/* ---------- live globe panel ---------- */

.sponsor-globe-strip {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  margin: 2rem 0;
}
.sponsor-globe-inner { padding: 1.1rem 1.3rem 1.4rem; display: flex; flex-direction: column; gap: 1rem; }
.sponsor-strip-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .4rem 1rem;
}
.sponsor-strip-note { font-size: .72rem; color: var(--muted); text-decoration: none; }
.sponsor-strip-note:hover { color: var(--text-2); text-decoration: underline; }

.sponsor-globe-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  gap: 1.6rem;
}
@media (min-width: 760px) {
  .sponsor-globe-grid { grid-template-columns: minmax(240px, 360px) 1fr; gap: 2.4rem; }
}
.sponsor-globe-wrap { display: flex; justify-content: center; }
/* Square and transparent on purpose: the globe strokes its own rim, and the
   circular clip this used to carry would have eaten the pin halos exactly
   where they matter, at the limb. `height` is set by the script from the
   measured width, so the box stays square at any column size. */
#sponsorGlobe { width: min(360px, 78vw); max-width: 100%; display: block; }

.sponsor-globe-side { display: flex; flex-direction: column; gap: .9rem; min-width: 0; }
.sponsor-g-count { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
.sponsor-g-num {
  font-size: clamp(2rem, 4.4vw, 2.9rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.sponsor-g-lab { font-size: .86rem; color: var(--text-2); }
.sponsor-g-sub { margin: -.5rem 0 0; font-size: .76rem; color: var(--muted); }

.sponsor-g-feed {
  list-style: none;
  margin: 0;
  padding: .9rem 0 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.sponsor-g-feed li { display: flex; align-items: center; gap: .55rem; min-width: 0; font-size: .78rem; color: var(--text-2); }
.sponsor-g-line { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sponsor-g-ago { color: var(--muted); font-size: .72rem; white-space: nowrap; }
.sponsor-g-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 45%, var(--surface));
}
.sponsor-g-fresh .sponsor-g-line { color: var(--text); }
.sponsor-g-fresh .sponsor-g-dot { background: var(--accent); animation: sponsor-pulse 2s ease-in-out infinite; }
@keyframes sponsor-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .45; transform: scale(1.35); }
}

.sponsor-g-countries { border-top: 1px solid var(--border); padding-top: .9rem; display: flex; flex-direction: column; gap: .6rem; }
.sponsor-g-chips { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.sponsor-g-chip {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  padding: .28rem .55rem;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
}
.sponsor-g-more { font-size: .74rem; color: var(--muted); }

/* Fixed, not absolute: the globe positions this from `clientX`/`clientY`,
   and an absolutely positioned tip would be clipped by the panel's own box
   the moment a pin near the edge was hovered. */
.sponsor-globe-tip {
  position: fixed;
  z-index: 90;
  pointer-events: none;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  box-shadow: var(--shadow-md);
  padding: .5rem .7rem;
  font-size: .74rem;
  line-height: 1.55;
  color: var(--text);
  max-width: 15rem;
}
.sponsor-globe-tip[hidden] { display: none; }
.sponsor-globe-tip span { color: var(--muted); }

/* ---------- price and timeline ---------- */

.sponsor-price-card {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1.4rem 1.5rem;
}
.sponsor-flat-price { margin: 0; display: flex; align-items: baseline; gap: .7rem; flex-wrap: wrap; }
.sponsor-flat-price s { font-size: 1.15rem; color: var(--muted); }
.sponsor-flat-price strong { font-size: clamp(2rem, 5vw, 2.8rem); line-height: 1; letter-spacing: -.03em; color: var(--accent); }
.sponsor-flat-note { margin: .5rem 0 0; font-size: .84rem; color: var(--text-2); max-width: 52ch; line-height: 1.55; }

.sponsor-timeline { list-style: none; margin: 1.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .9rem; }
.sponsor-tl-step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: .2rem .7rem;
  padding-left: .1rem;
}
.sponsor-tl-dot {
  grid-row: span 2;
  align-self: start;
  margin-top: .38rem;
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--border-strong);
}
.sponsor-tl-when { font-size: .84rem; font-weight: 650; color: var(--text); }
.sponsor-tl-what { grid-column: 2; font-size: .82rem; color: var(--text-2); line-height: 1.5; max-width: 56ch; }
.sponsor-tl-step--live .sponsor-tl-dot { background: var(--ok); animation: sponsor-pulse 2s ease-in-out infinite; }
.sponsor-tl-step--live .sponsor-tl-when { color: var(--ok); }

/* ---------- who sponsors here ---------- */

.sponsor-who-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.sponsor-who-card {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1.1rem 1.2rem;
}
.sponsor-who-head { display: block; font-size: .9rem; font-weight: 650; margin-bottom: .4rem; }
.sponsor-who-card p { margin: 0; font-size: .82rem; color: var(--text-2); line-height: 1.55; }

/* ---------- slot board ---------- */

.sponsor-board { display: grid; grid-template-columns: 1fr; gap: 1.6rem; }
@media (min-width: 640px) {
  .sponsor-board { grid-template-columns: 1fr 1fr; }
}
.sponsor-slot-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 .6rem;
}
.sponsor-board-col {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  border-radius: var(--r-md);
  transition: box-shadow .2s;
}
/* Marks the column a visitor arrived from — via a "Your app here" click on
   the landing rail/tape or the dashboard tape — so the page they land on
   still points at roughly the right half, even though those surfaces show
   as many filler cards as their viewport fits and this page always shows
   exactly four sellable slots a side. Class name unchanged from when this
   applied to `.sponsor-slot-col`: `landing.js` and `sponsor-view.js` both
   still address it by that name. */
.sponsor-slot-col--highlight {
  box-shadow: 0 0 0 2px var(--accent);
  padding: .6rem;
  margin: -.6rem;
}

/* One card per slot. Deliberately not a `.rail-card`: a rail card is a
   compact advert, this is an inventory row, and it has to carry a slot id
   and a status chip the rail version has no room for. The colour tints
   *are* borrowed though — `sponsor-view.js` puts `.rail-card--<hue>` on
   these, so a sponsor's colour on the board is the same one source as their
   colour in the rail. */
.sponsor-slot {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    'tag  tag  chip'
    'icon name name'
    'icon note note';
  align-items: center;
  gap: .15rem .6rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: .7rem .8rem;
  text-decoration: none;
  color: inherit;
  text-align: left;
  transition: border-color .15s, transform .15s;
}
.sponsor-slot:hover { border-color: var(--accent); transform: translateY(-1px); }
.sponsor-slot-tag { grid-area: tag; font-size: .64rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.sponsor-slot-icon { grid-area: icon; width: 34px; height: 34px; border-radius: var(--r-sm); margin-top: .3rem; }
.sponsor-slot-name { grid-area: name; font-size: .9rem; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sponsor-slot-note { grid-area: note; font-size: .74rem; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sponsor-chip {
  grid-area: chip;
  justify-self: end;
  border-radius: var(--r-pill);
  padding: .12rem .5rem;
  font-size: .64rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.sponsor-chip--open { background: var(--ok-soft); color: var(--ok); }
.sponsor-chip--taken { background: var(--surface-3); color: var(--muted); }
.sponsor-chip--house { background: var(--accent-soft); color: var(--accent-strong); }

/* An open slot is a `<button>` (it scrolls to the form rather than
   navigating), so it needs the button reset the anchor version doesn't. */
.sponsor-slot--open {
  width: 100%;
  font: inherit;
  cursor: pointer;
  border-style: dashed;
  background: transparent;
}
.sponsor-slot--open:hover { background: var(--surface-2); }
.sponsor-slot--open .sponsor-slot-name { color: var(--text-2); }

/* ---------- how it works ---------- */

.sponsor-how {
  list-style: none;
  counter-reset: sponsor-step;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.sponsor-how li {
  counter-increment: sponsor-step;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1.1rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}
/* `decimal-leading-zero` — "01" reads as one of a fixed short sequence in a
   way "1" doesn't. */
.sponsor-how li::before {
  content: counter(sponsor-step, decimal-leading-zero);
  font-size: .8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  margin-bottom: .3rem;
}
.sponsor-how li strong { font-size: .92rem; }
.sponsor-how li span { font-size: .82rem; color: var(--text-2); line-height: 1.45; }

/* ---------- why it works ---------- */

/* One live number, called out on its own — reusing the accent/tabular-nums
   treatment `.sponsor-g-num`/`.sponsor-stat dt` already establish for a
   headline figure, rather than duplicating the hero band's stat tiles a
   second time. */
.sponsor-why-stat {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .5rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1rem 1.2rem;
  margin: 0 0 1.2rem;
}
.sponsor-why-stat strong {
  font-size: 1.6rem;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.sponsor-why-stat span { font-size: .85rem; color: var(--text-2); }

.sponsor-why-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.sponsor-why-item {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1.1rem 1.2rem;
}
.sponsor-why-head { display: block; font-size: .88rem; font-weight: 650; margin-bottom: .35rem; }
.sponsor-why-item p { margin: 0; font-size: .8rem; color: var(--text-2); line-height: 1.5; }

/* ---------- waitlist ---------- */

.sponsor-notify {
  max-width: 30rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  padding: 1.1rem 1.2rem;
}
.sponsor-notify form { display: flex; flex-wrap: wrap; gap: .5rem; }
.sponsor-notify input[type="email"] {
  flex: 1 1 14rem;
  min-width: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  padding: .55rem .7rem;
  font: inherit;
  font-size: .85rem;
}
.sponsor-notify .status { margin: .6rem 0 0; }

/* Clipped rather than `display: none`: a bot skips fields it can tell are
   hidden, and a honeypot only works on the ones it can't. */
.sponsor-hp {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- terms ---------- */

.sponsor-terms-prose { max-width: 64ch; }
.sponsor-terms-prose p { margin: 0 0 .9rem; font-size: .82rem; color: var(--text-2); line-height: 1.65; }
.sponsor-terms-prose code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .78rem; }

@media (prefers-reduced-motion: reduce) {
  .sponsor-g-fresh .sponsor-g-dot,
  .sponsor-tl-step--live .sponsor-tl-dot { animation: none; }
  .sponsor-slot { transition: none; }
  .sponsor-slot:hover { transform: none; }
}

/* ============================ hero ============================ */

.hero {
  text-align: center;
  max-width: 46rem;
  margin: 0 auto;
  padding: 3.5rem 1.5rem 3rem;
}
.hero-brand {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-weight: 700;
  font-size: .9rem;
  letter-spacing: -0.01em;
  color: var(--text-2);
  margin-bottom: 2rem;
}
.hero-brand:hover { text-decoration: none; color: var(--text); }
.hero-brand img { border-radius: 6px; display: block; }

.hero h1 {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: clamp(1.9rem, 5vw, 3.1rem);
  line-height: 1.12;
  font-weight: 650;
  letter-spacing: -0.035em;
  margin: 0 0 1.2rem;
  text-wrap: balance;
}
.hero-sub {
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-2);
  max-width: 40rem;
  margin: 0 auto 1.8rem;
}

.hero-search {
  display: flex;
  gap: .5rem;
  max-width: 30rem;
  margin: 0 auto 1.2rem;
}
.hero-search input {
  flex: 1;
  min-width: 0;
  font-size: .9rem;
  padding: .6rem .85rem;
  border-radius: var(--r-md);
}
.hero-nav {
  display: flex;
  gap: .6rem;
  justify-content: center;
  align-items: center;
  font-size: .85rem;
  color: var(--muted);
  flex-wrap: wrap;
}
.hero-nav a { color: var(--text-2); }
.hero-nav a:hover { color: var(--text); text-decoration: none; }
/* Sponsor page only — its nav sits right under the stat band rather than a
   search bar, and reads better with a little more air above it than the
   shared `.hero-nav` gives every hero by default. */
.sponsor-hero .hero-nav { margin-top: 1.5rem; }

/* Anchors can't inherit the app's <button> rules, so the shared look is
   restated rather than approximated. */
.landing-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: .88rem;
  font-weight: 650;
  padding: .6rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: linear-gradient(140deg, var(--accent), var(--accent-strong));
  color: var(--on-accent);
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
  cursor: pointer;
  /* Matches `button.primary` in styles.css: a gradient can't cross-fade into
     the flat hover colour below, so `background` is left out of the
     transition — easing that swap is what causes the flicker, dropping it
     makes the swap instant instead of attempted-and-broken. */
  transition: border-color .12s, transform .06s;
}
/* The hero search's CTA is a real `<button>` (needed for form submit), so
   it also matches styles.css's generic `button:hover:not(:disabled)` —
   which, thanks to the extra element selector, outranks a plain
   `.landing-cta:hover` on specificity and wins regardless of stylesheet
   order, repainting it grey instead of the accent colour below. Repeating
   the selector as `button.landing-cta:hover` matches that specificity so
   this rule's later position in the cascade decides it instead. */
.landing-cta:hover,
button.landing-cta:hover {
  text-decoration: none;
  background: var(--accent);
  border-color: transparent;
}
.landing-cta:active { transform: translateY(1px); }
.landing-cta.lg { font-size: .95rem; padding: .72rem 1.5rem; }

/* ============================ sections ============================ */

.landing {
  max-width: var(--landing-max);
  margin: 0 auto;
  padding: 0 1.5rem 3rem;
}
.row-section { margin-bottom: 3rem; scroll-margin-top: 1.5rem; }

.row-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .7rem;
  padding: 0 .15rem;
}
/* Small and muted on purpose — see the note at the top of this file. Two
   selectors deep so it beats the app's own `h2` without !important. */
.row-section h2 {
  font-size: .88rem;
  font-weight: 650;
  color: var(--text-2);
  margin: 0;
  display: flex;
  align-items: center;
  gap: .5rem;
}
.row-link { font-size: .8rem; font-weight: 550; color: var(--muted); white-space: nowrap; }
.row-link:hover { color: var(--text); text-decoration: none; }
.row-sub { font-size: .85rem; color: var(--muted); margin: 0 0 1rem; max-width: 62ch; line-height: 1.55; }
.row-note { font-size: .8rem; color: var(--muted); margin: .7rem .15rem 0; max-width: 62ch; }

/* Placeholder content is labelled where it is shown, not only in a comment
   in the source — see landing-demo.js. */
.demo-tag {
  font-size: .62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  padding: .12rem .4rem;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  /* --muted on --surface-3 measures ~4.2:1, just under WCAG AA's 4.5:1 for
     text this small — --text-2 is the next tier up and clears it (~6:1). */
  color: var(--text-2);
}

/* ============================ card rows ============================ */

.card-row {
  display: flex;
  gap: .65rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
  padding: .2rem .15rem 1rem;
  scrollbar-width: none;
}
.card-row::-webkit-scrollbar { display: none; }

.listing-tile {
  position: relative;
  flex: 0 0 auto;
  width: 226px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  padding: .8rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: inherit;
  overflow: hidden;
  transition: border-color .12s, transform .1s, box-shadow .12s;
}
.listing-tile:hover {
  text-decoration: none;
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.tile-flag {
  position: absolute;
  top: 0; right: 0;
  z-index: 2;
  font-size: .58rem;
  font-weight: 800;
  letter-spacing: .05em;
  padding: .18rem .45rem;
  border-bottom-left-radius: var(--r-md);
}
.tile-flag.testing { background: var(--warn-soft); color: var(--warn); }
.tile-flag.launched { background: var(--ok-soft); color: var(--ok); }

/* ---------------------------------------------------------------------
   Contribution boost tiers

   Bronze through Légendaire, purely from the listing owner's lifetime
   High Fives received — `ownerBoostTier` in
   community/src/routes/listings.js, reading `owner_contribution_count`.
   Automatic and permanent (it only ever goes up, and can't be bought),
   unlike `.featured` below (an active, time-boxed spend). Deliberately
   built as escalating versions of `.featured`'s own look — a soft
   background tint, a tinted border, no box-shadow ring (a heavy outline
   read as a badge glued on top of the card, not a property of the card
   itself) — rather than a separate visual language: Silver (boost-2)
   is the exact accent-tint formula `.featured` uses, so a card that's
   merely Silver and one that's actively `.featured` are indistinguishable
   at that tier, and every tier above it just turns the same two dials
   (tint strength, hue) further. Every tier also gets the shine sweep,
   not just Légendaire — see `.listing-tile.boost-1::after` below.

   The two stack (a card can be both boosted and `.featured`), and where
   they'd conflict on background/border, `.featured`'s rules win by
   appearing later in this file — the thing you just paid for reads as
   the stronger signal than a passive badge. Légendaire (boost-7) is the
   one exception: declared after `.featured` further down, so the rarest
   tier this app can hand out for real, sustained contribution outranks
   a placement anyone can pay for. Same ramp as the dashboard's
   `.boost-1..7` in styles.css, so the two surfaces read as one system
   (minus the shine, which is landing-only motion).
   --------------------------------------------------------------------- */
/* Bronze mixes warn with --bad (red) before diluting into --border — an
   actual mahogany/acajou brown, not just a duller gold. Plain warn only
   ever ranges from pale to bright yellow-gold, never red-brown enough to
   read as a different metal from Gold; warn+bad gives it a hue of its
   own. */
.listing-tile.boost-1 {
  background: color-mix(in srgb, color-mix(in srgb, var(--warn) 60%, var(--bad) 40%) 10%, var(--surface));
  border-color: color-mix(in srgb, color-mix(in srgb, var(--warn) 60%, var(--bad) 40%) 55%, var(--border));
}
/* Silver — the exact `.featured` accent formula further down, on purpose. */
.listing-tile.boost-2 {
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
}
/* Gold — deliberately modest (a milder warm tint than Silver's accent
   strength, even), so it stays clearly a step below Légendaire's fully
   gold card rather than nearly matching it. */
.listing-tile.boost-3 {
  background: color-mix(in srgb, var(--warn) 8%, var(--surface));
  border-color: color-mix(in srgb, var(--warn) 45%, var(--border));
}
/* Platine — blue, same hex as the sponsor rail's `blue` swatch
   (RAIL_COLORS in landing.js), reused rather than inventing a new hue. */
.listing-tile.boost-4 {
  background: color-mix(in srgb, #2f5fa8 15%, var(--surface));
  border-color: color-mix(in srgb, #2f5fa8 45%, var(--border));
}
/* Diamant — violet, rail's `violet` swatch. */
.listing-tile.boost-5 {
  background: color-mix(in srgb, #6d3aa8 17%, var(--surface));
  border-color: color-mix(in srgb, #6d3aa8 50%, var(--border));
}
/* Rubis — red, rail's `red` swatch. Strongest tint before Légendaire. */
.listing-tile.boost-6 {
  background: color-mix(in srgb, #a8342f 19%, var(--surface));
  border-color: color-mix(in srgb, #a8342f 55%, var(--border));
}

/* One shine sweep, shared by every non-Légendaire boost tier (1-6) — see
   the comment above for why they all get it, not just the top tier.
   Declared before `.featured::after` so a card that's both boosted 1-6
   and `.featured` shows the paid placement's own (slightly softer) sweep,
   matching the precedence documented above. */
.listing-tile.boost-1::after, .listing-tile.boost-2::after, .listing-tile.boost-3::after,
.listing-tile.boost-4::after, .listing-tile.boost-5::after, .listing-tile.boost-6::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(
    120deg,
    transparent 0%, transparent 30%,
    rgb(255 255 255 / 45%) 44%,
    rgb(255 255 255 / 60%) 50%,
    rgb(255 255 255 / 45%) 56%,
    transparent 70%, transparent 100%
  ) 0 0 / 200% 100% no-repeat;
  animation: tile-streak 5s ease-in-out infinite;
}
@media (prefers-color-scheme: dark) {
  .listing-tile.boost-1::after, .listing-tile.boost-2::after, .listing-tile.boost-3::after,
  .listing-tile.boost-4::after, .listing-tile.boost-5::after, .listing-tile.boost-6::after {
    background-image: linear-gradient(
      120deg,
      transparent 0%, transparent 30%,
      rgb(255 255 255 / 12%) 44%,
      rgb(255 255 255 / 20%) 50%,
      rgb(255 255 255 / 12%) 56%,
      transparent 70%, transparent 100%
    );
  }
}
@media (prefers-reduced-motion: reduce) {
  .listing-tile.boost-1::after, .listing-tile.boost-2::after, .listing-tile.boost-3::after,
  .listing-tile.boost-4::after, .listing-tile.boost-5::after, .listing-tile.boost-6::after {
    animation: none; opacity: 0;
  }
}

/* ---------------------------------------------------------------------
   Featured listings

   What a developer gets for spending tokens: a tinted card and a highlight
   that crosses it now and then. `color-mix` against the surface keeps the
   tint readable in both themes instead of hardcoding one dark wash.

   The sweep pauses at each end rather than looping continuously — a
   constant animation in a grid of cards reads as a loading state, while an
   occasional glint reads as emphasis. That's what the long holds in the
   keyframes below are for.
   --------------------------------------------------------------------- */
.listing-tile.featured {
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
}
.listing-tile.featured.warm {
  background: color-mix(in srgb, var(--warn) 10%, var(--surface));
  border-color: color-mix(in srgb, var(--warn) 26%, var(--border));
}

.listing-tile.featured::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(
    120deg,
    transparent 0%, transparent 30%,
    rgb(255 255 255 / 38%) 44%,
    rgb(255 255 255 / 52%) 50%,
    rgb(255 255 255 / 38%) 56%,
    transparent 70%, transparent 100%
  ) 0 0 / 200% 100% no-repeat;
  animation: tile-streak 5s ease-in-out infinite;
}

@media (prefers-color-scheme: dark) {
  .listing-tile.featured::after {
    background-image: linear-gradient(
      120deg,
      transparent 0%, transparent 30%,
      rgb(255 255 255 / 9%) 44%,
      rgb(255 255 255 / 16%) 50%,
      rgb(255 255 255 / 9%) 56%,
      transparent 70%, transparent 100%
    );
  }
}

@keyframes tile-streak {
  0%, 18%   { background-position: 200% 0; }
  50%, 53%  { background-position: -100% 0; }
  85%, 100% { background-position: 200% 0; }
}

/* A sweep that repeats forever is exactly the kind of motion that makes a
   page unusable for some readers, and it carries no information. */
@media (prefers-reduced-motion: reduce) {
  .listing-tile.featured::after { animation: none; opacity: 0; }
}

/* Légendaire (boost-7) — the top of the contribution ladder, a fully gold
   card. Declared after `.featured` on purpose (see the comment above the
   boost tiers) so a listing that is both boosted to Légendaire and
   `.featured` reads as gold, not the paler `.featured` accent tint —
   contribution this sustained outranks a placement anyone can buy. Gets
   its own shine sweep rather than depending on `.featured` being present,
   reusing the same `tile-streak` keyframes. */
.listing-tile.boost-7 {
  border-color: var(--warn);
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--warn) 32%, var(--surface)),
    color-mix(in srgb, var(--warn) 16%, var(--surface))
  );
}
.listing-tile.boost-7::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(
    120deg,
    transparent 0%, transparent 30%,
    rgb(255 255 255 / 55%) 44%,
    rgb(255 255 255 / 72%) 50%,
    rgb(255 255 255 / 55%) 56%,
    transparent 70%, transparent 100%
  ) 0 0 / 200% 100% no-repeat;
  animation: tile-streak 5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .listing-tile.boost-7::after { animation: none; opacity: 0; }
}

.tile-head { display: flex; align-items: center; gap: .6rem; margin: .7rem 0 .7rem; }
/* `display:block` is load-bearing, not tidiness: `overflow`/`text-overflow`
   are ignored on a non-replaced inline element, so without it these two
   only stack (and only truncate) when the parent happens to be narrow
   enough to force a wrap. */
.tile-name { display: block; font-weight: 700; font-size: .87rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tile-genre { display: block; font-size: .68rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A generated letter tile rather than a logo: these listings are invented,
   and borrowing a real app's mark would imply a customer who isn't one. */
.tile-icon {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  border-radius: 9px;
  display: grid; place-items: center;
  font-weight: 700; font-size: .95rem;
  color: #fff;
  border: 1px solid rgb(255 255 255 / 14%);
  object-fit: cover;
}

.tile-note {
  font-size: .74rem;
  color: var(--muted);
  line-height: 1.4;
  margin-bottom: .7rem;
  min-height: 2.05em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tile-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; margin-top: auto; }
.tile-metrics > div { text-align: center; }
.tile-metrics .k {
  display: block;
  font-size: .56rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  margin-bottom: .1rem;
}
.tile-metrics .v {
  display: block;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: .78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.tile-metrics .v .metric-icon { vertical-align: -4px; margin-left: 1px; }
.tile-metrics .v.ok { color: var(--ok); }
.tile-metrics .v.warn { color: var(--warn); }
.tile-metrics .v.bad { color: var(--bad); }

.row-more {
  flex: 0 0 auto;
  width: 226px;
  scroll-snap-align: start;
  display: grid;
  place-items: center;
  padding: 1rem;
  text-align: center;
  font-size: .84rem;
  font-weight: 550;
  color: var(--muted);
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--r-md);
  transition: border-color .12s, color .12s, background .12s;
}
.row-more:hover { text-decoration: none; border-color: var(--accent); color: var(--text); background: var(--surface-2); }

/* ============================ tools ============================ */

/* A fixed 2-column-by-default, 4-across-from-tablet-up grid rather than
   auto-fill: with exactly 8 cards, a width-driven column count would give
   a clean 2×4 at some viewports and an uneven 3+3+2 at others (the sponsor
   rails alone shrink this section's own width by ~300px starting at
   1180px) — fixed columns keep it 2 full rows everywhere. */
.tools-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: .55rem;
}
@media (min-width: 640px) {
  .tools-grid { grid-template-columns: repeat(4, 1fr); }
}
.tool-card {
  display: flex;
  flex-direction: column;
  gap: .22rem;
  padding: .85rem .95rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: inherit;
  transition: border-color .12s, transform .1s, box-shadow .12s;
}
.tool-card:hover { text-decoration: none; border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tool-icon { display: inline-flex; color: var(--accent); margin-bottom: .15rem; }
.tool-icon svg { width: 19px; height: 19px; }
.tool-card strong { font-size: .87rem; }
.tool-card span:last-child { font-size: .78rem; color: var(--text-2); line-height: 1.45; }

/* ============================ leaderboard ============================ */

/* Segmented toggles: which number ranks the board, and over what window. */
.lb-controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
.seg {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.seg-btn {
  font: inherit;
  font-size: .78rem;
  font-weight: 600;
  padding: .3rem .7rem;
  border: none;
  border-radius: calc(var(--r-md) - 3px);
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: background .12s, color .12s;
}
.seg-btn:hover:not(.active) { color: var(--text); background: var(--surface-3); }
.seg-btn.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }

/* `overflow-x: auto`, not `hidden`: the table is wider than the panel on a
   phone, and hiding the overflow would clip the Tokens column off the right
   edge with no way to reach it. Scrolling keeps every column readable, and
   the rounded corners still clip because a non-visible overflow on one axis
   forces the other to clip too. */
.lb-wrap { padding: 0; overflow-x: auto; }
table.lb { width: 100%; border-collapse: collapse; font-size: .86rem; }
table.lb th {
  text-align: left;
  font-size: .66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  padding: .6rem .9rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
table.lb td { padding: .55rem .9rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.lb tbody tr:last-child td { border-bottom: none; }
table.lb tbody tr { transition: background .12s; }
table.lb tbody tr:hover { background: var(--surface-2); }
table.lb .c { text-align: center; width: 3.2rem; }
table.lb .n { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.lb .app-col { width: 15rem; }
table.lb td.app-col { font-size: .82rem; }
.lb-app-cell { display: flex; align-items: center; gap: .55rem; min-width: 0; }
/* Sized to match `.lb-who`'s tester avatar so the two entity columns sit on
   one another's baseline instead of stepping. */
.lb-app-icon {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  border-radius: 8px;
  display: grid; place-items: center;
  object-fit: cover;
  font-weight: 700; font-size: .8rem;
  color: #fff;
  border: 1px solid rgb(255 255 255 / 14%);
}
.lb-app {
  display: block;
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lb-app-desc {
  display: block;
  font-size: .72rem;
  color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The dash is an answer, not missing data, so it reads as deliberate rather
   than as a cell that failed to load. */
.lb-none { color: var(--muted); }

.lb-medal { font-size: 1.05rem; line-height: 1; }
.lb-rank { font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.lb-who { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.lb-who .tile-icon { width: 28px; height: 28px; border-radius: 8px; font-size: .8rem; }
.lb-name { display: block; font-weight: 600; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The sub-line under a name, the way a marketplace board carries a one-line
   description under each entry — `display:block` for the same reason as
   `.tile-name` above. */
.lb-sub { display: block; font-size: .72rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-ratings { font-weight: 700; font-variant-numeric: tabular-nums; }

.lb-more {
  width: 100%;
  font: inherit;
  font-size: .82rem;
  font-weight: 600;
  padding: .65rem;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
  transition: background .12s, color .12s;
}
.lb-more:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }
.lb-more:disabled { opacity: .55; cursor: default; }

/* A row's app icon gets a small snap of life on hover — the same treatment
   the browse table's rows use below, so a tester's linked app and a plain
   listing read as the same kind of thing on hover. */
.lb-app-icon, .lb-who .tile-icon { transition: transform .15s cubic-bezier(.2,.8,.3,1.4); }
table.lb tbody tr:hover .lb-app-icon,
table.lb tbody tr:hover .lb-who .tile-icon { transform: scale(1.12) rotate(-3deg); }

/* The little "which store" corner badge, marketplace-card style (`.store-
   badge`, styles.css — position/size/border already defined there). Tucked
   away until the row is hovered, same as the icon snap above, rather than
   sitting on the board permanently and competing with the rank/name for
   attention on every row at once. */
.lb-app-icon-wrap { position: relative; flex: 0 0 auto; line-height: 0; }
.lb-app-icon-wrap .store-badge {
  opacity: 0;
  transform: scale(.7);
  transition: opacity .15s ease, transform .15s cubic-bezier(.2,.8,.3,1.4);
}
table.lb tbody tr:hover .lb-app-icon-wrap .store-badge { opacity: 1; transform: scale(1); }

/* "Both" doesn't try to cram the App Store and Play marks into one 16px
   badge — illegible at that size either way — it fans two mini badges out
   diagonally instead. The outer `.store-badge` box becomes just a
   positioning anchor (no fill/border of its own); each mark gets its own
   small white tile, same look as the single-store badge, just two of them,
   sized and overlapped so the *pair* fills the same ~16px footprint the
   single-store badge does, rather than spilling past it. */
.store-badge.both { background: none; border: none; overflow: visible; }
/* Same shape `.store-badge` itself is (styles.css): a flex-centered,
   `overflow: hidden` tile with the mark filling it via `object-fit: cover`
   — not the mark styled directly, which is how the Apple mark previously
   ended up floating in whitespace instead of filling its tile. */
.store-badge.both .store-badge-a,
.store-badge.both .store-badge-b {
  position: absolute;
  width: 13px;
  height: 13px;
  border-radius: 3.5px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  border: 1.5px solid var(--surface);
  overflow: hidden;
}
.store-badge.both .store-badge-a { top: -.5px; left: -.5px; z-index: 2; }
.store-badge.both .store-badge-b { bottom: -.5px; right: -.5px; z-index: 1; }
.store-badge.both img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Play's own mark, at full tile size, reads as more zoomed-in/cropped than
   Apple's — the single-store badge already corrects for this
   (`.store-badge.google img` in styles.css); same 70% shrink here. */
.store-badge.both .store-badge-b img { width: 70%; height: 70%; }

/* ============================ category dropdown ============================
   Shared by the leaderboard-style browse table below — one distinct app
   category (an App Store/Play genre, not a listing field) per option, built
   at runtime from whatever has resolved so far rather than a fixed list. */
.cat-dd { display: inline-flex; position: relative; }
.cat-dd-btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font: inherit;
  font-size: .78rem;
  font-weight: 600;
  padding: .4rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: border-color .12s, color .12s;
}
.cat-dd-btn:hover, .cat-dd-btn[aria-expanded="true"] { border-color: var(--accent); }
.dd-caret { font-size: .65rem; color: var(--muted); }
.cat-dd-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  min-width: 15rem;
  max-height: 20rem;
  overflow-y: auto;
  padding: .3rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
}
.cat-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8rem;
  width: 100%;
  font: inherit;
  font-size: .8rem;
  text-align: left;
  padding: .45rem .6rem;
  border: none;
  border-radius: calc(var(--r-md) - 3px);
  background: none;
  color: var(--text);
  cursor: pointer;
}
.cat-opt:hover { background: var(--surface-2); }
.cat-opt.active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }
.co-count { color: var(--muted); font-variant-numeric: tabular-nums; font-size: .74rem; }

/* `.vchip`/`.vchip-group` (platform filter chips) moved to styles.css —
   the dashboard (app.html, be-tester.js's Marketplace tab) reuses them for
   its own platform filter but only loads styles.css, not this file. */

/* Solid fills, not soft tints — a platform badge is the one fact in this
   row a visitor filters and scans by, so it reads at a glance the way
   Death List's own verdict badge does, not as a quiet footnote. iOS gets
   Apple's own blue rather than the site's indigo `--accent`, so it reads
   as "which store", not "AppMates picked this app". */
.pf-badge {
  display: inline-block;
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .03em;
  text-transform: uppercase;
  padding: .28rem .6rem;
  border-radius: var(--r-sm);
  transition: transform .12s cubic-bezier(.2,.8,.3,1.4);
}
.pf-badge.ios { background: var(--ios); color: #fff; }
.pf-badge.android { background: var(--ok); color: #062b16; }
.pf-badge.both { background: var(--accent); color: var(--on-accent); }

/* ============================ browse view ============================ */

.browse-hero { padding-bottom: 1.5rem; }

.browse-toolbar {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  margin-bottom: .7rem;
}
.browse-count { margin-left: auto; font-size: .78rem; color: var(--muted); }

.browse-table-wrap { padding: 0; overflow-x: auto; }
.browse-thead, .browse-row {
  display: grid;
  grid-template-columns: 1fr 10rem 7rem 8rem;
  align-items: center;
  gap: .8rem;
  padding: .7rem .9rem;
}
.browse-thead {
  font-size: .66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.browse-row {
  position: relative;
  color: inherit;
  border-bottom: 1px solid var(--border);
  transition: background-color .15s;
}
.browse-row:last-child { border-bottom: none; }
.browse-row:hover { background: var(--surface-2); text-decoration: none; }
/* Anchored inside the row's own padding box (not past its edge, which the
   table wrap's `overflow-x: auto` was clipping) — always fully on-screen,
   just slides in those last few px and fades on hover. */
.browse-row::after {
  content: "→";
  position: absolute;
  right: .8rem;
  top: 50%;
  transform: translate(6px, -50%);
  color: var(--accent);
  opacity: 0;
  transition: opacity .15s ease, transform .2s ease;
}
.browse-row:hover::after { opacity: 1; transform: translate(0, -50%); }

.browse-c-app { display: flex; align-items: center; gap: .7rem; min-width: 0; }
.browse-rank { flex: 0 0 auto; width: 1.4rem; font-size: .74rem; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.browse-c-app img, .browse-icon-fallback {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  transition: transform .15s cubic-bezier(.2,.8,.3,1.4);
}
.browse-icon-fallback {
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 700;
  font-size: .8rem;
}
.browse-row:hover .browse-c-app img,
.browse-row:hover .browse-icon-fallback { transform: scale(1.15) rotate(-3deg); }
.browse-row:hover .pf-badge { transform: scale(1.06); }
.browse-name { font-weight: 600; font-size: .87rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.browse-c-cat {
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-right: .6rem;
}
.browse-c-platform { display: flex; justify-content: center; }
/* Right-aligned but pulled well clear of the row's own edge, so the "→"
   that fades in on hover (`.browse-row::after`, anchored to that same
   edge) always has clean space to appear in, never under the last
   character of "Testers"/"Status". */
.browse-c-metric {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding-right: 1.9rem;
}

.browse-empty { padding: 2.5rem 1rem; text-align: center; color: var(--muted); font-size: .86rem; }

@media (max-width: 720px) {
  .browse-thead { display: none; }
  .browse-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "app app platform" "cat cat metric";
    row-gap: .3rem;
  }
  .browse-c-app { grid-area: app; }
  .browse-c-cat { grid-area: cat; padding-left: 2.1rem; }
  .browse-c-platform { grid-area: platform; justify-self: end; }
  .browse-c-metric { grid-area: metric; justify-self: end; }
  .browse-row::after { display: none; }
}

/* ============================ steps ============================ */

.steps {
  list-style: none;
  margin: 0 0 1.2rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: .6rem;
}
.steps li {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.step-n {
  display: grid; place-items: center;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700; font-size: .78rem;
  margin-bottom: .15rem;
}
.steps strong { font-size: .88rem; }
.steps li > span:last-child { font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.callout.guardrail { max-width: none; font-size: .84rem; line-height: 1.6; }

/* ============================ faq ============================ */

/* `<details>/<summary>` rather than a JS accordion: the questions people
   actually search for should work even if the script never loads. */
/* Centred as a standalone block — unlike the row-based sections above it,
   the FAQ isn't paired with a "View all" link that needs the heading kept
   left, so `#faq .row-head` centres `<h2>Questions</h2>` too. */
#faq .row-head { justify-content: center; }
.faq-list { max-width: 42rem; margin: 0 auto; display: flex; flex-direction: column; gap: .6rem; }
.faq-item {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 0 1rem;
}
.faq-item summary {
  cursor: pointer;
  padding: .9rem 0;
  font-weight: 650;
  font-size: .92rem;
  color: var(--text);
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: '+';
  font-size: 1.2rem;
  color: var(--muted);
  flex-shrink: 0;
  transition: transform .15s;
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item p { margin: 0 0 1rem; font-size: .86rem; color: var(--text-2); line-height: 1.6; }

/* ============================ closing + footer ============================ */

.closing {
  text-align: center;
  padding: 3.2rem 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.closing h2 {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: 1.3rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  margin: 0;
}
.closing p { color: var(--text-2); max-width: 44ch; margin: .7rem auto 1.5rem; font-size: .9rem; line-height: 1.55; }

.landing-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  max-width: var(--landing-max);
  margin: 0 auto;
  padding: 2rem 1.5rem 3rem;
  border-top: 1px solid var(--border);
  font-size: .84rem;
}
.landing-footer > div:first-child { display: flex; flex-direction: column; gap: .15rem; }
.footer-brand {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-weight: 700;
  color: var(--text);
}
.footer-brand:hover { text-decoration: none; color: var(--text); }
.footer-brand img { border-radius: 5px; display: block; }
.landing-footer-links { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.footer-links-col { display: flex; flex-direction: column; gap: .5rem; }

/* Sized to the footer's own font-size rather than the badge's intrinsic
   250x54 — consistent with the sidebar's copy of this badge, and it keeps
   the footer's height from being dictated by an embed neither this file
   nor the badge's own markup controls. */
.footer-badge { display: block; flex-shrink: 0; width: 130px; }
.footer-badge img { display: block; width: 100%; height: auto; }

@media (max-width: 640px) {
  .hero { padding: 2.2rem 1.1rem 2rem; }
  .hero-search { flex-direction: column; }
  .landing { padding: 0 1.1rem 2.5rem; }
  .row-section { margin-bottom: 2.4rem; }
  /* Keeps rank, who, and the number they are actually ranked by. The app
     and its store ratings are context, and context is what a phone drops
     first — hiding "Tests" instead would leave a board with no visible
     basis for its own order. */
  table.lb .app-col,
  table.lb th:last-child,
  table.lb td:last-child { display: none; }
  .closing { padding: 2.4rem 1.2rem; }
}

