/* =============================================================================
   Hero system + motion

   Two hero shapes, on purpose:

   .hero--home  full-bleed photograph. The homepage is the brand statement and the
                photograph IS the message.
   .phero       split — claim on one side, evidence on the other. Every inner page.
                The old build used the full-bleed shape everywhere, which meant all
                thirteen pages opened identically and none of them announced what
                they were for. It also forced every photo through a 2:1 crop, which
                is how a square karaoke shot became a close-up of two faces.
   ============================================================================= */

/* --- Motion primitives --------------------------------------------------------
   One reveal, driven by IntersectionObserver in motion.js. Elements start hidden
   ONLY when the script has confirmed it is running (html.js) — otherwise content
   that never gets observed would be invisible forever, which is how "tasteful
   animation" turns into a blank page. */
html.js [data-reveal] {
  opacity: 0;
  transform: translateY(22px);
}
html.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity .72s var(--ease), transform .72s var(--ease);
}
html.js [data-reveal="left"] { transform: translateX(-30px); }
html.js [data-reveal="right"] { transform: translateX(30px); }
html.js [data-reveal="card"] { transform: translateY(24px) scale(.975); }
html.js [data-reveal="media"] {
  transform: translateY(18px) scale(.985);
  clip-path: inset(8% 0 0 0 round var(--radius-lg));
}
html.js [data-reveal="media"].is-in {
  clip-path: inset(0 0 0 0 round var(--radius-lg));
  transition: opacity .8s var(--ease), transform .8s var(--ease), clip-path .9s var(--ease);
}
[data-reveal-delay="1"] { transition-delay: .07s; }
[data-reveal-delay="2"] { transition-delay: .14s; }
[data-reveal-delay="3"] { transition-delay: .21s; }
[data-reveal-delay="4"] { transition-delay: .28s; }
[data-reveal-delay="5"] { transition-delay: .35s; }
[data-reveal-delay="6"] { transition-delay: .42s; }
[data-reveal-delay="7"] { transition-delay: .49s; }

@media (prefers-reduced-motion: reduce) {
  html.js [data-reveal] {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: none;
  }
}

/* --- Split hero --------------------------------------------------------------- */
.phero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--ink-900);
  color: var(--ink-000);
  padding-block: var(--space-2xl);
}
.phero__glow {
  position: absolute;
  inset: -30% -10% auto auto;
  width: min(70vw, 780px);
  aspect-ratio: 1;
  z-index: -1;
  background: radial-gradient(circle, rgba(190,60,220,.42) 0%, rgba(190,60,220,0) 62%);
  pointer-events: none;
}
.phero::after {
  /* Fine diagonal rule, the same texture as the designed hero, so a page with a
     photograph and a page without still look like the same site. */
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: repeating-linear-gradient(115deg, rgba(255,255,255,.045) 0 1px,
                                              transparent 1px 24px);
}

.phero__inner {
  display: grid;
  gap: var(--space-xl);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 56rem) {
  /* Block padding scales with viewport HEIGHT, so a taller hero image can use the room
     on a big screen without pushing the fold down on a laptop. */
  .phero { padding-block: clamp(var(--space-l), 9vh, var(--space-3xl)); }
  /* The media column is the wider of the two: a landscape photograph shown in full needs
     width to have any presence, where a cropped portrait panel did not.
     Widened again, and the gap tightened, because a LANDSCAPE hero is bound by this
     column and not by the height cap below it — at .96/1.24 the panel stopped at 703px
     and the height cap never came into play, so raising that cap did nothing at all for
     the two thirds of the library that is landscape. */
  .phero__inner { grid-template-columns: minmax(0, .84fr) minmax(0, 1.36fr); gap: var(--space-xl); }
}

.phero__kicker {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  /* Balanced, so a wrapped kicker does not leave "AREA TOWNS" hanging alone on a third
     line with the break falling inside "Houston-area". */
  text-wrap: balance;
  margin: 0 0 var(--space-s);
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--brand-200);
}
.phero__kicker::before {
  content: "";
  width: 34px;
  height: 2px;
  background: var(--brand-400);
  flex: 0 0 auto;
}

.phero__title {
  font-size: var(--step-5);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  margin: 0 0 var(--space-s);
  max-width: 15ch;
}
.phero__intro {
  font-size: var(--step-1);
  line-height: 1.5;
  color: rgba(255,255,255,.86);
  max-width: 44ch;
  margin: 0 0 var(--space-l);
}
.phero__actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }

/* The image panel. A soft frame plus an offset accent edge, so the photograph reads
   as placed rather than pasted. */
.phero__media {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-3);
  /* The image's own ratio, handed over as an inline custom property by the template.
     A fixed box here cropped 40-55% off the width of every landscape hero. */
  aspect-ratio: var(--hero-ratio, 16 / 10);
  border: 1px solid rgba(255,255,255,.16);
}
@media (min-width: 56rem) {
  /* No fixed ratio and a much larger ceiling, so the whole photograph is visible and
     is actually big enough to look at. The cap is on height only, to stop a tall
     portrait source pushing the fold down. */
  .phero__media {
    aspect-ratio: var(--hero-ratio, 4 / 5);
    /* A flat 38rem left the photograph 600px tall inside a 790px section on a desktop —
       a lot of empty panel around a small picture. Take the room where the screen has
       it and give it back where it does not: 66vh wins on a laptop, 44rem on a tall
       monitor, so the hero never outgrows the fold. */
    max-height: min(50rem, 74vh);
    /* width: auto, not 100%. With a fixed width the height cap fights the aspect ratio:
       a 1122x1402 portrait wanted 854px of height, got clamped to 608, and the box
       became 1.12 instead of 0.80 — so the image letterboxed inside it with grey bands.
       Sizing from the ratio and capping the height keeps the box exactly the shape of
       the photograph at every size. */
    width: auto;
    max-width: 100%;
    margin-inline-start: auto;
  }
}
.phero__media img,
.phero__media picture {
  width: 100%; height: 100%;
  display: block;
  /* The panel already matches the image, so this crops nothing. `contain` rather than
     `cover` is the safe default: if --hero-ratio were ever missing, this letterboxes
     instead of silently cutting the subject out of frame. */
  object-fit: contain;
}
.phero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 58%, rgba(12,5,18,.28) 100%);
  pointer-events: none;
}
.phero__media-line {
  position: absolute;
  inset: auto 0 0;
  height: 4px;
  z-index: 1;
  background: linear-gradient(90deg, var(--brand-300), var(--brand-500), transparent 85%);
  transform-origin: left;
  animation: media-line-in .8s .35s var(--ease) both;
}
@keyframes media-line-in { from { transform: scaleX(0); } }
/* Slow, barely-there drift. Enough to stop the page feeling like a printed sheet,
   small enough that nobody has to watch it. */
.phero__media img { animation: hero-drift 22s var(--ease) infinite alternate; }
@keyframes hero-drift {
  from { transform: scale(1.02) translate3d(0, 0, 0); }
  to   { transform: scale(1.09) translate3d(-1.2%, -1.4%, 0); }
}

.phero--nomedia .phero__inner { grid-template-columns: 1fr; }
.phero--nomedia .phero__title { max-width: 20ch; }
.phero--nomedia { background: linear-gradient(160deg, var(--ink-900) 0%, var(--brand-800) 62%, var(--ink-900) 100%); }

/* --- Home hero ---------------------------------------------------------------- */
/* svh, not vh: on mobile `vh` is the LARGEST viewport — toolbars hidden — so a
   vh-sized hero is taller than the screen actually shows until the user scrolls.
   Subtracting the header keeps the whole hero, headline included, on one screen;
   min() means the shorter of the two always wins, so a laptop at 720px and a
   landscape phone at 400px both stay inside. */
.hero--home .hero__body {
  min-height: min(78vh, calc(100svh - var(--header-h)));
}
.hero--home .hero__media img { animation: hero-drift 26s var(--ease) infinite alternate; }
.hero__kicker {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0 0 var(--space-xs);
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--brand-200);
}
/* align-self, not the flex container's align-items: center.
   "15 venues - 27 regular nights - 8 Houston-area towns" is one line on a desktop and
   FOUR on a phone, and a centred rule beside a four-line block floats away on its own
   next to line three, reading as a stray dash rather than as the mark that opens the
   sentence. Pinned to the top and dropped by half a line, it sits against the first
   line at every wrap. */
.hero__kicker::before {
  content: "";
  align-self: flex-start;
  margin-top: .55em;
  width: 34px;
  height: 2px;
  flex: 0 0 auto;
  background: var(--brand-400);
}
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-l); }

/* --- Home carousel ------------------------------------------------------------
   The old GoDaddy carousel proved that rotating HPN photography can establish range,
   but it hid four unnamed images behind 10px mouse-only dots and gave visitors no way
   to pause. This version keeps one stable proposition over the pictures and treats the
   controls as real, labelled controls. */
.hero-carousel {
  min-height: min(78vh, calc(100svh - var(--header-h)));
  overflow: hidden;
  background: var(--ink-900);
}
.hero-carousel__stage {
  grid-area: 1 / 1;
  position: relative;
  min-width: 0;
  overflow: hidden;
}
/* The scrim that makes the copy legible on ANY slide.
   On the stage rather than on the copy, because it has to hold over the whole set — the
   foam-party photograph is near-white on its right-hand side and the bar photograph is
   nearly black, and a treatment tuned to either one fails on the other. Two gradients:
   one from the left for the headline column, one from the bottom for the controls. Both
   fade to fully transparent, so the right two-fifths of the picture is untouched. */
.hero-carousel__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(8,6,12,.72) 0%, rgba(8,6,12,.54) 30%,
                           rgba(8,6,12,.20) 55%, rgba(8,6,12,0) 74%),
    linear-gradient(0deg,  rgba(8,6,12,.62) 0%, rgba(8,6,12,.22) 20%,
                           rgba(8,6,12,0) 42%);
}
.hero-carousel__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transform: scale(1.025);
  transition: opacity .9s var(--ease), visibility 0s linear .9s;
}
.hero-carousel__slide.is-active {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  transition: opacity .9s var(--ease), visibility 0s linear 0s;
}
.hero-carousel__slide img {
  /* The subtle drift happens only inside the selected layer. Hidden photographs stay
     still, which avoids burning paint work on three invisible images. */
  animation: none;
  transform: scale(1.035);
  transition: transform 7.8s linear;
}
.hero-carousel__slide.is-active img { transform: scale(1.085); }

.hero-carousel .hero__body {
  justify-content: center;
  padding-bottom: clamp(8.5rem, 20vh, 11rem);
}
/* The copy sits ON the photograph, not in a panel on top of it.
   It used to be a bordered, blurred, drop-shadowed card with a purple bar down its left
   edge — six separate decorations fighting the picture behind them, which is what made
   the hero read as a widget dropped onto a background rather than one image. The
   photograph is the brand statement here; the type belongs in it.
   Contrast is carried by .hero-carousel__stage::after below, which is a property of the
   STAGE and therefore guaranteed over every slide, including the near-white foam one. */
.hero-carousel__copy {
  position: relative;
  max-width: 44rem;
  /* Only the text shadow remains, and only as insurance: the scrim does the work, this
     covers the highlight that lands under a descender anyway. */
  text-shadow: 0 2px 18px rgba(0,0,0,.55);
}
.hero-carousel__copy .hero__sub { max-width: 34rem; }

.hero-carousel__controls {
  position: absolute;
  inset: auto 0 clamp(var(--space-s), 3vh, var(--space-l));
  z-index: 3;
  color: var(--ink-000);
}
.hero-carousel__controls-inner {
  display: flex;
  align-items: flex-end;
  gap: var(--space-s);
}
/* Four labelled tabs sharing one hairline rail.
   They were four separate bordered, blurred, filled boxes — which at a glance read as
   four disabled input fields sitting on the photograph, not as a way to change the
   picture. The rail says "these belong together and one of them is current"; the filled
   segment says how long is left on it. Same markup, same behaviour, no chrome. */
.hero-carousel__tabs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  flex: 1;
  gap: 0;
  max-width: 46rem;
}
.hero-carousel__tab {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: .5em;
  min-height: var(--tap-min);
  padding: var(--space-2xs) var(--space-xs) var(--space-2xs) 0;
  border: 0;
  /* The rail. A top border rather than a pseudo-element, so the four segments meet
     exactly with no seam and no rounding. */
  border-top: 2px solid rgba(255,255,255,.22);
  background: none;
  color: rgba(255,255,255,.72);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* The progress fill, drawn over this tab's own segment of the rail. */
.hero-carousel__tab::after {
  content: "";
  position: absolute;
  inset: -2px auto auto 0;
  width: 100%;
  height: 2px;
  background: var(--brand-300);
  transform: scaleX(0);
  transform-origin: left;
}
.hero-carousel__tab:is(:hover, :focus-visible) {
  color: #fff;
  border-top-color: rgba(255,255,255,.55);
}
.hero-carousel__tab:focus-visible {
  outline: 3px solid var(--brand-300);
  outline-offset: 3px;
}
.hero-carousel__tab.is-active { color: #fff; }
/* The filled segment says which slide you are on. It used to FILL, animating over the
   seven seconds until the slideshow moved itself along; with the autoplay gone there is
   no elapsing time to draw, so it is simply filled. */
.hero-carousel__tab.is-active::after { transform: scaleX(1); }
.hero-carousel__tab-number {
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: .12em;
  color: var(--brand-200);
  font-variant-numeric: tabular-nums;
}
.hero-carousel__tab-label {
  font-size: var(--step--1);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
}
/* Previous / next, on the edges of the picture and vertically centred.
   `.hero` is position:relative, so these anchor to the whole carousel rather than to the
   stage — which matters because the stage is a grid area and the copy sits on top of it. */
.hero-carousel__arrow {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 4;
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  padding: 0;
  border: 1px solid rgba(255,255,255,.32);
  border-radius: 50%;
  background: rgba(8,6,12,.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: #fff;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}
.hero-carousel__arrow--prev { left: var(--space-s); }
.hero-carousel__arrow--next { right: var(--space-s); }

/* Between a small laptop and a wide one the container's own 1rem gutter puts the copy
   exactly where the left arrow is, so the headline ran underneath it — measured at 1024
   and 768. Widen the gutter for the carousel only, on BOTH the copy and the controls so
   they stay aligned with each other, and only in the range where it is needed: above
   90rem the container is already inset past the arrows, and below 36rem the arrows
   shrink and tuck into the corner instead. */
@media (min-width: 36rem) and (max-width: 90rem) {
  .hero-carousel .hero__body > .container,
  .hero-carousel__controls > .container { padding-inline: 3.25rem; }
}
.hero-carousel__arrow:is(:hover, :focus-visible) {
  background: var(--brand-600);
  border-color: var(--brand-500);
}
.hero-carousel__arrow:focus-visible { outline: 3px solid var(--brand-300); outline-offset: 3px; }

@media (max-width: 54rem) {
  /* The scrim turns through 90 degrees, because the hero does.

     The desktop treatment is a LEFT-TO-RIGHT wash: opaque under the headline column,
     fully clear by 74% of the width, so the right two-fifths of the photograph is
     untouched. That is exactly right at 1440px and exactly wrong at 390px, where 74% of
     the width is 289px and the clear zone is a 100px strip past the edge of the copy.
     Every slide came out as a dark purple smear with half a face at each edge -- the one
     page where the photograph IS the message, and the photograph could not be seen.

     Vertically. The stops are measured, not judged: the page was rendered at 390x844
     with the copy hidden and every band sampled against all four slides. The top
     ninth is above the copy entirely, so it carries almost no wash and the
     photograph reads; from there it ramps hard, because the first line of copy
     starts at 20% and has to be legible over whichever picture is showing.

     Worst case across the four, measured on the pixels behind each element with the
     copy hidden: kicker 5.31:1, headline 16.21:1, paragraph 18.20:1.

     The kicker is what sets the ramp, and it is the reason the 21% stop is .60 rather
     than the .52 that would do for the rest. It is --brand-200 at 18.2px/700 -- just
     under the 18.66px bold that WCAG counts as large text, so it needs 4.5:1 and not
     the 3:1 the headline gets. Before this rule turned the wash upright it measured
     3.70:1 to 3.93:1 on three of the four photographs; at .52 it came back at 4.68:1,
     which passes and leaves nothing for the next photograph Craig uploads.

     54rem rather than 36rem: the turn happens as soon as the copy runs the full width,
     which is what makes the horizontal wash meaningless, not at some narrower phone. */
  .hero-carousel__stage::after {
    background:
      linear-gradient(180deg, rgba(8,6,12,.20) 0%, rgba(8,6,12,.08) 11%,
                              rgba(8,6,12,.60) 21%, rgba(8,6,12,.76) 44%,
                              rgba(8,6,12,.90) 100%);
  }
  .hero-carousel .hero__body {
    justify-content: flex-end;
    padding-bottom: 8.5rem;
  }
  .hero-carousel__copy {
    max-width: 38rem;
    padding: var(--space-m);
  }
  .hero-carousel__controls-inner { align-items: center; }
  .hero-carousel__tabs {
    display: flex;
    justify-content: center;
    gap: var(--space-3xs);
  }
  .hero-carousel__tab {
    display: grid;
    place-items: center;
    width: var(--tap-min);
    min-width: var(--tap-min);
    height: var(--tap-min);
    padding: 0;
    border-top: 0;
    text-align: center;
  }
  /* No rail at this size, so each tab carries its own short track under the number and
     the progress sweeps along it.
     Track and fill have to be two different things. Sharing one element made the CURRENT
     slide the only one drawn part-width while the other three sat at full width — so the
     selected tab read as the least selected, which is exactly backwards. The track is a
     background on the button; ::after is only ever the fill. */
  .hero-carousel__tab {
    background: linear-gradient(rgba(255,255,255,.3), rgba(255,255,255,.3))
                no-repeat bottom center / 1.25rem 2px;
  }
  .hero-carousel__tab::after {
    inset: auto auto 0 50%;
    width: 1.25rem;
    height: 2px;
    margin-inline-start: -.625rem;
    background: var(--brand-300);
  }
  .hero-carousel__tab-number { font-size: .78rem; line-height: 1; }
  .hero-carousel__tab-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
}

@media (max-width: 36rem) {
  .hero-carousel {
    min-height: max(39rem, calc(100svh - var(--header-h)));
  }
  .hero-carousel .hero__body { padding: var(--space-m) var(--space-xs) 9.5rem; }
  .hero-carousel__copy .hero__sub { font-size: var(--step-0); }
  /* Clear of the concierge toggle, which is position:fixed at bottom:--space-s and, at
     this width, stretches the full width of the screen — so it sat directly on top of
     these controls and hid them completely. Its own tap target plus both gaps is the
     height that has to be given up. */
  .hero-carousel__controls {
    bottom: calc(var(--tap-min) + var(--space-s) * 2);
  }
  .hero-carousel__controls-inner { gap: var(--space-2xs); }
  /* Down beside the tabs, not centred on the picture.

     Vertically centred is where a carousel arrow belongs when there is picture to
     the left and right of the copy. On a phone there is not: the copy runs the full
     width, so both discs sat ON the paragraph -- "partner, helping you choose the"
     ran straight through them. Moving them onto the control row puts every way of
     changing the slide in one band at the bottom, where the thumb already is.

     Back up to --tap-min as well. They had been shrunk to 2.5rem to get out of the
     way of the headline, which is 40px -- under this design system's own 44px floor,
     and set on the only control that moves the hero. */
  .hero-carousel__arrow {
    top: auto;
    translate: none;
    bottom: calc(var(--tap-min) + var(--space-s) * 2);
    width: var(--tap-min);
    height: var(--tap-min);
  }
  .hero-carousel__arrow--prev { left: var(--space-xs); }
  .hero-carousel__arrow--next { right: var(--space-xs); }
}

/* --- Short screens: give the hero back the room it does not have --------------
   The rule this hero is built on -- "keep the whole hero, headline included, on one
   screen" -- was true at 390x844 and quietly false below it. Measured: at 360x640 the
   slide controls sat 60px under the fold, and at 320x568 the paragraph was cut mid-
   sentence with the buttons 152px below it. A carousel whose controls cannot be seen is
   not a carousel.

   Keyed on HEIGHT, not width, because height is the thing that ran out. At 390x844
   nothing here applies: the hero is min-height'd to the viewport there, so trimming the
   bottom padding would only push the copy DOWN into the space it freed -- measured, it
   moved the call to action 24px lower for no gain. Below 44rem of viewport the hero is
   sized by its own content instead, and the same trim genuinely shortens it.

   The reserve is the only thing worth taking. 9.5rem of bottom padding holds the copy
   clear of the control row; the row occupies 120px from the bottom (44px of tabs, 76px
   of offset), so 8rem keeps the clearance and returns 24px.

   Not tried and rejected: tightening the kicker's tracking to --tracking-label. It made
   the line narrower and never once dropped a line -- 5 at 320, 4 at 360 and 390, before
   and after -- so it was a change to the brand's typography that bought nothing. */
@media (max-width: 36rem) and (max-height: 44rem) {
  .hero-carousel .hero__body { padding-bottom: 8rem; }
  .hero-carousel .hero__actions { margin-top: var(--space-m); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-carousel__slide,
  .hero-carousel__slide img,
  .hero-carousel__tab { transition: none; }
}

.hero__cue {
  position: absolute;
  left: 50%;
  bottom: var(--space-m);
  translate: -50% 0;
  width: var(--tap-min);
  height: var(--tap-min);
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.35);
  z-index: 2;
}
.hero__cue span {
  width: 9px; height: 9px;
  border-right: 2px solid #fff;
  border-bottom: 2px solid #fff;
  rotate: 45deg;
  translate: 0 -2px;
  animation: cue 2.2s var(--ease) infinite;
}
@keyframes cue {
  0%, 100% { translate: 0 -3px; opacity: .55; }
  50%      { translate: 0 2px;  opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__cue span,
  .phero__media img,
  .hero--home .hero__media img,
  .phero__media-line { animation: none; }
}

/* --- Router cards ------------------------------------------------------------- */
.router-section { padding-block: var(--space-3xl); }
.router__item {
  display: grid;
  grid-template-rows: 1fr auto;
  aspect-ratio: auto;
  min-height: 15rem;
}
.router__media {
  grid-area: 1 / 1 / 3 / 2;
  overflow: hidden;
}
.router__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .7s var(--ease);
}
.router__item:hover .router__media img,
.router__item:focus-visible .router__media img { transform: scale(1.07); }

.router__body {
  grid-area: 2 / 1 / 3 / 2;
  z-index: 1;
  align-self: end;
  padding: var(--space-m);
  display: grid;
  gap: 2px;
}
.router__label {
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--ink-000);
}
.router__meta {
  font-size: var(--step--1);
  color: rgba(255,255,255,.82);
}
.router__go {
  grid-area: 1 / 1 / 2 / 2;
  z-index: 1;
  justify-self: end;
  align-self: start;
  margin: var(--space-2xs);
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(255,255,255,.14);
  color: #fff;
  opacity: 0;
  translate: -6px 0;
  transition: opacity .3s var(--ease), translate .3s var(--ease), background .3s var(--ease);
}
.router__item:hover .router__go,
.router__item:focus-visible .router__go {
  opacity: 1; translate: 0 0; background: var(--brand-600);
}

/* --- Card media zoom (service cards, offering cards) --------------------------- */
.card__media { overflow: hidden; }
.card__media img { transition: transform .6s var(--ease); }
.card:hover .card__media img { transform: scale(1.06); }

@media (prefers-reduced-motion: reduce) {
  .router__media img, .card__media img, .router__go { transition: none; }
}

/* --- Fixes on top of the older component rules --------------------------------
   Both of these are consequences of changes made in this file, so they are corrected
   here rather than left for someone to find. */

/* The hero-drift animation scales the image to 1.09. Without clipping, that pushed
   the page 39px wider than the viewport and produced a horizontal scrollbar on every
   page with a full-bleed hero. */
.hero__media { overflow: hidden; }

/* The router scrim was authored as `grid-area: 1 / 1` when the card was a single-cell
   grid. The card now has two rows (media, body), so 1/1 covered only the top half —
   leaving the label sitting on bare photograph, illegible on any light image. It has
   to span both rows, and be darkest where the text actually is. */
.router__item::after {
  grid-area: 1 / 1 / 3 / 2;
  background: linear-gradient(180deg,
              rgba(0,0,0,.10) 0%,
              rgba(0,0,0,.34) 46%,
              rgba(0,0,0,.86) 100%);
}
/* Consistent card height, so a two-line label does not make one card taller than the
   rest of its row. */
.router__item { aspect-ratio: 4 / 3; min-height: 0; }
@media (min-width: 56rem) { .router__item { aspect-ratio: 5 / 4; } }

/* --- Hero text legibility ------------------------------------------------------
   Two bugs, fixed together because fixing either alone makes the other worse.

   1. The kicker was rendering WHITE, not --brand-200. base.css has
      `.hero__body :is(h1, p) { color: inherit }` at specificity (0,1,1), which
      outranks `.hero__kicker` at (0,1,0). The brand tint was never applied.

   2. Measured on the homepage: the kicker sits 45% down the hero, where the media
      scrim has only reached alpha 0.52, and white on the brightest pixel there is
      4.26:1 — under the 4.5 small text needs. Applying fix (1) alone would make it
      WORSE, because #dda2ee is lighter than white against a bright background
      (~3.2:1).

   So the scrim gets a second, text-local layer. It sits on .hero__body rather than
   on the image, so the photograph keeps its midtones everywhere the text is not. */
.hero--scrim .hero__body {
  background: linear-gradient(180deg,
              rgba(0,0,0,0) 0%,
              rgba(0,0,0,.22) 30%,
              rgba(0,0,0,.62) 100%);
}

/* Specificity high enough to beat `.hero__body :is(h1, p)`. */
.hero__body .hero__kicker,
.phero__text .phero__kicker { color: var(--brand-200); }

/* The kicker carries the page's key fact — "15 venues · 27 nights a month · 8 towns" —
   and it was set at 12.5px, smaller than the body copy under it. Raising it to --step-1
   fixes the hierarchy and settles the contrast question at the same time: at 19.2px
   minimum and weight 700 it is WCAG "large text", which needs 3:1 rather than 4.5:1.
   Measured against the actual photograph through both scrim layers: 3.71:1. */
.hero__kicker, .phero__kicker {
  font-size: var(--step-1);
  line-height: 1.2;
}
