/* ============================================================================
   SecondWave — Component library
   ----------------------------------------------------------------------------
   Every reusable piece of UI in the product. Build screens by composing these.
   If a screen needs something that isn't here, add it HERE (with a comment
   explaining when to use it) rather than writing one-off CSS in a page file.

   Naming: .block, .block__element, .block--modifier.
   No raw values. No !important. No inline styles in the HTML.
   ========================================================================== */

/* ============================================================================
   HERO
   Full-width photographic hero with text overlaid on a dark gradient scrim.
   Use once per page, at the top, when there's a real photo to show — never
   with a stock/generic image. Square corners (it's content, not a pill).
   ========================================================================== */

.hero {
  position: relative;
  min-height: 56vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: var(--bg-recessed);
  /* Bleeds edge-to-edge past .page's own padding. Matches .page's gutter
     breakpoint exactly (see base.css) so the two never drift apart. */
  margin-inline: calc(-1 * var(--gutter));
}

@media (min-width: 768px) {
  .hero {
    margin-inline: calc(-1 * var(--gutter-lg));
  }
}

.hero__media {
  position: absolute;
  inset: 0;
}

.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Framing helpers for photography whose subject sits away from the centre of
   the frame. These shots are taken from the beach at distance, so a centred
   cover crop in a wide box lands on empty sea or sky and cuts the rider out.
   Add alongside .hero__media or .listing-card__media. */
.frame-high img {
  object-position: center 12%;
}

.frame-low img {
  object-position: center 78%;
}

/* Strong at the bottom for text legibility, fading out toward the top so the
   photo still reads as a photo, not just a dark rectangle. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.88) 0%,
    rgba(0, 0, 0, 0.5) 45%,
    rgba(0, 0, 0, 0.1) 100%
  );
}

.hero__content {
  position: relative;
  width: 100%;
  padding: var(--space-7) var(--gutter) var(--space-8);
}

@media (min-width: 768px) {
  .hero__content {
    padding-inline: var(--gutter-lg);
  }
}

/* ============================================================================
   BUTTONS
   The white pill is the brand's primary action and should appear roughly once
   per screen — it means "the main thing to do here". Everything else is a
   secondary (outlined) or ghost button.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-pill);
  font-size: var(--text-body);
  font-weight: var(--weight-medium);
  letter-spacing: var(--track-body);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

/* Primary — white pill, black text. The signature button. */
.btn--primary {
  background: var(--ink-100);
  color: var(--ground-100);
  border: var(--hairline) solid var(--ink-100);
}

.btn--primary:hover:not(:disabled) {
  background: var(--ink-80);
  border-color: var(--ink-80);
}

.btn--primary:active:not(:disabled) {
  background: var(--ink-60);
  border-color: var(--ink-60);
}

/* Secondary — outlined. For alternative actions sitting beside a primary. */
.btn--secondary {
  background: transparent;
  color: var(--ink-100);
  border: var(--hairline) solid var(--border-strong);
}

.btn--secondary:hover:not(:disabled) {
  background: var(--bg-hover);
  border-color: var(--ink-100);
}

/* Ghost — no chrome until hovered. For tertiary actions and toolbars. */
.btn--ghost {
  background: transparent;
  color: var(--text-secondary);
  border: var(--hairline) solid transparent;
  padding-inline: var(--space-4);
}

.btn--ghost:hover:not(:disabled) {
  background: var(--bg-hover);
  color: var(--ink-100);
}

/* Destructive actions are outlined, never filled — the same treatment now
   used for every non-primary button. The word ("Remove", "Delete listing")
   carries the meaning; these are low-frequency admin actions, always paired
   with a confirmation step, not a flow a buyer or seller hits often. */
.btn--danger {
  background: transparent;
  color: var(--ink-100);
  border: var(--hairline) solid var(--ink-100);
}

.btn--danger:hover:not(:disabled) {
  background: var(--bg-recessed);
}

.btn--sm {
  min-height: 36px;
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
}

.btn--lg {
  min-height: 56px;
  padding: var(--space-4) var(--space-7);
  font-size: var(--text-body-lg);
}

/* Full width — the default for primary actions on mobile. */
.btn--block {
  display: flex;
  width: 100%;
}

.btn:disabled,
.btn[aria-disabled='true'] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Icon-only button. MUST carry an aria-label or a .visually-hidden span —
   there is no visible text to fall back on. */
.btn--icon {
  width: var(--touch-min);
  height: var(--touch-min);
  min-height: var(--touch-min);
  padding: 0;
  border-radius: var(--radius-round);
}

.btn__icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ============================================================================
   CHIPS — filters and quick toggles
   Used along the top of Explore. Horizontally scrollable on mobile.
   ========================================================================== */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  border: var(--hairline) solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  white-space: nowrap;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}

.chip:hover {
  border-color: var(--border-strong);
  color: var(--ink-100);
}

/* Selected state carries BOTH a fill change and a tick icon, so it never
   depends on colour alone. */
.chip[aria-pressed='true'],
.chip--active {
  background: var(--ink-100);
  border-color: var(--ink-100);
  color: var(--ground-100);
  font-weight: var(--weight-medium);
}

.chip__count {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.chip[aria-pressed='true'] .chip__count {
  color: var(--ground-100);
  opacity: 0.7;
}

/* Horizontal scroller for a chip row. Hides the scrollbar but stays swipeable
   and keyboard-reachable. */
.chip-row {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-block: var(--space-2);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.chip-row::-webkit-scrollbar {
  display: none;
}

/* ============================================================================
   BADGES
   Small, non-interactive status markers.
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Verified seller. The tick is part of the badge, not decoration — it is the
   thing users scan for. */
.badge--verified {
  background: var(--ink-100);
  color: var(--ground-100);
}

.badge--outline {
  border: var(--hairline) solid var(--border);
  color: var(--text-secondary);
}

/* Condition grades. Six fixed values, defined in PRODUCT.md and not to be
   extended: New, Like New, Excellent, Good, Fair, For Repair.
   They form a visual ramp from solid to outlined so the grade is legible
   without reading — but the word is always present too. */
.badge--condition-new       { background: var(--ink-100); color: var(--ground-100); }
.badge--condition-like-new  { background: var(--ink-80);  color: var(--ground-100); }
.badge--condition-excellent { background: var(--ink-60);  color: var(--ground-100); }
.badge--condition-good      { border: var(--hairline) solid var(--ink-60);  color: var(--ink-80); }
.badge--condition-fair      { border: var(--hairline) solid var(--ground-60); color: var(--text-muted); }
.badge--condition-repair    { border: var(--hairline) solid var(--ink-100);  color: var(--ink-100); }

.badge__icon {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* ============================================================================
   FIT INDICATOR — the product's signature component
   ----------------------------------------------------------------------------
   Shows whether an item suits THIS user, based on their profile rider stats.
   Three states, each with its own icon AND its own wording. Never render this
   as colour alone, and never render it at all when the user has no rider stats
   on file — show the .fit--unknown prompt instead, which is an invitation to
   complete their profile.
   ========================================================================== */

.fit {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1.2;
}

.fit__icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* "Good fit for you" — tick icon */
.fit--good {
  background: var(--ink-100);
  color: var(--ground-100);
}

/* "A step up" / "Outside your usual range" — chevron icon */
.fit--stretch {
  background: var(--bg-recessed);
  color: var(--ink-100);
  border: var(--hairline) solid var(--ink-100);
}

/* "Not suited to your stats" — slash icon. Deliberately quiet: we inform,
   we don't scold, and we never hide the listing. */
.fit--poor {
  background: transparent;
  color: var(--text-muted);
  border: var(--hairline) solid var(--border);
}

/* No rider stats yet — a prompt, not a verdict. */
.fit--unknown {
  background: transparent;
  color: var(--text-secondary);
  border: var(--hairline) dashed var(--border-strong);
  cursor: pointer;
}

/* ============================================================================
   LISTING CARD
   ----------------------------------------------------------------------------
   The most-repeated component in the product. Priorities, in order:
     1. the photo    2. the price    3. how far away it is    4. fit
   Title comes after those because buyers scan images and numbers first.
   Square corners, no shadow — depth comes from the raised ground only.
   ========================================================================== */

.listing-card {
  /* M23.7: this is an <a> now, not an <article role="button"> — so it needs
     the anchor defaults cleared. `display: flex` was already here and is
     what keeps an inline element laying out as the card it looks like. */
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  background: var(--bg-raised);
  border: var(--hairline) solid transparent;
  border-radius: var(--radius-none);
  overflow: hidden;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.listing-card:hover {
  background: var(--bg-hover);
  border-color: var(--border);
}

.listing-card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--bg-recessed);
}

.listing-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}

.listing-card:hover .listing-card__media img {
  transform: scale(1.03);
}

/* Overlaid on the photo, top-left. Keep to ONE badge here; more than one and
   the photo stops doing its job. */
.listing-card__flag {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
}

/* Save/favourite, top-right. Icon button with an accessible label. */
.listing-card__save {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: 40px;
  height: 40px;
  min-height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-round);
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(8px);
  color: var(--ink-100);
}

.listing-card__save[aria-pressed='true'] {
  background: var(--ink-100);
  color: var(--ground-100);
}

.listing-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
}

.listing-card__price {
  font-size: var(--text-body-lg);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-heading);
}

.listing-card__title {
  font-size: var(--text-body);
  font-weight: var(--weight-regular);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
  /* Two lines maximum, then ellipsis — keeps the grid even. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Specs line: the structured attributes that make this a specialist product.
   e.g. "7'6 · 52L · Thruster". Always from typed fields, never parsed text. */
.listing-card__specs {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.listing-card__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin-top: auto;
  padding-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Inside a card the fit pill is compact: cards get narrow on a phone (two
   columns at 375px) and a full-size pill wraps to three lines. Keep the label
   itself short here — "Good fit" not "Good fit for you". */
.listing-card .fit {
  align-self: flex-start;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  line-height: 1.3;
}

.listing-card .fit__icon {
  width: 13px;
  height: 13px;
}

/* Distance is a first-class citizen of this product — see PRODUCT.md
   principle 3. It always renders with its pin icon. */
.distance {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.distance__icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
}

/* Collection-only items say so explicitly. This is not a warning — for a
   kayak it is simply the truth, and hiding it wastes the buyer's time. */
.listing-card__collect {
  font-size: var(--text-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* --- Listing grid --------------------------------------------------------- */

.listing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-3);
}

@media (min-width: 640px) {
  .listing-grid {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-4);
  }
}

@media (min-width: 1024px) {
  .listing-grid {
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  }
}

/* ============================================================================
   CATEGORY TILE
   ----------------------------------------------------------------------------
   The "Shop by sport" grid on the home page. Shares the listing card's visual
   language — raised ground, square corners, hairline border on hover — but is
   its own component: a category is a route into the catalogue, not an item in
   it, and it carries an icon where a card carries a photo.

   Tiles are narrower than listing cards (140px vs 160px) because a category
   name and a count need far less room than a photo, a price and a fit line.
   ========================================================================== */

.category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-3);
}

.category-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-5);
  background: var(--bg-raised);
  border: var(--hairline) solid transparent;
  border-radius: var(--radius-none);
  text-decoration: none;
  color: inherit;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.category-tile:hover {
  background: var(--bg-hover);
  border-color: var(--border);
}

.category-tile__icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  color: var(--ink-80);
  transition: color var(--duration-fast) var(--ease-out);
}

/* Monochrome equivalent of the old orange-on-hover treatment: the icon rests
   dimmer than body text and brightens to full white on hover or focus, so
   interactivity is still marked by a colour *change*, just without an
   accent hue — there is no accent hue left on this screen. */
.category-tile:hover .category-tile__icon,
.category-tile:focus-visible .category-tile__icon {
  color: var(--ink-100);
}

.category-tile__name {
  font-weight: var(--weight-medium);
}

.category-tile__count {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* M16.2: the sell flow's "what are you selling?" step reuses .category-tile
   but as a picker, not a browse entry point — no count to show, and it needs
   to fit all nine (soon ten, with "Other") above the fold at 375px, which
   .category-grid's 140px minimum and home page padding can't do (was ~744px
   of buttons in a forced single column before this). Three narrower columns,
   centred content, no hover-only border since every tile here is already an
   equally-weighted choice, not a card with a photo to frame. */
.category-grid--compact {
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}
.category-grid--compact .category-tile {
  align-items: center;
  text-align: center;
  padding: var(--space-3) var(--space-2);
  gap: var(--space-2);
}
.category-grid--compact .category-tile__name {
  font-size: var(--text-sm);
  line-height: 1.2;
}

/* The narrow centred column every step of the sell flow uses. Was a
   `style="max-width: …; margin-inline: auto;"` repeated on each step's
   wrapper (CLAUDE.md rule 10) — lifted here for the step this task actually
   touches. The other four steps' identical inline pattern (560px, not 640px)
   is untouched; same fix, not yet applied, tracked as a follow-up rather
   than risked here. */
.sell-shell {
  max-width: 640px;
  margin-inline: auto;
}

/* ============================================================================
   FORM FIELDS
   Square inputs, matching the existing SecondWave contact form.
   ========================================================================== */

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

.field__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}

/* Mark optional fields rather than required ones — in this product most
   fields are required, so marking the exceptions is less visual noise. */
.field__optional {
  color: var(--text-muted);
  font-weight: var(--weight-regular);
}

.field__hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.input,
.textarea,
.select {
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-recessed);
  border: var(--hairline) solid var(--border);
  border-radius: var(--radius-none);
  color: var(--text-primary);
  font-size: var(--text-body);
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* 16px minimum on mobile or iOS Safari zooms the viewport on focus. */
@media (max-width: 767px) {
  .input,
  .textarea,
  .select {
    font-size: 16px;
  }
}

.input::placeholder,
.textarea::placeholder {
  color: var(--text-muted);
}

.input:hover,
.textarea:hover,
.select:hover {
  border-color: var(--border-strong);
}

.textarea {
  min-height: 120px;
  resize: vertical;
  line-height: var(--leading-normal);
}

.select {
  appearance: none;
  /* Chevron drawn as an inline SVG data URI so there is no image request and
     no icon font. Stroke colour matches --ink-60. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238f8f8f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 18px;
  padding-right: var(--space-8);
}

/* Error state carries a border change AND a message with an icon. Never rely
   on the border alone. */
.input[aria-invalid='true'],
.textarea[aria-invalid='true'],
.select[aria-invalid='true'] {
  border-color: var(--ink-100);
  border-width: 2px;
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--ink-100);
}

.field__error-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-top: 2px;
}

/* Prefix for currency inputs. Wrap the input in .input-group. */
.input-group {
  display: flex;
  align-items: stretch;
}

.input-group__prefix {
  display: grid;
  place-items: center;
  padding-inline: var(--space-4);
  background: var(--bg-recessed);
  border: var(--hairline) solid var(--border);
  border-right: 0;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* --- Checkbox and radio --------------------------------------------------- */
/* Custom-drawn so they match the monochrome system. The native input stays in
   the DOM for accessibility and is visually hidden behind the drawn control. */

.check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  min-height: var(--touch-min);
  padding-block: var(--space-2);
  cursor: pointer;
}

.check__input {
  appearance: none;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  margin-top: 1px;
  background: var(--bg-recessed);
  border: var(--hairline) solid var(--border-strong);
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}

.check__input[type='radio'] {
  border-radius: var(--radius-round);
}

.check__input:checked {
  background-color: var(--ink-100);
  border-color: var(--ink-100);
}

.check__input[type='checkbox']:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  background-size: 16px;
  background-position: center;
  background-repeat: no-repeat;
}

.check__input[type='radio']:checked {
  /* Inner dot drawn with a ring, so it works without an image. */
  box-shadow: inset 0 0 0 4px var(--ground-100);
}

.check__text {
  font-size: var(--text-body);
  line-height: var(--leading-snug);
}

/* ============================================================================
   STEP PROGRESS — for the guided listing flow
   Sellers abandon long forms. Showing exactly where they are, and that the end
   is close, is the single most effective thing we can do about that.
   ========================================================================== */

.steps {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.steps__item {
  flex: 1;
  height: 3px;
  background: var(--ground-60);
  transition: background-color var(--duration-base) var(--ease-out);
}

.steps__item--done { background: var(--ink-100); }
/* ink-80, not ink-100 — --done is already solid ink-100, so --current needs
   its own value or a completed step and the current step become the same
   solid white bar with nothing to tell them apart. Brightness now reads as
   progress: brightest (done) -> dimmer (current) -> --ground-60 (upcoming). */
.steps__item--current { background: var(--ink-80); }

.steps__label {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ============================================================================
   PHOTO CHECKLIST — guided listing
   Each category demands specific shots (a surfboard needs nose, tail, rails,
   deck, fin boxes). This kills the "hidden damage" dispute category and gives
   every listing a consistent, professional look.
   ========================================================================== */

.photo-slot {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--bg-recessed);
  border: var(--hairline) dashed var(--border-strong);
  color: var(--text-muted);
  text-align: center;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}

.photo-slot:hover {
  border-color: var(--ink-100);
  color: var(--ink-100);
}

.photo-slot--filled {
  border-style: solid;
  border-color: var(--border);
  padding: 0;
}

.photo-slot--filled img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Required shots that are still empty. Prompts, never blocks — a seller who
   genuinely cannot photograph the fin boxes should still be able to publish. */
.photo-slot--required {
  border-color: var(--ink-100);
  color: var(--ink-100);
}

.photo-slot__label {
  font-size: var(--text-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.photo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}

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

/* ============================================================================
   SELLER IDENTITY
   Appears on listings, messages and reviews. Always shows the verification
   state — trust is the product's differentiator and must be visible everywhere.
   ========================================================================== */

.seller {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.seller__avatar {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: var(--radius-round);
  object-fit: cover;
  background: var(--bg-recessed);
}

.seller__avatar--lg {
  width: 72px;
  height: 72px;
}

.seller__name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--weight-medium);
}

.seller__meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* --- Rating --------------------------------------------------------------- */
/* Stars are inline SVG. Never emoji — see the project's UI rules. */

.rating {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-variant-numeric: tabular-nums;
}

.rating__stars {
  display: inline-flex;
  gap: 1px;
}

.rating__star {
  width: 14px;
  height: 14px;
  fill: var(--ink-40);
}

.rating__star--filled {
  fill: var(--ink-100);
}

.rating__count {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ============================================================================
   RIDER STATS — profile
   The data that powers fit matching. Presented as a compact spec block so it
   reads as equipment-relevant information, not as personal body data.
   ========================================================================== */

.stats {
  display: grid;
  /* 140px, not 100px: at 375px 100px gave three ~110px columns, and a word
     value ("Intermediate") only fit by breaking mid-word. Two columns on a
     phone leave room for it on one line. */
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-1);
  border: var(--hairline) solid var(--border);
}

.stats__item {
  padding: var(--space-3) var(--space-4);
  background: var(--bg-raised);
}

.stats__value {
  font-size: var(--text-h3);
  font-weight: var(--weight-semi);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-heading);
  /* M17.1: nothing here stopped a word-valued stat (Ability: "Intermediate")
     from painting outside its cell — the track floor (minmax(90px, …) below)
     is a fixed minimum a word that size can't fit inside, and there was no
     wrap rule at all, so it overflowed into the next cell and got visually
     clipped by that cell's own background. Wrapping mid-word if it must is
     still correct here: a stat cell is not a place text needs to stay on
     one line, and it's the fallback of last resort under --text--word below,
     not the primary fix. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* A word-valued stat (Ability) reads oddly at the same heading-sized weight
   numeric stats use — "72kg" and "Intermediate" are different kinds of
   content wearing the same style. Smaller and not tabular (it's not a
   number), same label styling either way. */
.stats__value--word {
  font-size: var(--text-body-lg);
  font-variant-numeric: normal;
  letter-spacing: normal;
}

.stats__label {
  font-size: var(--text-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ============================================================================
   MESSAGES
   ========================================================================== */

.msg {
  display: flex;
  max-width: 78%;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-body);
  line-height: var(--leading-snug);
  border-radius: var(--radius-none);
}

/* Incoming — raised ground, left aligned. */
.msg--in {
  align-self: flex-start;
  background: var(--bg-raised);
  border: var(--hairline) solid var(--border);
}

/* Outgoing — inverted, right aligned. Inversion (not colour) marks ownership,
   which keeps the thread readable in monochrome. */
.msg--out {
  align-self: flex-end;
  background: var(--ink-100);
  color: var(--ground-100);
}

.msg-thread {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.msg__time {
  font-size: var(--text-xs);
  color: var(--text-muted);
  align-self: flex-end;
}

/* System messages mark real events in the transaction — payment taken,
   collection arranged, receipt confirmed. They are the audit trail a buyer
   relies on, so they are visually distinct from chat. */
.msg--system {
  align-self: center;
  max-width: 100%;
  background: transparent;
  border: var(--hairline) dashed var(--border);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-align: center;
}

/* ============================================================================
   EMPTY STATES
   ----------------------------------------------------------------------------
   Load-bearing in this product. Because listings are UK-wide but sparse at
   launch, "nothing near you" will be common early on. An empty state here must
   ALWAYS offer a way forward — widen the radius, browse nationally, save the
   search — and must never be a dead end.
   ========================================================================== */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-8) var(--space-4);
  text-align: center;
}

.empty__icon {
  width: 48px;
  height: 48px;
  color: var(--text-muted);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
}

.empty__title {
  font-size: var(--text-h3);
  font-weight: var(--weight-semi);
  letter-spacing: var(--track-heading);
}

.empty__body {
  max-width: 42ch;
  color: var(--text-secondary);
}

.empty__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
}

/* ============================================================================
   NAVIGATION
   Mobile: fixed bottom tab bar — Home, Explore, Sell, Messages, Profile.
   Desktop: the same destinations in the top header.
   ========================================================================== */

.appbar {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 64px;
  padding-inline: var(--gutter);
  /* M15.2: index.html's viewport meta now carries viewport-fit=cover, which
     is what makes env() resolve to anything but 0 in the first place — this
     bar sits sticky at the very top of a now-edge-to-edge layout, so without
     this it renders straight under a notch/Dynamic Island. .tabbar already
     had the equivalent bottom padding (base.css/components.css), but it was
     silently inert for the same reason: no viewport-fit=cover to feed it. */
  padding-top: env(safe-area-inset-top);
  background: var(--bg-page);
  border-bottom: var(--rule);
}

.appbar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--weight-semi);
  font-size: var(--text-body-lg);
  letter-spacing: var(--track-heading);
  text-decoration: none;
}

.appbar__mark {
  width: 49px;
  height: 28px;
  object-fit: contain;
  flex-shrink: 0;
  /* base.css gives every <img> a recessed background as a loading/letterbox
     placeholder for listing photography. The logo is a transparent PNG, not
     a photo, so that background shows through as a visible grey box around
     the mark instead of blending into the page. */
  background: transparent;
}

/* Desktop echo of the tabbar, in the appbar. Same five destinations, same
   aria-current contract, styled as horizontal pills instead of a bottom bar. */
.appbar__nav {
  display: none;
  align-items: center;
  gap: var(--space-1);
}

@media (min-width: 768px) {
  .appbar__nav {
    display: flex;
  }
}

.appbar__nav-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.appbar__nav-item:hover {
  background: var(--bg-hover);
  color: var(--ink-100);
}

.appbar__nav-item[aria-current='page'] {
  background: var(--ink-100);
  color: var(--ground-100);
}

/* The client router's mount point. Reserves space so the fixed mobile
   tabbar never overlaps the last thing on screen. */
.view {
  min-height: 60vh;
  padding-block: var(--space-6);
}

@media (max-width: 767px) {
  .view {
    padding-bottom: calc(56px + env(safe-area-inset-bottom) + var(--space-6));
  }
}

.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  display: flex;
  background: var(--bg-page);
  border-top: var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}

@media (min-width: 768px) {
  .tabbar {
    display: none;
  }
}

.tabbar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 56px;
  padding-block: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-xs);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-out);
}

/* Current tab is marked by colour AND weight AND aria-current, so it never
   depends on the accent alone. */
.tabbar__item[aria-current='page'] {
  color: var(--ink-100);
  font-weight: var(--weight-semi);
}

.tabbar__icon {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Unread dot on the Messages tab. */
.tabbar__dot {
  position: absolute;
  top: 6px;
  margin-left: 16px;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-round);
  background: var(--ink-100);
}

/* ============================================================================
   SHEETS AND OVERLAYS
   Filters open in a bottom sheet on mobile, a panel on desktop.
   ========================================================================== */

.sheet {
  position: fixed;
  inset: auto 0 0 0;
  z-index: var(--z-sheet);
  max-height: 88dvh;
  overflow-y: auto;
  background: var(--bg-raised);
  border-top: var(--hairline) solid var(--border);
  box-shadow: var(--shadow-overlay);
  padding: var(--space-5) var(--gutter) calc(var(--space-5) + env(safe-area-inset-bottom));
}

.scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(2px);
}

/* ============================================================================
   SITE FOOTER
   Persistent site-wide chrome (M6.2/M6.3): brand, policy links, copyright.
   Lives outside #view-root in index.html, so it needs its own mobile
   clearance above the fixed tabbar — same reasoning as .view.
   ========================================================================== */

.site-footer {
  border-top: var(--rule);
  margin-top: var(--space-9);
  padding-block: var(--space-8) var(--space-6);
}

@media (max-width: 767px) {
  .site-footer {
    padding-bottom: calc(56px + env(safe-area-inset-bottom) + var(--space-6));
  }
}

.site-footer__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
}

.site-footer__nav a {
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--text-sm);
}

.site-footer__nav a:hover {
  color: var(--text-primary);
}

/* ============================================================================
   UTILITY — skeleton loading
   Listing grids load asynchronously; showing the shape of what's coming beats
   a spinner. Animation is suppressed under prefers-reduced-motion via tokens.
   ========================================================================== */

.skeleton {
  background: linear-gradient(
    90deg,
    var(--ground-80) 25%,
    var(--ground-70) 50%,
    var(--ground-80) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
}

/* ============================================================================
   FEE BREAKDOWN (PLAN.md M22)
   When to use: anywhere a price is shown alongside SecondWave's transaction
   fee — the sell flow's price step, checkout, an order, the Fees page. Always
   label/value rows, so the fee is stated in words and figures and never
   signalled by colour. The last row (--total) is what the seller keeps.
   ========================================================================== */

.fee-breakdown {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--bg-recessed);
  border: var(--rule);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

/* display:grid above would otherwise beat the [hidden] attribute. */
.fee-breakdown[hidden] {
  display: none;
}

.fee-breakdown__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  color: var(--text-secondary);
}

.fee-breakdown__row--total {
  padding-top: var(--space-2);
  border-top: var(--rule);
  color: var(--text-primary);
  font-weight: var(--weight-semi);
}

.fee-breakdown__note {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* ============================================================================
   REPORT CONTROL (PLAN.md M28.11)
   When to use: a quiet way to flag user-generated content — the listing page,
   a seller's public profile, a message thread. Collapsed to a single ghost
   button so it never competes with the page's real action; the form opens
   inline. Built by web/js/report.js.
   ========================================================================== */

.report {
  margin-top: var(--space-6);
}

.report__form {
  display: grid;
  gap: var(--space-4);
  max-width: 480px;
  margin-top: var(--space-3);
  padding: var(--space-5);
  border: var(--rule);
}

/* display:grid above would otherwise beat the [hidden] attribute. */
.report__form[hidden] {
  display: none;
}

.report__done,
.report__signin {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
