/* ==========================================================================
   The directory card.

   Shared on purpose. The public page renders these from src/lib/render.js;
   the partner portal builds the same markup in public/js/partner-preview.js
   so a partner sees their real card while they edit it. One stylesheet, so
   the preview cannot quietly drift away from the thing it is previewing.

   Depends on tokens.css. Contains no layout above the card itself -- the
   public grid and the portal's preview column each place it their own way.
   ========================================================================== */

.card {
  /* Both are for the optional background photo below: the photo is a child
     painted behind the text, and without its own stacking context a negative
     z-index would drop it behind the card entirely. */
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1.05rem 1.05rem 1.15rem;
  background: var(--surface);
  border: var(--line-w) solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  transition:
    box-shadow 0.18s ease,
    transform 0.18s ease;
}

/*
 * The partner's own background photo.
 *
 * A separate layer rather than a background on .card itself, for the same
 * reason the hero splits its photo from its scrim: opacity on the element
 * would fade the text with the picture. Here the layer fades and the words on
 * top stay at full strength.
 *
 * Both custom properties are set per listing, in the page's nonce'd style
 * block. A card without a photo has no --card-image, so this paints nothing.
 */
.card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: var(--card-image, none);
  background-size: cover;
  background-position: center;
  opacity: var(--card-image-opacity, 0);
  pointer-events: none;
}

.card[hidden] {
  display: none;
}


.card__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* Logo slot. Fixed square so every row lines up whether the listing has a
   real logo file or falls back to initials. */
/* Height-limited and nothing else: a logo is never cropped or stretched, so a
   wide one stays wide and a tall one stays tall. Only the monogram fallback
   below is square, because it is type rather than artwork.

   Nothing sits behind or around it: no plate, no stroke, no shadow. A logo cut
   out to a shape has to read as that shape, and every one of those would draw a
   box back around it. */
.card__logo {
  flex: 0 0 auto;
  width: auto;
  height: 3.25rem;
  max-width: 9rem;
  object-fit: contain;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  outline: 0;
  filter: none;
}

/* The fallback is type rather than artwork, so it keeps its plate: two initials
   floating on the card would read as a mistake. */
.card__logo--monogram {
  width: 3.25rem;
  display: grid;
  place-items: center;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--brand);
  background: var(--accent-soft);
  border-radius: 10px;
}


.card__headings {
  flex: 1 1 auto;
  min-width: 0;
}

.card__name {
  font-size: 1.08rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

.card__category {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  margin-top: 0.15rem;
}



/* Landscape banner between the service title and the description. */
.card__banner {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--banner-ratio);
  object-fit: cover;
  border-radius: var(--radius-sm);
  /* A tint under the image so a transparent PNG or a slow load is not a
     white hole in the middle of a white card. */
  background: var(--accent-soft);
}

/* The short line describing the service. */
.card__title {
  font-weight: 600;
  color: var(--ink);
  font-size: 0.97rem;
  line-height: 1.35;
}

/* Self-defined tags. Plain labels -- nothing here is clickable. */
.card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.tag {
  font-size: 0.73rem;
  font-weight: 500;
  padding: 0.2rem 0.55rem;
  color: var(--ink-soft);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
}

.card__desc {
  font-size: 0.9rem;
  color: var(--ink-soft);
}




/* Three routes out, laid out predictably instead of left to wrap: the
   website spans the row, the phone number and map share the one below. A
   phone number is long, so it gets a full half rather than shrink-to-fit. */
.card__actions {
  display: grid;
  /*
   * 9rem is roughly the width of the longest label we render -- a formatted
   * phone number. Below that the row collapses to one button per line rather
   * than shrinking a button until its text no longer fits.
   */
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: 0.45rem;
  margin-top: auto;
  padding-top: 0.65rem;
}

.card__actions .btn--site {
  grid-column: 1 / -1;
}

/* With no map details there is nothing to put beside the phone number, so it
   takes the row rather than sitting in half of one with a hole next to it.
   Same for a listing with a map but no phone. */
.card__actions .btn--call:nth-child(2):last-child,
.card__actions .btn--map:nth-child(2):last-child {
  grid-column: 1 / -1;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--tap);
  padding: 0.5rem 1rem;
  font-size: 0.92rem;
  font-weight: 600;
  font-family: inherit;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  min-width: 0;
  padding-inline: 0.55rem;
  text-align: center;
  /* Single line, always. The clamp shrinks the label a little on the narrowest
     cards; the ellipsis is the backstop so a long label can never bleed past
     the button edge. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: clamp(0.8rem, 0.74rem + 0.25vw, 0.92rem);
}

.btn--site {
  color: var(--action-ink);
  background: var(--action-site);
}

/* Outlined: blue text and blue border on white, so the row reads as one solid
   primary action plus two secondary ones rather than three competing blocks. */
.btn--call {
  color: var(--action-call);
  background: var(--surface);
  border-color: var(--action-call);
}

.btn--map {
  color: var(--action-ink);
  background: var(--action-map);
}


@media (hover: hover) {
  .card:hover {
    box-shadow: var(--shadow-lift);
    transform: translateY(-3px);
  }

  /* Each of the three moves to a stated colour rather than a filter: the solid
     ones step a shade deeper, and the outlined phone button fills light blue,
     because brightness() does nothing at all to a white background. The lift
     and the press come from the shared layer in forms.css. */
  .btn--site:hover {
    background: var(--action-site-hover);
  }

  .btn--call:hover {
    background: var(--action-call-hover);
    border-color: var(--action-call);
  }

  .btn--map:hover {
    background: var(--action-map-hover);
  }
}

/* --------------------------------------------------------------------------
   Map snippet under each listing
   -------------------------------------------------------------------------- */
.card__map {
  margin-top: 0.65rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}

.card__map-img {
  display: block;
  width: 100%;
  height: 7rem;
  object-fit: cover;
  /* Reserved height, so the card does not jump when the tile arrives. */
  background: var(--line);
}

.card__map-label {
  padding: 0.4rem 0.6rem;
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--ink-soft);
}

/* Without a static-map key the box is just the address line, so it should not
   pretend to be an image frame. */
.card__map--textonly {
  background: transparent;
  border-style: dashed;
}

.card__map-pin {
  color: var(--action-map);
  font-size: 0.7em;
  vertical-align: 0.15em;
}



/* --------------------------------------------------------------------------
   Open / closed indicator

   Top right, above the head row rather than inside it, so a long business name
   wraps against the card edge instead of against the badge.
   -------------------------------------------------------------------------- */

.card__hours {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  z-index: 2;
}

.card__status {
  /* A button now rather than a paragraph, so it needs the reset that comes
     with that. Deliberately no colour: the state modifiers below set it, and
     a colour here would have to be overridden by every one of them. */
  font-family: inherit;
  cursor: pointer;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.5rem 0.15rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.01em;
  white-space: nowrap;
  border-radius: 999px;
  /* Its own surface, because the card behind it may be carrying a photo. */
  background: var(--surface);
  border: 1px solid var(--line);
}

.card__status-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
  /* The dot is the signal for anyone reading at a glance; the ring lifts it off
     a background photo without needing a second colour. */
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 22%, transparent);
}

/* Colour lives on the parent so the dot, the text and the ring all take it. */
.card__status--open {
  color: var(--ok);
}

.card__status--closing-soon,
.card__status--opening-soon {
  color: var(--warn);
}

.card__status--closed {
  color: var(--danger);
}

/*
 * The head needs to keep clear of the badge, but only on the side it sits on
 * and only as far down as it actually reaches.
 */
.card__hours ~ .card__head .card__headings {
  padding-right: 5.5rem;
}

/* --------------------------------------------------------------------------
   Emergency line

   Shown on the phone button in place of the usual number once the business is
   shut. It has to read as a different number to ring, not as a louder version
   of the same one.
   -------------------------------------------------------------------------- */

.btn--emergency {
  color: var(--brand-ink);
  background: var(--brand);
  border-color: var(--brand);
  /* Two lines: the flag, then the number. The column does that on its own --
     the flag and the number are separate flex items -- so the nowrap inherited
     from .btn stays, and a phone number can never break across lines. */
  flex-direction: column;
  gap: 0;
  line-height: 1.15;
  padding-block: 0.35rem;
}

/*
 * The phone button while the doors are shut: still there, still readable, and
 * inert. Muted rather than hidden, because a number that vanishes in the
 * evening makes a listing look pulled -- and somebody who wants to ring at
 * eight in the morning still needs to see there is a number to ring.
 *
 * The hover lift and the press are already withheld: the shared layer in
 * forms.css skips anything carrying aria-disabled.
 */
.btn--call.is-shut {
  color: var(--ink-faint);
  background: var(--surface-sunken);
  border-color: var(--line);
  cursor: not-allowed;
  /* Two lines, the same as the emergency variant: the flag, then the number. */
  flex-direction: column;
  gap: 0;
  line-height: 1.15;
  padding-block: 0.35rem;
}

.btn__flag {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  opacity: 0.85;
}

/* ==========================================================================
   List view

   The same card, the same content, the same three ways out -- turned on its
   side into a full-width row. Nothing is hidden: a row that dropped the banner
   or the tags would be a different listing, not a different view of one.

   Laid out with grid areas rather than wrappers, because the card's children
   are flat siblings by design -- one render function, shared with the portal
   preview. Grid is the one tool that places flat siblings in two dimensions
   without adding markup purely for layout. Every element gets its own area, so
   nothing can land on top of anything else when a listing happens to be
   missing a piece.
   ========================================================================== */

@media (min-width: 48rem) {
  .grid[data-view='list'] .card {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr) minmax(12rem, 14rem);
    /* Only the last row stretches, so the words stay stacked against each
       other instead of drifting apart to fill a tall row. */
    grid-template-rows: auto auto auto 1fr;
    grid-template-areas:
      'banner head  status'
      'banner title offers'
      'banner desc  actions'
      'banner tags  map';
    align-items: start;
    column-gap: 1.25rem;
    row-gap: 0.45rem;
  }

  /* No banner, no banner column -- otherwise every listing without artwork
     opens with thirteen rems of nothing. */
  .grid[data-view='list'] .card:not(:has(.card__banner)) {
    grid-template-columns: minmax(0, 1fr) minmax(12rem, 14rem);
    grid-template-areas:
      'head  status'
      'title offers'
      'desc  actions'
      'tags  map';
  }

  /*
   * The open/closed badge is pinned to the card's top-right corner, which in a
   * row is exactly where the Offers button lands -- it sat straight on top of
   * it. Here it takes a place in the flow instead, at the head of the column
   * of things you can act on.
   */
  .grid[data-view='list'] .card__status {
    position: static;
    grid-area: status;
    justify-self: end;
  }

  .grid[data-view='list'] .card__banner {
    grid-area: banner;
    align-self: stretch;
    /* Squarer than the card's 5:2 strip: a wide banner beside a tall row would
       leave the picture stranded in white space. */
    aspect-ratio: 4 / 3;
    height: auto;
    max-height: 10.5rem;
  }

  .grid[data-view='list'] .card__head { grid-area: head; }
  .grid[data-view='list'] .card__title { grid-area: title; }
  .grid[data-view='list'] .card__desc { grid-area: desc; }

  /* Directly under the description rather than at the foot of the row: the
     stretch belongs to this row, not to the space above the tags. */
  .grid[data-view='list'] .card__tags {
    grid-area: tags;
    align-self: start;
    margin-top: 0.15rem;
  }

  /* The ways out get their own column, so they line up down the page however
     tall each row turns out to be. */
  .grid[data-view='list'] .card__offers-btn { grid-area: offers; }

  .grid[data-view='list'] .card__actions {
    grid-area: actions;
    align-self: start;
  }

  .grid[data-view='list'] .card__map {
    grid-area: map;
    align-self: end;
  }
}

/*
 * Wide enough for the actions to pair up.
 *
 * .card__actions keeps whole 9rem columns or none -- it will collapse to one
 * button per line rather than shrink one until a phone number no longer fits.
 * At 12rem that collapse is the right call and the row runs tall; give the
 * column 19rem and the phone and the map sit side by side under the website,
 * which takes about 140px off every row. In a view whose whole purpose is
 * seeing more listings at once, that is the difference worth having.
 */
@media (min-width: 75rem) {
  .grid[data-view='list'] .card {
    grid-template-columns: 13rem minmax(0, 1fr) 19rem;
  }

  .grid[data-view='list'] .card:not(:has(.card__banner)) {
    grid-template-columns: minmax(0, 1fr) 19rem;
  }
}

/*
 * Below 48rem a row has nowhere to go sideways -- three columns inside 380px is
 * not a list, it is a squeeze. The card falls back to exactly what it is in
 * grid view and the toggle stops making a visible difference, which is why the
 * switch hides itself at that width rather than lying about what it does.
 */


.card__status:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   The schedule panel

   A popover under the badge rather than a dialog: reading opening hours is a
   glance, not a task, and a modal that dims the page for it is too much
   machinery. It opens on hover and on focus for a mouse or a keyboard, and on
   tap for a phone -- which is most of this traffic, and which has no hover at
   all.
   -------------------------------------------------------------------------- */

.hours-panel {
  position: absolute;
  top: calc(100% + 0.35rem);
  right: 0;
  z-index: 3;
  width: max-content;
  min-width: 11rem;
  max-width: min(15rem, 70vw);
  padding: 0.6rem 0.7rem;
  font-size: 0.76rem;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0;
  text-align: left;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
}

.hours-panel[hidden] {
  display: none;
}

.hours-panel__now {
  margin: 0 0 0.4rem;
  padding-bottom: 0.4rem;
  font-weight: 650;
  border-bottom: 1px solid var(--line);
}

.hours-panel__row {
  display: flex;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.08rem 0;
  color: var(--ink-soft);
}

.hours-panel__row.is-today {
  font-weight: 650;
  color: var(--ink);
}

.hours-panel__day {
  flex: 0 0 2.2rem;
}

.hours-panel__spans {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.hours-panel__emergency {
  margin: 0.45rem 0 0;
  padding-top: 0.4rem;
  font-weight: 650;
  color: var(--brand);
  border-top: 1px solid var(--line);
}

.hours-panel__emergency-label {
  display: block;
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

/*
 * .card isolates its own stacking context, so a panel escaping the card would
 * still be painted under the next card in the grid. Lifting the whole card
 * while its panel is open is what puts it on top.
 */
.card.is-hours-open {
  z-index: 5;
}

/* A card near the right edge would push its panel off screen; anchoring it to
   the left of the badge instead keeps it on the page. */
.hours-panel--flip {
  right: auto;
  left: 0;
}
