/* =============================================================================
   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);
    /* 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.

       Not `width: auto` either, which is what replaced it. An auto-width grid item takes
       its width from its content, and until the photograph has DOWNLOADED the content has
       no width: the panel painted as a 38x74px sliver and then jumped to full size.
       Lighthouse measured that as a layout shift of 0.33 on /karaoke/ and 0.49 on
       /plan-my-event/ (0.1 is "needs improvement"). This computes the same final width
       from the ratio the template already hands over — the column's width, or the width
       at which the height cap is reached, whichever is smaller — so the box is the shape
       of the photograph before a single byte of it arrives. */
    width: min(100%, calc(min(50rem, 74vh) * var(--hero-ratio, 4 / 5)));
    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 stage darkens ONE thing now: the strip the controls sit on.

   It used to carry the copy's legibility too, as a wash from the left edge -- .72 at 0,
   .54 at 30%, gone by 74% -- on the theory that the text lives in the left half. It does
   at 1440 and not at 1024, where the headline runs to 70% of the width, and a gradient
   written in percentages of the STAGE cannot know where the text ends at a given width.
   Measured with the old full-frame layers switched off (see below): the kicker came out
   at 1.00:1 on the Weddings slide at 1280, sitting on sunlit confetti the wash had
   already given up on. So the copy's protection moved onto the copy, where it follows
   the text at every width -- .hero-carousel__copy::before -- and the stage keeps only
   what nothing else can do: the tab labels and numbers along the bottom, which are 14px
   and need 4.5:1 over whatever the photograph has at its lower edge. */
.hero-carousel__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(0deg, rgba(8,6,12,.80) 0, rgba(8,6,12,.80) 4.5rem,
                                    rgba(8,6,12,0) 10rem);
}
/* ONE scrim on the carousel, not three.

   The stage's own gradient above was written to leave "the right two-fifths of the
   picture untouched", and it never did, because two older layers sat underneath it:

     .hero--scrim .hero__media::after   (base.css)  --scrim-strong, 35% to 72% black over
                                                    the WHOLE frame of every slide -- the
                                                    full-bleed hero's scrim from before the
                                                    carousel existed, still attached because
                                                    each slide carries .hero__media.
     .hero--scrim .hero__body           (below)     0 to 62% black over the full width,
                                                    added to rescue the kicker at a time
                                                    when the photograph under it was
                                                    already 35-72% dark.

   Stacked, the right-hand face on the UV-party slide sat under roughly 60% black and the
   bottom third of every picture was close to black: a mask, not a scrim. The homepage is
   the one page where the photograph IS the message.

   Both are switched off here for the carousel only. The single-image fallback hero (no
   slides set) keeps them -- it has no stage scrim of its own and still needs one. The
   stage gradient alone now carries legibility, and its stops are set by measurement:
   see the notes on it. (0,3,x) so neither the file order nor the other file decides it. */
.hero-carousel.hero--scrim .hero__media::after { content: none; }
.hero-carousel.hero--scrim .hero__body { background: none; }

.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;
  /* Its own stacking context, so the ::before below can sit at z-index -1 -- behind the
     words, in front of the photograph -- instead of dropping behind the stage. */
  isolation: isolate;
  max-width: 44rem;
  /* Only the text shadow remains, and only as insurance: the shade below does the work,
     this covers the highlight that lands under a descender anyway. */
  text-shadow: 0 2px 18px rgba(0,0,0,.55);
}
/* The shade that makes the copy legible, drawn exactly where the copy is.

   Dark behind the words and nowhere else, so the rest of the photograph shows at its real
   brightness -- which is the point of a photographic hero, and what the three stacked
   full-width layers it replaces took away. It is not a panel: no edge, no border, no
   blur of the picture behind it. A uniform shade across the copy's own box, feathered to
   nothing over 3-4rem past it on every side by two intersected masks, so there is no line
   anywhere for the eye to find.

   Uniform on purpose. A radial falloff looks softer and puts the kicker and the buttons
   -- the corners of the block -- in its lightest part, which is exactly where the
   measured failures were.

   The strength is set against the WORST case, not the photographs that happen to be up
   today: pure white under every letter, because Craig can swap a slide in the CMS for one
   that is. The paragraph and the "Get a quote" button need 4.5:1 in white, which is .54
   against white, and .58 leaves margin over that. Below 30rem the kicker drops under the
   large-text line (18.2px, needs 4.5:1), and that is what the .68 on phones is for: over
   pure white it floors at 4.90:1. (.66 floored at 4.55 -- the shade's own #08060c
   lifts it a little -- which passes for any photograph ever uploaded, by 1%.) */
.hero-carousel__copy::before {
  content: "";
  position: absolute;
  /* The solid part reaches .75rem PAST the copy's box; only then does the feather start.
     With the fade ending exactly on the box edge, the kicker's first line sat on the last
     of it: measured, the worst pixel was the top row of that line at 1280, with the
     photograph brightening row by row above it. */
  inset: -3.75rem -4.75rem;
  z-index: -1;
  pointer-events: none;
  background: rgba(8,6,12,.58);
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 4rem, #000 calc(100% - 4rem), transparent),
    linear-gradient(to bottom, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right, transparent, #000 4rem, #000 calc(100% - 4rem), transparent),
    linear-gradient(to bottom, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
          mask-composite: intersect;
}
@media (max-width: 30rem) {
  .hero-carousel__copy::before { background: rgba(8,6,12,.68); }
}
/* --brand-100 on the carousel's kicker, not --brand-200.

   With the shade confined to the copy, the kicker is what decides how dark that shade has
   to be, and brand-200 needed it darkest: over a white slide it wants .61 to reach 3:1 as
   large text, and .71 below 30rem, where it is 18.2px and so needs 4.5:1 -- measured, it
   came out at 4.30:1 on two slides at .72. brand-100 clears the same worst case at .53
   and .64, so the shade can be LIGHTER everywhere, which is the whole point of taking the
   mask off. It is still a brand tint, and the one .section-mark already uses on dark.

   (0,3,0): `.hero__body .hero__kicker` further down this file sets brand-200 at (0,2,0),
   and an earlier attempt at this override lost to it on equal specificity without anyone
   noticing. Checked in the browser, not assumed. */
.hero-carousel .hero__body .hero__kicker { color: var(--brand-100); }
.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%;
  /* .55, not .42. The disc used to sit on a picture already 35-72% dark; on the
     unmasked photograph .42 left the white chevron at about 3:1 over a white slide, the
     bare minimum for a control. .55 is 4.6:1 in the same worst case. */
  background: rgba(8,6,12,.55);
  -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) {
  /* On a phone the control row sits higher -- clear of the fixed concierge toggle, at
     76px from the bottom -- so the band goes up with it. The copy's own shade covers
     everything above that; see .hero-carousel__copy::before. */
  .hero-carousel__stage::after {
    background: linear-gradient(0deg, rgba(8,6,12,.80) 0, rgba(8,6,12,.80) 8rem,
                                      rgba(8,6,12,0) 13rem);
  }
  .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;
}
