/* =============================================================================
   HPN components — layered on tokens.css + base.css

   This is the layer that makes the site feel finished: real hero treatment, cards
   with hierarchy, section rhythm, and the blocks Craig assembles pages from.
   ============================================================================= */

/* --- Header ------------------------------------------------------------------
   Three zones: brand, navigation, actions. Previously the inner element was a
   3-child space-between flex, which left nowhere sane to put a fourth child —
   the account control — without the nav drifting off-centre. Now the nav is
   pushed right with an auto margin and the actions cluster owns its own box. */
.site-header__inner {
  min-height: var(--header-h);
  padding-block: var(--space-2xs);
  justify-content: flex-start;
  gap: var(--space-s);
}
/* min-height, not just the SVG height: the anchor is a real tap target and was
   measuring 40px because it shrink-wrapped the 40px logo. */
.site-header__logo { flex: 0 0 auto; display: flex; align-items: center;
                     min-height: var(--tap-min); }
.site-header__logo .logo { height: 40px; width: auto; color: var(--ink-000); }
.site-nav { margin-inline-start: auto; }

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  flex: 0 0 auto;
}

/* Sticky header + keyboard focus: without this, Tab can land a link underneath the
   bar. scroll-padding on the scroll container fixes SEQUENTIAL focus scrolling,
   which scroll-margin on individual targets does not.

   var(--header-h), not the 80px that used to be hardcoded here. The bar is 5rem on
   desktop and 4rem below 75rem, and it is sticky at BOTH widths — chrome.css loads
   after this file and re-asserts position:sticky, so the comment below about it going
   static on a phone has not been true for a while. A phone was therefore reserving
   96px for a 64px bar and dropping every anchor target 32px lower than it needed to.

   scroll-behavior belongs on the same rule because it is the same journey: this is how
   a #link travels and scroll-padding is where it stops. Every in-page button on the
   site teleported — "Start the planner", "Show me what fits", "See the packages". A jump
   cut gives no sense of direction or distance, so the reader cannot tell whether the page
   moved down a little or replaced itself with a different one, and scrolls back up to
   check. Animating the journey answers both questions and costs nothing.

   Only the document scrolls smoothly. scroll-behavior is not an inherited property, so
   the concierge log still snaps to the newest message the instant it arrives, which is
   what a chat should do and not what a page should do.

   Motion-sensitive visitors are already covered: tokens.css sets `scroll-behavior: auto
   !important` under prefers-reduced-motion, and !important wins regardless of specificity
   or order. That escape hatch has been guarding nothing since it was written — until now
   there was no smooth scrolling anywhere on the site for it to switch off. */
:root {
  scroll-padding-top: calc(var(--header-h) + 1rem);
  scroll-behavior: smooth;
}

.site-nav ul { gap: var(--space-3xs); }
/* Eight items, a logo and a phone button have to share one line. At the previous
   padding they totalled 1421px inside a 1312px header and wrapped onto a second row. */
.site-nav a {
  padding-inline: var(--space-2xs);
  font-size: var(--step--1);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
}

/* The phone CTA should read as a button, not wrap into two lines. */
.site-header .btn { flex: 0 0 auto; white-space: nowrap; }

/* Below this the nav gets its own row deliberately, rather than wrapping mid-list.
   It also stops being sticky: eight visible links wrap to three rows at 375px, and a
   210px sticky header was eating 25% of the viewport on every scroll. Every service
   stays visible — the old site's answer was a "More" dropdown that hid Karaoke,
   Jukebox Bingo and Foam Parties, which is the worse trade. */
/* Same switch as the one in chrome.css — 75rem. The inline nav fits at that width because
   the secondary pages collapse into a "More" disclosure, not because the breakpoint moved.
   Change both together. */
@media (max-width: 75rem) {
  .site-header { position: static; }
  .site-header__inner { flex-wrap: wrap; row-gap: var(--space-2xs); }
  .site-nav { order: 3; width: 100%; }
  .site-nav ul { justify-content: flex-start; }
  /* The auto margin that pushes everything right lives on .site-nav, and .site-nav is
     display:none here — so the account control and the burger sat bunched against the
     logo with the right half of the bar empty. The actions cluster has to claim the free
     space itself once the nav is not there to do it. Scoped to this breakpoint: adding a
     second auto margin above it would split the free space and shift the desktop nav. */
  .site-header__actions { margin-inline-start: auto; }
}

/* --- Hero -------------------------------------------------------------------- */
/* min-height deliberately NOT set here. It was the middle of three competing
   declarations (base.css 60vh, this 66vh, hero.css 78vh) — the same cascade
   accident this file already documents for the hero paragraph. base.css holds
   the default; .hero--home sets its own in hero.css.

   Block padding scales with viewport HEIGHT. A flat 6rem top and bottom is 192px,
   which is most of the usable height on a landscape phone (390px) and pushed the
   hero buttons below the fold there. 9vh keeps the same generous feel on a desktop
   and gets out of the way when there is no room. */
.hero__body { padding-block: clamp(var(--space-l), 9vh, var(--space-3xl)); }
.hero h1 { font-size: var(--step-5); margin-bottom: var(--space-xs); }
/* 40ch measured as ~28 rendered characters per line — the display font is wide
   relative to its "0" advance, which is what ch is based on. A fixed measure keeps
   the line length inside the readable 45-75 character range. */
.hero__sub {
  font-size: var(--step-1);
  max-width: 34rem;
  color: rgba(255,255,255,.92);
  margin: 0;
}
.hero--no-media .hero__body { min-height: 46vh; padding-block: var(--space-2xl); }

/* --- Designed hero (no photograph) ------------------------------------------
   Used where Craig has no photograph good enough to go full-bleed, and where
   inventing one is not an option — outdoor movies is a service he is still adding.
   This has to look deliberate rather than like a missing image, so it is a real
   composition: a brand gradient, a soft off-centre glow standing in for stage
   lighting, and a fine diagonal rule. No raster, no bytes, sharp at any size. */
.hero--no-media {
  background:
    radial-gradient(120% 90% at 78% 8%,  rgba(190, 60, 220, .55) 0%, rgba(190, 60, 220, 0) 55%),
    radial-gradient(80% 70% at 8% 92%,   rgba(88, 40, 190, .5)  0%, rgba(88, 40, 190, 0)  60%),
    linear-gradient(160deg, var(--ink-900) 0%, var(--brand-800) 58%, var(--ink-900) 100%);
  position: relative;
  isolation: isolate;
}
.hero--no-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: repeating-linear-gradient(
    115deg, rgba(255,255,255,.05) 0 1px, transparent 1px 22px);
}

/* A stronger scrim at the bottom where the text sits, lighter at the top so the
   photograph is still visible. Contrast on the old hero failed on all four slides. */
.hero--scrim .hero__media::after { background: var(--scrim-strong); }

/* --- Section rhythm ----------------------------------------------------------
   Alternating surfaces so a long page has structure instead of one flat column. */
.section + .section { padding-top: 0; }
.section.bg-muted, .section.bg-dark, .section.bg-brand { padding-block: var(--space-2xl); }
.section.bg-muted + .section,
.section.bg-dark + .section,
.section.bg-brand + .section { padding-top: var(--space-2xl); }

.section > .container > h2 { margin-bottom: var(--space-m); }

/* --- Cards ------------------------------------------------------------------- */
.card-grid { gap: var(--space-m); }
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-1);
}
.card:hover { box-shadow: var(--shadow-3); transform: translateY(-3px); }
.card--static:hover { transform: none; box-shadow: var(--shadow-1); }
.card__media { background: var(--surface-sunken); aspect-ratio: 3 / 2; }
/* Vertical padding down a step, horizontal kept. Craig asked for shorter cards, and the
   height was going into space above and below the text rather than into the text — a
   16-card page ran to 7,285px. The side padding stays: narrowing that would push the
   summary against the card edge, which looks cramped rather than compact. */
.card__body { padding: var(--space-xs) var(--space-s); gap: var(--space-3xs); flex: 1; }
.card__title { font-size: var(--step-1); line-height: var(--leading-snug); }
.card__meta { color: var(--text-muted); font-size: var(--step--1); max-width: none; }
.card__price {
  font-family: var(--font-display);
  letter-spacing: var(--tracking-tight);
  font-size: var(--step-1);
  color: var(--brand-ink);
  margin: 0;
}
.card__more {
  margin-top: auto;
  padding-top: var(--space-2xs);
  font-weight: var(--weight-bold);
  color: var(--brand-ink);
  font-size: var(--step--1);
}
.card-grid__note { color: var(--text-muted); font-size: var(--step--1); margin-top: var(--space-m); }

/* --- Text + image ------------------------------------------------------------ */
.block--text-image {
  display: grid;
  gap: var(--space-xl);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 48rem) {
  .block--text-image { grid-template-columns: 1fr 1fr; }
  .block--text-image.is-image-left .block__media { order: -1; }
}
.block--text-image img { border-radius: var(--radius-lg); }

/* --- Stats ------------------------------------------------------------------- */
.stats {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  text-align: center;
}
.stat__value {
  font-family: var(--font-display);
  font-size: var(--step-5);
  line-height: 1;
  letter-spacing: var(--tracking-display);
  color: var(--brand-ink);
  display: block;
}
.bg-dark .stat__value, .bg-brand .stat__value { color: var(--ink-000); }
.stat__label { color: var(--text-muted); font-size: var(--step--1); }
.bg-dark .stat__label, .bg-brand .stat__label { color: rgba(255,255,255,.8); }

/* --- CTA banner -------------------------------------------------------------- */
.block--cta { text-align: center; }
.block--cta h2 { margin-bottom: var(--space-2xs); }
.block--cta p { margin-inline: auto; margin-bottom: var(--space-m); max-width: 46ch; }
.block--cta .btn { background: var(--ink-000); color: var(--brand-600); }
.block--cta .btn:hover { background: var(--brand-50); color: var(--brand-700); }

/* --- FAQ --------------------------------------------------------------------- */
.faq { display: grid; gap: var(--space-2xs); max-width: var(--width-prose); }
.faq details {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.faq summary {
  cursor: pointer;
  padding: var(--space-s);
  font-weight: var(--weight-bold);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; margin-left: auto; font-size: var(--step-2); line-height: 1; }
.faq details[open] summary::after { content: "\2212"; }
.faq__answer { padding: 0 var(--space-s) var(--space-s); }

/* --- Venue schedule ---------------------------------------------------------- */
.venue-day__heading {
  font-size: var(--step-2);
  padding-bottom: var(--space-2xs);
  border-bottom: 2px solid var(--brand-500);
  margin-bottom: var(--space-s);
}
.venue-list { grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); display: grid; }
.venue {
  background: var(--surface);
  box-shadow: var(--shadow-1);
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}
.venue__name { font-size: var(--step-1); letter-spacing: var(--tracking-tight); }
.venue__time { color: var(--brand-ink); font-weight: var(--weight-bold); }
.venue__links {
  margin-top: auto; display: flex; gap: var(--space-s); margin-top: auto; padding-top: var(--space-2xs); }
.venue__links a { font-size: var(--step--1); font-weight: var(--weight-semibold); }

/* --- Footer ------------------------------------------------------------------
   Rebuilt. The previous footer was a single undifferentiated run of every page on
   the site with the phone number set as fine print underneath — no grouping, no
   headings, no hierarchy, which is exactly why it read as unfinished. */
/* A step down all round, at Craig's request that the footer sit shorter. 64px of space
   above a list of links is generous for something people reach at the end. */
.site-footer { padding-block: var(--space-xl) var(--space-m); margin-top: 0; }
.site-footer p { color: rgba(255,255,255,.75); }

.site-footer__top {
  display: grid;
  gap: var(--space-l);
  grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
  .site-footer__top { grid-template-columns: minmax(16rem, 1fr) 2fr; }
}

.site-footer__logo .logo { height: 46px; width: auto; color: var(--ink-000); }
.site-footer__pitch {
  margin: var(--space-2xs) 0 var(--space-s);
  max-width: 34ch;
  font-size: var(--step--1);
  line-height: var(--leading-body);
}

.site-footer__cols {
  display: grid;
  gap: var(--space-m) var(--space-l);
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
.site-footer__heading {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgba(255,255,255,.55);
  margin: 0 0 var(--space-2xs);
}
.site-footer__col ul { list-style: none; margin: 0; padding: 0; }
/* min-height is the tap target, and it is the single biggest thing in this footer: seven
   links at 44px is 308px of one column. It stays 44px for FINGERS and comes down for a
   mouse — a pointer that lands where you aim it does not need the same target, and
   shrinking it for touch would trade a real usability guarantee for a shorter page. */
.site-footer__col a {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  color: rgba(255,255,255,.88);
  font-size: var(--step--1);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .site-footer__col a { min-height: 2rem; }
}
.site-footer__col a:hover { color: var(--ink-000); text-decoration: underline; }

/* Contact bar. The phone number is set at body size, not as fine print — it is the
   most valuable control on the site for this business. */
.site-footer__contact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-l);
  align-items: center;
  margin-top: var(--space-l);
  padding-top: var(--space-s);
  border-top: 1px solid rgba(255,255,255,.12);
}
.site-footer__contact a,
.site-footer__place {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-height: var(--tap-min);
  color: rgba(255,255,255,.92);
  font-size: var(--step-0);
  text-decoration: none;
}
.site-footer__contact a:hover { color: var(--ink-000); text-decoration: underline; }
.site-footer__place { color: rgba(255,255,255,.7); }
.site-footer__contact .icon { flex: 0 0 auto; opacity: .7; }

.site-footer__legal {
  margin-top: var(--space-m);
  font-size: var(--step--1);
}
.site-footer__legal p { max-width: none; color: rgba(255,255,255,.55); }

/* --- Social buttons ----------------------------------------------------------
   Three, because HPN has three accounts. Icon-only, so the accessible name comes
   from aria-label on the anchor (WCAG 4.1.2) and the SVG is aria-hidden. */
.social { display: flex; gap: var(--space-2xs); list-style: none; margin: 0; padding: 0; }
.social__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-md);
  border: 1px solid rgba(255,255,255,.18);
  color: rgba(255,255,255,.85);
  background: rgba(255,255,255,.04);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.social__btn:hover {
  background: var(--brand-600);
  border-color: var(--brand-ink);
  color: var(--ink-000);
}
/* In Windows High Contrast the background and border are dropped, so the button
   would dissolve into the page. Force a system-colour border. */
@media (forced-colors: active) {
  .social__btn { border-color: CanvasText; }
}

/* --- Divider ----------------------------------------------------------------- */
.divider { border: 0; border-top: 1px solid var(--border); }
.divider--tight { margin-block: var(--space-s); }
.divider--normal { margin-block: var(--space-l); }
.divider--generous { margin-block: var(--space-2xl); }

/* --- Prose ------------------------------------------------------------------- */
.prose h2 { margin-top: var(--space-l); }
.prose h3 { margin-top: var(--space-m); font-size: var(--step-2); }
.prose ul { padding-left: 1.2em; }
.prose li { margin-bottom: var(--space-2xs); }
/* `em` is emphasis, so it must not read as de-emphasis. base.css sets it upright and
   semibold (Montserrat has no italic file); greying it as well fought that. */
.prose em { color: inherit; }

/* --- Refinements ------------------------------------------------------------- */

/* Six services divide evenly into three columns; auto-fit was leaving two orphans. */
@media (min-width: 56rem) {
  .card-grid { grid-template-columns: repeat(3, 1fr); }
  /* Event-type pages list 2-4 services. Four in a three-column grid leaves a lone card
     stranded on the second row, which reads as a missing item rather than a layout. */
  .card-grid:has(> li:nth-child(2):last-child),
  .card-grid:has(> li:nth-child(4):last-child) { grid-template-columns: repeat(2, 1fr); }
}

/* A centred heading needs its supporting paragraph centred too, or the block reads
   as two unrelated pieces. */
.align-center + .section .prose,
.section:has(> .container > .align-center) + .section .prose { text-align: center; }
.section .prose > p:only-child { margin-inline: auto; }

/* Intro paragraph directly after a centred heading */
.lede {
  text-align: center; max-width: 52ch; margin-inline: auto;
  font-size: var(--step-1);
  /* Medium, not regular: at 22.7px a 400 intro reads thin against the headline above
     it. This is also the only thing that made the downloaded Montserrat 500 earn its
     place — nothing referenced it before. */
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
}

/* The old .site-footer .site-nav overrides lived here. They are gone with the markup
   they targeted — the footer is now grouped columns, not one flat nav list. The 44px
   floor they existed to enforce is built into .site-footer__col a above. */

/* Hero on a service page can be a little shorter than the homepage */
.hero--service .hero__body { min-height: 46vh; }

/* --- Event-type router --------------------------------------------------------
   The primary journey. Big, obvious targets — this is the first decision the
   visitor makes, and the positioning depends on it being unmissable. */
.router__heading { text-align: center; }
/* Six event types divide evenly into three columns. auto-fit left an orphan on
   the second row, which made the primary journey look unfinished. */
.router {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: 1fr;
  list-style: none;
  margin: var(--space-l) 0 0;
  padding: 0;
}
@media (min-width: 36rem) { .router { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 56rem) { .router { grid-template-columns: repeat(3, 1fr); } }
.router__item {
  position: relative;
  display: grid;
  isolation: isolate;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-lg);
  overflow: hidden;
  text-decoration: none;
  background: var(--ink-800);
  box-shadow: var(--shadow-2);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.router__item:hover { transform: translateY(-3px); box-shadow: var(--shadow-3); }
.router__item img {
  grid-area: 1 / 1;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* Router cards with no photograph get the same designed treatment as the hero, so a
   card without an image reads as a deliberate tile rather than a broken one. */
.router__item:not(:has(img)) {
  background:
    radial-gradient(110% 90% at 80% 10%, rgba(190, 60, 220, .6) 0%, rgba(190, 60, 220, 0) 58%),
    linear-gradient(155deg, var(--ink-900) 0%, var(--brand-800) 70%, var(--ink-900) 100%);
}
.router__item:not(:has(img))::after { background: none; }

/* Scrim so the label stays readable over any photograph — the failure mode that
   broke the old site's hero on all four slides. */
.router__item::after {
  content: "";
  grid-area: 1 / 1;
  background: linear-gradient(180deg, rgba(0,0,0,.15) 30%, rgba(0,0,0,.78) 100%);
}
.router__label {
  grid-area: 1 / 1;
  z-index: 1;
  align-self: end;
  padding: var(--space-m);
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: var(--leading-tight);
  color: var(--ink-000);
}


/* The .account <details> disclosure that lived here is gone. It was replaced by a
   real modal (partials/auth-modal.html + css/chrome.css): the client asked for a
   modal, and a dialog also solves the mobile case a dropdown never did — a panel
   anchored to a 34px button at the top of a phone screen. */

/* =============================================================================
   Conversion-page polish

   These rules give every StreamField block the same editorial rhythm, icon language,
   image treatment and interaction feedback. The motion layer remains progressive:
   content is readable without JavaScript and all transitions collapse for people who
   request reduced motion.
   ============================================================================= */

main { overflow: clip; }

.section-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-bottom: var(--space-xs);
  color: var(--brand-ink);
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  line-height: var(--leading-snug);
  text-transform: uppercase;
}
.section-mark::before {
  content: "";
  width: 30px;
  height: 2px;
  flex: 0 0 auto;
  background: linear-gradient(90deg, var(--brand-400), var(--brand-600));
}
.bg-brand .section-mark,
.bg-dark .section-mark { color: var(--brand-100); }
.bg-brand .section-mark::before,
.bg-dark .section-mark::before { background: var(--brand-200); }

.section-heading {
  max-width: 48rem;
  margin-bottom: var(--space-xl);
}
.section-heading h2 { margin-bottom: var(--space-xs); }
.section-heading p {
  max-width: 58ch;
  color: var(--text-muted);
  font-size: var(--step-1);
  line-height: var(--leading-snug);
}

.section--heading {
  padding-block: var(--space-2xl) var(--space-xs);
  background: var(--surface);
}
.section--heading :is(h2, h3) { margin: 0; }
.section--heading + .prose-section { padding-top: var(--space-s); }
.prose-section .prose { font-size: var(--step-0); }
.page-service .phero + .prose-section .prose,
.page-standard .phero + .prose-section .prose {
  max-width: 49rem;
  font-size: var(--step-1);
  line-height: var(--leading-relaxed);
}
.prose-section .prose p:last-child { margin-bottom: 0; }
.prose-section .prose li::marker { color: var(--brand-ink); }

/* Cards carry a subtle brand edge and a clear interactive state without becoming
   glossy dashboard tiles. Static catalog cards share the surface but do not pretend
   to be clickable. */
.card {
  position: relative;
  border-color: color-mix(in srgb, var(--border) 78%, var(--brand-200));
  box-shadow: 0 1px 0 rgba(141,11,171,.05), var(--shadow-1);
}
.card:not(.card--static):hover,
.card:not(.card--static):focus-within {
  border-color: var(--brand-300);
  transform: translateY(-5px);
}
.card__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
}
.card__more .icon { transition: transform .25s var(--ease); }
.card:hover .card__more .icon,
.card:focus-within .card__more .icon { transform: translateX(4px); }

.offering-section {
  position: relative;
  background:
    radial-gradient(55rem 28rem at 90% 0%, rgba(221,162,238,.22), transparent 66%),
    var(--surface-muted);
}
.offering-card {
  overflow: visible;
  background: var(--surface-veil);
  border-radius: var(--radius-lg);
}
.offering-card::before {
  content: "";
  position: absolute;
  inset: -1px auto -1px -1px;
  width: 4px;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  background: linear-gradient(180deg, var(--brand-400), var(--brand-700));
}
.offering-card .card__body { padding: var(--space-s) var(--space-m); }

/* Every offering card has a media area so a row reads as one row. overflow is visible
   on .offering-card so its brand spine can sit on the edge, which means the media has
   to clip itself. */
.offering-card--media { overflow: hidden; }
.offering-card--media .card__media {
  aspect-ratio: 4 / 3;
  background: var(--surface-sunken);
}
.offering-card--media .card__media img { width: 100%; height: 100%; object-fit: cover; }
.offering-card--media .card__body { padding-top: var(--space-s); }

/* No photograph: a brand panel carrying the item's icon, rather than an invented stock
   photo. HPN has no picture of the photo booth, the audio guest book or the LED sticks
   — the only LED image the old site had was an eBay listing shot.

   Note the selector. This used to be a bare `.card__media--icon`, which loses to
   `.offering-card--media .card__media` above it on specificity (0,1,0 against 0,2,0), so
   the gradient never painted and every image-less card was a flat grey box with a small
   tile floating in it. It read as a broken image rather than a designed panel — which is
   exactly what it was being reported as. Matching the specificity fixes it. */
.offering-card--media .card__media--icon {
  display: grid;
  place-items: center;
  background:
    radial-gradient(130% 100% at 50% -10%, rgba(255,255,255,.85), rgba(255,255,255,0) 62%),
    linear-gradient(145deg, var(--brand-50), var(--brand-200));
}
.card__media--icon .card__icon {
  width: 92px;
  height: 92px;
  margin: 0;
  border-radius: 26px;
  background: var(--surface-veil-strong);
  box-shadow: var(--shadow-2);
}
/* The glyph scales with the tile — a 24px icon inside a 92px tile is the small-lost-mark
   look this panel is replacing. */
.card__media--icon .card__icon .icon { width: 42px; height: 42px; }
.offering-card:hover .card__media--icon .card__icon { transform: translateY(-3px) rotate(-3deg); }
.card__icon {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  margin-bottom: var(--space-xs);
  border-radius: 14px;
  color: var(--brand-ink-strong);
  background: linear-gradient(145deg, var(--brand-50), var(--brand-100));
  transition: transform .3s var(--ease), background .3s var(--ease);
}
.offering-card:hover .card__icon {
  transform: translateY(-3px) rotate(-3deg);
  background: var(--brand-wash-strong);
}
.offering-card .card__title { margin-top: 0; }

.service-match-section {
  background:
    linear-gradient(180deg, var(--surface) 0%, var(--surface-muted) 100%);
}
.service-match-section .card__media { position: relative; }
.service-match-section .card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 55%, rgba(20,10,24,.24));
  pointer-events: none;
}

/* Text-and-image blocks look composed, not like a paragraph beside a pasted JPEG. */
.text-image-section .block__media {
  position: relative;
  isolation: isolate;
}
.text-image-section .block__media::before {
  content: "";
  position: absolute;
  inset: var(--space-m) calc(-1 * var(--space-m)) calc(-1 * var(--space-m)) var(--space-m);
  z-index: -1;
  border-radius: var(--radius-lg);
  background: linear-gradient(145deg, var(--brand-100), rgba(141,11,171,.08));
}
.block--text-image img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  box-shadow: var(--shadow-3);
}

.stats-section .stats { gap: var(--space-s); }
.stats-section .stat {
  padding: var(--space-l) var(--space-s);
  border: 1px solid color-mix(in srgb, var(--border) 76%, var(--brand-200));
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
.stats-section.bg-muted .stat { background: var(--surface-veil-strong); }

.faq-section .faq { max-width: 52rem; }
.faq details {
  overflow: hidden;
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.faq details:hover,
.faq details[open] { border-color: var(--brand-300); box-shadow: var(--shadow-2); }
.faq summary::after {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--brand-ink-strong);
  background: var(--brand-wash);
}

/* Schedules are a major landing-page destination. A compact day rail and semantic
   icon rows make the list scannable before the visitor commits to reading addresses. */
.schedule-section {
  background:
    radial-gradient(60rem 34rem at 100% 0%, rgba(200,111,227,.13), transparent 70%),
    var(--surface-muted);
}
.schedule-intro { max-width: 50rem; }
.schedule-intro h2 { margin-bottom: var(--space-xs); }
.schedule-intro p {
  max-width: 56ch;
  color: var(--text-muted);
  font-size: var(--step-1);
}
.day-nav {
  display: flex;
  gap: var(--space-2xs);
  margin: var(--space-l) 0 var(--space-xl);
  padding: var(--space-3xs);
  overflow-x: auto;
  scrollbar-width: thin;
}
.day-nav a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: var(--space-2xs) var(--space-s);
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  font-size: var(--step--1);
  font-weight: var(--weight-bold);
  text-decoration: none;
  box-shadow: var(--shadow-1);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
              border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.day-nav a:hover,
.day-nav a:focus-visible {
  color: var(--ink-000);
  background: var(--brand-600);
  border-color: var(--brand-ink);
  transform: translateY(-2px);
}
.schedule-filters:not(:empty) { margin-bottom: var(--space-xl); }
.venue-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: var(--space-s);
  padding: var(--space-m);
  border: 1px solid color-mix(in srgb, var(--border) 76%, var(--brand-200));
  border-radius: var(--radius-lg);
  background: var(--surface-veil);
  box-shadow: var(--shadow-1);
}
.venue-filter__controls {
  display: flex;
  flex: 1 1 28rem;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--space-s);
}
.venue-filter label {
  display: grid;
  flex: 1 1 12rem;
  gap: var(--space-3xs);
  color: var(--text-muted);
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.venue-filter select {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2xs) 2.5rem var(--space-2xs) var(--space-xs);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-md);
  color: var(--text);
  background: var(--surface);
  font: inherit;
  font-size: 1rem;
  font-weight: var(--weight-semibold);
  letter-spacing: normal;
  text-transform: none;
}
.venue-filter__clear {
  min-height: var(--tap-min);
  padding: var(--space-2xs) var(--space-s);
  border: 1px solid var(--brand-300);
  border-radius: var(--radius-pill);
  color: var(--brand-ink-strong);
  background: var(--brand-wash);
  font: inherit;
  font-size: var(--step--1);
  font-weight: var(--weight-bold);
  cursor: pointer;
}
.venue-filter__count {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
}
.venue-day { scroll-margin-top: 6rem; margin-block: var(--space-xl); }
.venue-day__heading {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  border: 0;
  padding: 0;
}
.venue-day__heading::after {
  content: "";
  height: 2px;
  flex: 1;
  background: linear-gradient(90deg, var(--brand-400), transparent);
}
/* align-items: start, so a card is only as tall as its own content. Stretching every
   card to match the tallest left a void under the ones with no photograph — 10 of the
   15 venues have none yet, so the ragged column was the common case, not the exception.
   The links still sit at the foot of each card via margin-top: auto. */
.venue-list { gap: var(--space-m); align-items: start; }
.venue {
  position: relative;
  min-height: 14rem;
  padding: var(--space-s);
  border-radius: var(--radius-lg);
  border-color: color-mix(in srgb, var(--border) 74%, var(--brand-200));
  overflow: hidden;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.venue::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: linear-gradient(90deg, var(--brand-400), var(--brand-700));
}
.venue:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-3);
  border-color: var(--brand-300);
}
.venue__topline {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2xs);
  margin-bottom: var(--space-xs);
}
.venue__service,
.venue__time {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--brand-ink-strong);
  font-size: var(--step--2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.venue__service .icon,
.venue__time .icon { width: 17px; height: 17px; }
.venue__name { font-size: var(--step-1); margin-bottom: var(--space-3xs); }
.venue__cadence {
  margin: 0;
  color: var(--brand-ink-strong);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
}
.venue__address {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3xs);
  margin-top: var(--space-2xs);
}
.venue__address .icon { width: 17px; height: 17px; flex: 0 0 auto; margin-top: 3px; color: var(--brand-ink); }
.venue__links { gap: var(--space-2xs); flex-wrap: wrap; }
.venue__links a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-height: 48px;
  padding: var(--space-2xs) var(--space-xs);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--brand-ink-strong);
  background: var(--surface);
  text-decoration: none;
}
.venue__links a:hover { border-color: var(--brand-300); background: var(--brand-wash); }
.venue__links .icon { width: 17px; height: 17px; }

.cta-section {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(44rem 20rem at 15% -10%, rgba(255,255,255,.19), transparent 65%),
    radial-gradient(34rem 18rem at 90% 120%, rgba(51,3,61,.48), transparent 72%),
    linear-gradient(135deg, var(--brand-500), var(--brand-700));
}
.cta-section::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: .28;
  pointer-events: none;
  background-image: repeating-linear-gradient(115deg, rgba(255,255,255,.12) 0 1px,
                                              transparent 1px 30px);
}
.cta-section .block--cta { position: relative; z-index: 1; }
.cta-section .btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  box-shadow: 0 10px 30px rgba(37,0,48,.28);
}
.cta-section .btn .icon { transition: transform .25s var(--ease); }
.cta-section .btn:hover .icon,
.cta-section .btn:focus-visible .icon { transform: translateX(4px); }

/* .gallery-grid's layout is declared in the Gallery section lower in this file. It
   was ALSO declared here, and the later rule won — so editing this one changed
   nothing, which is a trap for whoever tries. Kept as a signpost, not a rule. */
.gallery-grid li {
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}
.gallery-grid .card__media {
  width: 100%;
  height: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform .7s var(--ease);
}
.gallery-grid li:hover .card__media { transform: scale(1.045); }

.columns-grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; }
@media (min-width: 48rem) {
  .columns-grid--2 { grid-template-columns: repeat(2, 1fr); }
  .columns-grid--3 { grid-template-columns: repeat(3, 1fr); }
}
.columns-grid__item {
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
.columns-grid__item > .section { padding: var(--space-l); }
.columns-grid__item .container { width: 100%; }
.columns-grid__media img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }

.quote {
  position: relative;
  max-width: 50rem;
  margin: 0;
  padding: var(--space-xl);
  border-radius: var(--radius-lg);
  color: var(--ink-000);
  background: linear-gradient(145deg, var(--ink-900), var(--brand-800));
  box-shadow: var(--shadow-3);
}
.quote > span {
  position: absolute;
  top: var(--space-s);
  right: var(--space-m);
  color: rgba(255,255,255,.12);
  font-family: Georgia, serif;
  font-size: 7rem;
  line-height: 1;
}
.quote p { position: relative; font-size: var(--step-2); line-height: var(--leading-snug); }
.quote cite { color: var(--brand-100); font-size: var(--step--1); font-style: normal; }
.embed-section iframe { width: 100%; border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-3); }

/* The three footer contacts stack gracefully on a phone; previously the location was
   visibly clipped at the right edge even though the document itself did not overflow. */
.site-footer__contact > * {
  min-width: 0;
  flex: 1 1 13rem;
  overflow-wrap: anywhere;
}

@media (max-width: 40rem) {
  .section { padding-block: var(--space-xl); }
  .section--heading { padding-block: var(--space-xl) var(--space-2xs); }
  .section-heading { margin-bottom: var(--space-l); }
  .offering-card .card__body { padding: var(--space-s) var(--space-m); }
  .text-image-section .block__media::before { inset: var(--space-s) calc(-1 * var(--space-2xs)) calc(-1 * var(--space-2xs)) var(--space-s); }
  .stats-section .stats { grid-template-columns: 1fr; }
  .venue { min-height: 0; }
  .venue__topline { align-items: flex-start; flex-direction: column; }
  .quote { padding: var(--space-l); }
}

@media (prefers-reduced-motion: reduce) {
  .card, .card__more .icon, .card__icon, .day-nav a, .venue,
  .cta-section .btn .icon, .gallery-grid .card__media { transition: none; }
  .card:not(.card--static):hover, .day-nav a:hover, .venue:hover { transform: none; }
}

/* --- Reviews ------------------------------------------------------------------
   The site shipped with no social proof at all. Cards rather than a carousel: three
   reviews fit on one row on a desktop and stack on a phone, and nobody has to wait for
   a slide to rotate to read the second one. */
.testimonials-section { padding-block: var(--space-3xl); }
.testimonials__head { max-width: var(--width-prose); margin-bottom: var(--space-xl); }
.testimonials__heading { margin-bottom: var(--space-2xs); }
.testimonials__intro { color: var(--text-muted); margin: 0; }
.bg-dark .testimonials__intro, .bg-brand .testimonials__intro { color: rgba(255,255,255,.82); }

.testimonials {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}
.testimonial {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-s);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}
.bg-muted .testimonial { background: var(--surface); }
.bg-dark .testimonial, .bg-brand .testimonial {
  background: rgba(255,255,255,.06);
  border-color: rgba(255,255,255,.14);
  box-shadow: none;
}

/* Gold, not brand purple: a star rating that is not the colour people expect reads as
   decoration. 4.6:1 on white. */
/* 96px at a 192px source, so it stays sharp on a 2x display. The reviewer's name is the
   accessible label, so the image itself carries an empty alt rather than repeating it.
   text-align rather than align-items: the card is a flex column, and centring the items
   would shrink the quote to its own content width instead of filling the card. */
.testimonial { text-align: center; }
.testimonial__avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  margin-inline: auto;
  margin-bottom: var(--space-2xs);
}
.bg-dark .testimonial__avatar, .bg-brand .testimonial__avatar {
  box-shadow: 0 0 0 2px rgba(255,255,255,.18);
}

.testimonial__stars { margin: 0; color: var(--star); letter-spacing: .12em; font-size: var(--step-0); }
.bg-dark .testimonial__stars, .bg-brand .testimonial__stars { color: var(--star-on-dark); }

.testimonial__quote { margin: 0; flex: 1; }
.testimonial__quote p { margin: 0; max-width: none; }

.testimonial__by {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-top: var(--space-2xs);
  padding-top: var(--space-2xs);
  border-top: 1px solid var(--border);
}
.bg-dark .testimonial__by, .bg-brand .testimonial__by { border-top-color: rgba(255,255,255,.14); }
.testimonial__name { font-style: normal; font-weight: var(--weight-bold); }
.testimonial__meta { color: var(--text-muted); font-size: var(--step--1); }
.bg-dark .testimonial__meta, .bg-brand .testimonial__meta { color: rgba(255,255,255,.7); }

/* Circular portrait beside copy. Square rendition, clipped to a circle, capped so it
   never grows past the size its grading allows — "Bride and groom" is graded card-only
   (567px originally, upscaled x1.60), so it must not be asked to fill a wide box. */
.block__media--circle {
  justify-self: center;
  width: min(100%, 24rem);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
}
.block__media--circle img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }

/* --- Reviews over a photograph -------------------------------------------------
   The old /parties-%26-events ran its REVIEWS section over a venue photo. Repeating
   that here needs the thing that site never had: a scrim. Its headline contrast over
   photographs measured 2.65:1 at worst against a 3.0:1 requirement, because the text
   sat directly on the picture. */
.testimonials-section--media { position: relative; isolation: isolate; }
/* Three background layers on ONE element, first listed painting on top:
     1. a flat wash   2. --scrim-strong   3. the photograph
   These were briefly split across this element and a ::before, both at z-index -1 —
   which does not work: this element is a real child, so it painted OVER the ::before
   and the flat wash never appeared at all. Changing its opacity had literally no
   effect on the rendered page.

   The flat wash is needed because --scrim-strong is weakest at the TOP (0.35), which
   is exactly where the heading and the intro sit. Measured on the rendered page at
   0.55: the intro line hit 3.30:1 against the brightest part of the photograph, under
   the 4.5:1 AA requires for normal text. The heading passes either way, being large
   text at 3.0:1.

   Set to 0.74 rather than the 0.72 that only just passed (4.57:1 against a 4.5
   requirement). Craig picks this photograph from the CMS, so the scrim has to hold for
   a brighter one than the one measured, not only for this one. */
.testimonials__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(rgba(10, 4, 14, .74), rgba(10, 4, 14, .74)),
    var(--scrim-strong),
    /* `none` fallback, not for tidiness: without it an unset --testimonials-bg makes the
       WHOLE declaration invalid, and the two gradients above are the contrast scrim that
       keeps the quotes readable over Craig's photograph. The scrim must not be able to
       disappear because the photograph did. */
    var(--testimonials-bg, none);
  background-size: cover, cover, cover;
  background-position: center, center, center;
  background-repeat: no-repeat, no-repeat, no-repeat;
}
.testimonials-section--media .testimonials__heading { color: var(--ink-000); }
/* Pure white, not 85%. Muting a line that is already fighting a photograph is the
   wrong lever — it lowers the very contrast the scrim exists to protect, and it made
   the measured figure optimistic, since the measurement assumed pure white. */
.testimonials-section--media .testimonials__intro { color: var(--ink-000); }
/* Cards sit on their own solid surface rather than a translucent one — a card that lets
   the photograph through is where review text stops being readable. */
.testimonials-section--media .testimonial {
  background: rgba(22, 22, 22, .92);
  border-color: rgba(255,255,255,.14);
  box-shadow: var(--shadow-3);
}
.testimonials-section--media .testimonial__quote,
.testimonials-section--media .testimonial__name { color: var(--ink-050); }
.testimonials-section--media .testimonial__meta { color: rgba(255,255,255,.72); }
.testimonials-section--media .testimonial__stars { color: #f0c040; }
.testimonials-section--media .testimonial__by { border-top-color: rgba(255,255,255,.16); }

/* --- Venue map (Milestone 3) --------------------------------------------------
   Leaflet with OpenStreetMap tiles. Leaflet ships its own CSS, bundled with the
   island; these rules only size and frame the canvas. */
.schedule-map { margin-block: var(--space-l); }
.venue-map__canvas {
  height: clamp(16rem, 42vw, 26rem);
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-1);
  /* Leaflet panes stack at z-index 400-700 and would otherwise sit above the sticky
     header and the auth modal's backdrop. */
  z-index: 0;
  isolation: isolate;
}
/* The map is raster tiles from OpenStreetMap and stays light in both themes: there is
   no dark tile set behind this, and inverting a map turns roads into rivers. Dimmed a
   little under the dark theme so a 1088x416 sheet of near-white is not the brightest
   thing on the page — enough to sit in the design, not enough to stop it being a map.
   Markers are excluded, or the pins would be dimmed along with the streets. */
/* Leaflet paints its container #ddd behind the tiles, which shows while they load and
   in the gaps at the edges of the world. Left alone it is a white flash on a dark page
   every time the map moves. */
:root[data-theme="dark"] .leaflet-container { background: var(--surface-sunken); }

:root[data-theme="dark"] .venue-map__canvas .leaflet-tile-pane {
  filter: brightness(.82) saturate(.85);
}

/* OpenStreetMap requires the attribution and Leaflet paints it in #0078A8 on its own
   near-white strip: 3.64:1, an AA failure at this size, and it was failing that way in
   BOTH themes long before there was a dark one — the theme audit is simply the first
   thing that looked. Darkened to the brand ink, which is a real link colour and passes.
   Given its own opaque surface rather than left on the tiles, so the ratio does not
   depend on what is underneath it.

   Leaflet ships its own stylesheet and the island injects it at runtime, so it lands
   AFTER this file and its `.leaflet-container a` wins on order at equal specificity.
   Hence the extra class rather than !important: 0-2-1 beats 0-1-1 on the cascade's own
   terms, and stays beatable by anything more specific later. */
.leaflet-container .leaflet-control-attribution {
  /* An OPAQUE surface of our own, rather than Leaflet's translucent white strip.
     Translucent means the contrast underneath it depends on which streets happen to be
     behind it, which is not something that can be measured once and relied on — and it
     went wrong the moment the container behind it was themed. A solid token is the same
     ratio over every tile, in both themes. */
  background: var(--surface-raised);
  color: var(--text-muted);
}
.leaflet-container .leaflet-control-attribution a { color: var(--brand-ink); }
.venue-map__note {
  margin: var(--space-2xs) 0 0;
  font-size: var(--step--1);
  color: var(--text-muted);
}
.map-popup__nights { color: var(--text-muted); }

/* --- Venue photo --------------------------------------------------------------- */
.venue__photo {
  margin: 0 0 var(--space-2xs);
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 2 / 1;
  background: var(--surface-sunken);
}
.venue__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The filter now has three selects; let them wrap rather than squeeze. */
.venue-filter__controls { flex-wrap: wrap; }

/* Map pin, drawn in CSS. See venue-map.jsx for why this is not a Leaflet default icon.

   The TARGET is 28x28, not the 22x22 it was. WCAG 2.2 SC 2.5.8 sets a 24x24 minimum for
   a pointer target, and these are the only way to reach a venue's popup — there is no
   keyboard-equivalent list inside the map. The visible dot stays 18px; the extra size is
   transparent padding around it, so the map does not get heavier-looking to buy the
   tap area. Keep this in step with iconSize/iconAnchor in venue-map.jsx. */
.venue-pin { background: none; border: 0; }
.venue-pin__dot {
  display: block;
  width: 18px; height: 18px;
  margin: 5px;
  border-radius: 50%;
  background: var(--brand-500);
  border: 3px solid #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,.45);
}
.leaflet-marker-icon:hover .venue-pin__dot { background: var(--brand-700); }
@media (forced-colors: active) { .venue-pin__dot { border-color: CanvasText; } }

.venue-filter__auto { color: var(--text-muted); }

/* Shown when a filter combination matches nothing. Without it the page just went blank
   below the controls, which reads as broken rather than as an answer. */
.venue-filter__empty {
  margin-top: var(--space-xs);
  padding: var(--space-s) var(--space-m);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-muted);
  color: var(--text);
}
.venue-filter__empty .venue-filter__clear { margin-left: .25rem; }

/* The little outbound arrow on links that open a new tab. aria-hidden in the markup —
   the "opens in a new tab" meaning is carried by the visually-hidden text, not by this. */
.venue__external { opacity: .6; font-size: .85em; }

/* Venue cards are more compact on a phone. The photograph earns its place — it is how
   someone recognises the bar — but at 2:1 across 13 cards it was a large part of why
   the page ran to 12,000px. */
@media (max-width: 48rem) {
  .venue__photo { aspect-ratio: 3 / 1; }
  .venue { gap: var(--space-3xs); }
}

/* --- Gallery ------------------------------------------------------------------
   Used for the social grid on /karaoke. Square tiles by default there: the posts are
   promotional graphics whose content IS text, so a landscape crop deletes the message. */
.gallery-section { padding-block: var(--space-3xl); }
.gallery__head { max-width: var(--width-prose); margin-bottom: var(--space-l); }
.gallery__intro { color: var(--text-muted); margin: 0; }
.gallery-grid {
  display: grid;
  gap: var(--space-s);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 48rem) {
  .gallery-grid { grid-template-columns: repeat(var(--gallery-columns, 3), 1fr); }
}
.gallery-grid li {
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  box-shadow: var(--shadow-1);
}
.gallery-grid img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-grid--square li { aspect-ratio: 1; }
.gallery-grid--portrait li { aspect-ratio: 4 / 5; }
.gallery-grid--landscape li { aspect-ratio: 3 / 2; }

/* "Natural" keeps each image's own shape, so a set of mixed square, portrait and
   landscape social posts is not cropped to a common rectangle. Multi-column rather than
   grid: this is the one layout CSS gives us where tiles of different heights pack
   without leaving a row-height gap under the short ones. */
.gallery-grid--natural {
  display: block;
  columns: var(--gallery-columns, 3);
  column-gap: var(--space-s);
}
.gallery-grid--natural li {
  aspect-ratio: auto;
  break-inside: avoid;
  margin-bottom: var(--space-s);
}
.gallery-grid--natural img { height: auto; }
@media (max-width: 48rem) { .gallery-grid--natural { columns: 2; } }
.gallery__link { margin-top: var(--space-m); font-weight: var(--weight-bold); }

/* The button a text+image section can now carry. */
.block__action { margin: var(--space-m) 0 0; max-width: none; }

/* --- Plan My Event funnel (Milestone 4) --------------------------------------
   One question per screen. The panel holds a min-height so answering a question does
   not make the page jump under the Continue button — the commonest way a multi-step
   form loses somebody is by moving the target they were reaching for. */
/* NO padding-block rule here, deliberately, so `.section` governs it: 64px, dropping to
   48px under 40rem like every other section on the site.

   It used to say `padding-block: var(--space-3xl)` — 96px, 1.5x the site's own desktop
   section padding. Worse on a phone: `.section` drops to --space-xl (48px) inside the
   max-width:40rem block earlier in this file, but `.funnel-section` matched at the SAME
   specificity and sat later in the stylesheet, so it won inside the media query too and a
   390px screen got 96px of blank above the form and another 96px below it.

   Overriding it again, even to a smaller value, would keep that bug alive — the fix is to
   stop overriding. The class stays because the template uses it as a hook. */
/* The heading block is capped to the CARD's width, not the prose measure, and centred
   with it — the two have to share a left and right edge or the eyebrow and H2 float off
   the panel they introduce.
   Left-anchoring a measure-capped block inside the centred container is this stylesheet's
   convention for PROSE (base.css:68 does it for every <p>), and that convention is right
   for a line of text. The funnel is not a line of text: it is a bordered, shadowed
   surface, and at 1920 it sat with 0px to its left and 352px to its right, which reads as
   a panel that slipped rather than a column that was set. */
.funnel__head {
  max-width: 46rem;
  margin-inline: auto;
  margin-bottom: var(--space-l);
}
.funnel__intro { color: var(--text-muted); margin: 0; }
.funnel__loading, .funnel__fallback { color: var(--text-muted); }

.funnel {
  margin-inline: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: var(--space-l);
  max-width: 46rem;
}
.funnel__panel { min-height: 16rem; }
/* min-inline-size, because a <fieldset> defaults to min-content and will not shrink to
   its grid/flex parent. Without it the contact fieldset measured 52px WIDER than the card
   it sits in and hung outside its right edge, and between 640px and 767px the third
   column of each row was cut off with no way to scroll to it. forms.css:39 already had
   this fix; the funnel's own reset did not. */
.funnel fieldset { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.funnel__q {
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: var(--leading-snug);
  margin: 0 0 var(--space-xs);
  padding: 0;
}
/* The legend takes focus on each step so the new question is announced. It is focused
   programmatically, never by tabbing, so the ring would be confusing on a mouse click —
   but it must still show for keyboard users, which :focus-visible gets right. */
.funnel__q:focus { outline: none; }
.funnel__q:focus-visible { outline: 3px solid var(--brand-500); outline-offset: 3px; }
.funnel__hint { color: var(--text-muted); font-size: var(--step--1); margin: var(--space-2xs) 0 0; }
/* The plan could not be worked out. Not muted: this one is asking to be read, and it
   carries the way out — a Try again button and permission to send the enquiry anyway.
   Before it existed a failed request rendered as "Working out the best fit…" for ever. */
.funnel__hint--error { color: var(--text); background: var(--surface-muted); border-left: 3px solid var(--danger); padding: var(--space-2xs) var(--space-xs); }
.funnel__hint--error .btn { margin: 0 .15rem; }

.funnel__progress {
  display: flex; flex-wrap: wrap; gap: var(--space-3xs);
  list-style: none; margin: 0 0 var(--space-m); padding: 0;
  font-size: var(--step--2); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-muted);
}
.funnel__pip {
  padding: .3rem .6rem; border-radius: 99px;
  border: 1px solid var(--border); background: var(--surface-muted);
}
.funnel__pip.is-done { color: var(--brand-ink-strong); border-color: var(--brand-200); background: var(--brand-wash-strong); }
.funnel__pip.is-current { color: var(--text-on-brand); background: var(--brand-600); border-color: var(--brand-ink); }

.funnel__choices { display: grid; gap: var(--space-2xs); margin-top: var(--space-s); }
@media (min-width: 40rem) {
  .funnel__choices { grid-template-columns: repeat(2, 1fr); }
  .funnel__choice--wide { grid-column: 1 / -1; }
}
.funnel__choice {
  display: flex; align-items: flex-start; gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-s);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-raised); cursor: pointer;
  /* 44px, so a choice is a comfortable tap target rather than a line of text. */
  min-height: var(--tap-min);
}
.funnel__choice:hover { border-color: var(--brand-400); }
.funnel__choice:has(input:checked) {
  border-color: var(--brand-ink); background: var(--brand-wash-strong);
  box-shadow: inset 0 0 0 1px var(--brand-600);
}
.funnel__choice:has(input:focus-visible) { outline: 3px solid var(--brand-500); outline-offset: 2px; }
.funnel__choice input { margin-top: .25rem; flex: 0 0 auto; }
.funnel__choice span { display: flex; flex-direction: column; gap: .1rem; }
.funnel__choice em { font-style: normal; color: var(--brand-ink-strong); font-weight: var(--weight-bold); }
.funnel__choice small { color: var(--text-muted); font-size: var(--step--1); }

/* minmax(0, 1fr), not 1fr — and min-width: 0 on the items.
   ---------------------------------------------------------------------------------
   `1fr` is shorthand for `minmax(auto, 1fr)`, and `auto` as a track MINIMUM is the
   item's min-content contribution, not zero. The items here are labels wrapping an
   <input>, and an input with no width declared sits at its intrinsic size=20 width,
   which with this padding and border measures 230px. So each track was pinned at 230
   and could not take its share of a narrower row.

   Measured on the last funnel step at a 1440px viewport, where the panel is 670px wide:
   the three tracks of the name/email/phone row resolved to `230px 230px 230px`, and
   230 x 3 + 16 x 2 = 722. The Phone field ran 52px past the row and 19px past the
   visible edge of the card — a text box with its right-hand border off the panel.

   The row above it showed the other half of the same bug. Two items in a three-track
   grid resolved to `230px 230px 178px`: both fields pinned at their min-content and the
   leftover handed to an empty third track, so Venue and Town did not line up with Date
   and Starts directly above them.

   BOTH rules are needed, but not for the reason it looks like. Measured, at 1440px,
   restoring one old declaration at a time: either fix ALONE brings the overflow to zero
   there, because minmax(0, …) drops the track's floor and min-width: 0 drops the item's
   min-content contribution that the floor was reading — two ways to the same result.

   Below 40rem they stop being interchangeable. The three-track template only exists
   inside the media query, so above it there is a grid to fix and beneath it there is
   not: the row is one implicit track sized by its item. At 320px, where the panel is
   207px wide and an input wants 230, `min-width: 0` is the only thing doing anything at
   all — restoring `min-width: auto` there puts 20px of Date field back outside the card
   whatever the media query says. So this was never only a desktop bug, and the rule that
   fixes the phone is deliberately outside the media query.

   Why .item-form's song rows never had this, despite the same `Nfr` tracks and the same
   bare inputs: portal.css gives its controls `width: 100%`, and that alone holds the
   tracks down — measured, removing it takes them straight to 221px each and pushes the
   Remove button off the screen. The funnel inputs are the only form controls on the site
   with no width declared, which is why the funnel is where this surfaced. Adding one here
   would be redundant with the two rules above, and it would shrink the honeypot field
   from 227px to 26px for no gain. */
.funnel__row { display: grid; gap: var(--space-s); margin-top: var(--space-s); }
@media (min-width: 40rem) {
  .funnel__row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.funnel__row label, .funnel__single {
  display: flex; flex-direction: column; gap: .3rem; min-width: 0;
}
.funnel__single { margin-top: var(--space-s); }

/* --- Funnel fields: label, required marker, hint, error ------------------------
   The error sits INSIDE the label, directly under its own input, and is tied to it by
   aria-describedby. It used to be one line under the buttons, 158px below the two boxes
   it was describing — close enough to "something is wrong somewhere" to be useless on a
   form this size. */
.funnel__field { display: flex; flex-direction: column; gap: .3rem; }
.funnel__label { font-size: var(--step--1); font-weight: var(--weight-bold); }
/* The same asterisk the contact form uses, so both forms teach one convention. */
.funnel__required { color: var(--brand-ink); margin-inline-start: 2px; }
.funnel__reqkey { margin: var(--space-2xs) 0 0; font-size: var(--step--2); color: var(--text-muted); }
.funnel__fieldhint { font-size: var(--step--2); color: var(--text-muted); }
.funnel__fielderror {
  font-size: var(--step--2);
  font-weight: var(--weight-semibold);
  color: var(--danger);
}
/* Invalid is a border AND a message, never colour alone: a red edge says "wrong" to
   people who can see red and nothing at all to anyone else. */
.funnel :is(input, select, textarea)[aria-invalid="true"] { border-color: var(--danger); }
.funnel :is(input, select, textarea)[aria-invalid="true"]:focus-visible { outline-color: var(--danger); }

.funnel__row span, .funnel__single > span { font-size: var(--step--1); font-weight: var(--weight-bold); }
.funnel input[type="text"], .funnel input[type="email"], .funnel input[type="tel"],
.funnel input[type="date"], .funnel input[type="time"], .funnel input[type="number"],
.funnel textarea {
  padding: var(--space-2xs) var(--space-xs);
  border: 1px solid var(--border-control); border-radius: var(--radius-sm);
  font: inherit; background: var(--surface-raised); color: var(--text);
  min-height: var(--tap-min);
}
.funnel textarea { min-height: 5rem; }
.funnel :is(input, textarea):focus-visible { outline: 3px solid var(--brand-500); outline-offset: 1px; }

/* The one line under the homepage router that names what HPN sells. Quiet on purpose —
   it answers "what is HPN?" without competing with "what are you planning?" above it. */
.section--tight { padding-block: var(--space-m); }
/* Not prose, despite being a <p>.

   `base.css` sets `p { max-width: var(--width-prose) }` -- 38rem -- with no auto margin,
   so this row inherited a 608px cap and sat FLUSH LEFT inside a 1088px container.
   `justify-content: center` then centred the chips within that 608px box, which is why
   they looked centred relative to each other while the whole strip hugged the left edge,
   directly under a row of centred cards. Measured in a browser at 1440: container x=176
   w=1088, this element x=176 w=608.

   The strip is deliberately quiet -- a sentence, not a second grid of six equal cards,
   which is the "everything presented equally" problem Craig objected to. Quiet is not the
   same as misaligned. */
.service-chips { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--space-3xs) var(--space-2xs); margin: 0 auto; max-width: 58rem; text-align: center; }
.service-chips__lead { color: var(--text-muted); }
.service-chips__chip { display: inline-flex; align-items: center; min-height: var(--tap-min); padding: .35rem .8rem; border: 1px solid var(--border); border-radius: var(--radius-pill); font-size: var(--step--1); font-weight: var(--weight-medium); color: var(--text); text-decoration: none; }
.service-chips__chip:hover, .service-chips__chip:focus-visible { border-color: var(--brand-ink); color: var(--brand-ink-strong); }

/* "Add or change something" — the two catalogue screens that used to come BEFORE the
   plan, folded into it. Closed by default: the plan is the answer, this is an adjustment
   to it, and a wall of twelve checkboxes under a recommendation reads as more homework. */
.funnel__tweak { margin-top: var(--space-m); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-2xs) var(--space-xs); }
/* `display: flex` on a <summary> suppresses the disclosure triangle, so this read as a
   plain bordered heading and the eleven packages and add-ons inside it were invisible —
   nobody opens a box that does not look openable. The flex layout is still wanted (it is
   what gives the 44px tap target), so the marker is drawn back explicitly and rotated
   when open. */
.funnel__tweak > summary {
  cursor: pointer;
  font-weight: var(--weight-bold);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  list-style: none;
}
.funnel__tweak > summary::-webkit-details-marker { display: none; }
.funnel__tweak > summary::before {
  content: "";
  flex: 0 0 auto;
  width: .5rem; height: .5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  rotate: -45deg;
  transition: rotate var(--dur) var(--ease);
}
.funnel__tweak[open] > summary::before { rotate: 45deg; }
.funnel__tweak[open] > summary { margin-bottom: var(--space-2xs); }

/* The secondary path under a CTA — "or send us a message". Quiet, but present, because
   some people would always rather write a sentence to a human. */
.cta-alt { margin: var(--space-2xs) 0 0; font-size: var(--step--1); opacity: .85; }
.cta-alt a { color: inherit; }

/* `.funnel fieldset` above is (0,1,1) and this was (0,1,0), so the reset won and the
   separator never rendered: measured gap between the "Add or change something" box and
   the "Where should we send it?" legend was 0px at every width. Naming the element here
   raises this to (0,2,1) so the rule it was always meant to be actually applies. */
.funnel fieldset.funnel__contact {
  margin-top: var(--space-l);
  padding-top: var(--space-m);
  border-top: 1px solid var(--border);
}
.funnel__contact legend { font-weight: var(--weight-bold); }
/* The honeypot. Off-screen rather than display:none — some bots skip hidden fields but
   fill in anything they can find in the DOM. aria-hidden keeps it from screen readers. */
.funnel__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.funnel__nav { display: flex; gap: var(--space-2xs); margin-top: var(--space-l); }
.funnel__nav .btn--primary { margin-inline-start: auto; }
.funnel__error { color: var(--danger); font-weight: var(--weight-bold); margin-top: var(--space-s); }

/* --- The recommendation ------------------------------------------------------ */
.plan { border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-m); background: var(--surface-muted); }
.plan__headline { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-s); flex-wrap: wrap; }
.plan__headline h3 { font-size: var(--step-2); margin: 0; }
.plan__price { font-family: var(--font-display); font-size: var(--step-2); color: var(--brand-ink-strong); margin: 0; }
.plan__why { color: var(--text-muted); margin: var(--space-2xs) 0 var(--space-s); }
.plan__sub { font-size: var(--step--1); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); margin: var(--space-s) 0 var(--space-3xs); }
.plan__lines { list-style: none; margin: 0; padding: 0; }
/* Let the line wrap rather than pushing the price out of it.
   ---------------------------------------------------------------------------------
   Both children are flex items, so both default to min-width: auto and refuse to shrink
   below their content — and the price is `white-space: nowrap` deliberately, so it
   cannot give way even in principle. A long name therefore pushed the price out of the
   row instead of the row growing a second line.

   Not hypothetical: five add-ons in the catalogue have no price and publish as "Price on
   request", which is four times the width of "$400". Measured at 360px with Outdoor
   Movie Night selected, the price ran 18px past its row and 17px into the card's 24px of
   padding, out of line with every other price in the list — on the screen the whole
   funnel exists to reach.

   min-width: 0 alone fixes the overflow and looks worse than the bug: the name is
   squeezed to 50px and breaks to "Outdoor / Movie / Night", one word per line, while the
   price sits alongside it. So the row is allowed to WRAP instead. When the two do not fit
   the price drops to its own line and margin-inline-start: auto keeps it right-aligned
   under itself; the name then has the full 197px and stays on one line. min-width: 0 is
   still there for the case wrapping cannot help — a single word longer than the row. */
.plan__lines li {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  gap: var(--space-s); padding: .35rem 0; border-bottom: 1px dashed var(--border);
}
.plan__lines li > :first-child { flex: 1 1 auto; min-width: 0; }
.plan__lines small { display: block; color: var(--text-muted); font-size: var(--step--2); }
.plan__line-price { white-space: nowrap; margin-inline-start: auto; color: var(--brand-ink-strong); }
.plan__total { display: flex; justify-content: space-between; margin: var(--space-s) 0 0; font-size: var(--step-1); }
.plan__note { color: var(--text-muted); font-size: var(--step--1); margin: var(--space-3xs) 0 0; }
/* Warnings are the engine telling the customer something true that they may not like —
   over budget, incompatible, unavailable. Styled to be read, not to alarm. */
.plan__warning {
  margin: var(--space-xs) 0 0; padding: var(--space-2xs) var(--space-xs);
  border-inline-start: 3px solid var(--star); background: var(--surface-raised);
  color: var(--text); font-size: var(--step--1);
}
.plan__suggestions { margin-top: var(--space-m); padding-top: var(--space-s); border-top: 1px solid var(--border); }
.plan__upsell { margin: var(--space-s) 0 0; color: var(--brand-ink-strong); font-weight: var(--weight-bold); }

.funnel--done h2 { font-family: var(--font-display); }
.funnel--done { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-l); max-width: 46rem; }

@media (prefers-reduced-motion: reduce) { .funnel__choice { transition: none; } }

/* --- AI event concierge (Milestone 6) ----------------------------------------
   A floating panel, in the shape people already expect from a chat widget — which is
   also the shape of the Reamaze widget it is meant to replace. It renders NOTHING until
   /api/concierge/status/ says it can run, so a missing API key produces no button rather
   than a button that apologises. */
.concierge {
  position: fixed;
  right: var(--space-s);
  bottom: var(--space-s);
  z-index: 60;                /* above content, below the auth dialog */
  display: flex;
  /* column-REVERSE, so the panel opens UPWARD from the launcher.
     Plain `column` put the panel below its own button, which is the one place a chat
     widget never is: the button jumped to the top of a 36rem panel the moment it was
     pressed, and the X ended up at the far end from the thumb that had just tapped.
     DOM order stays button-then-panel — that is the order the keyboard and a screen
     reader need for a disclosure, and only the paint order is reversed. */
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: var(--space-2xs);
  max-width: min(24rem, calc(100vw - 2 * var(--space-s)));

  /* This box is a POSITIONER, not a surface. It paints nothing -- no background, no
     border -- but it is fixed, it is on top, and it was still hit-testable, so it ate
     every tap that landed inside it. On a phone that is measurable: the media query
     below anchors it to both edges, and the button only fills the right-hand end, so
     88x44px of invisible nothing sat over the bottom-left corner of every page,
     swallowing taps on whatever was underneath. Nothing is drawn there, so a visitor
     tapping a link and getting no response has no way to work out why -- they just
     conclude the link is broken. The gap between the button and the open panel is the
     same hazard, one row up.

     `none` here, `auto` on the parts that are actually the widget. */
  pointer-events: none;
}

.concierge__toggle,
.concierge__panel { pointer-events: auto; }

/* --- The launcher -------------------------------------------------------------
   This is the only invitation the concierge gets. It sits over whatever happens to be
   at the bottom of the page — a white section, the near-black footer, a photograph —
   so "purple pill with a drop shadow" disappeared against half the site. Three things
   fix that, and none of them is a bigger button:

     the ring   a 3px white halo that separates it from ANY background,
     the depth  a brand-tinted shadow, so it reads as floating above the page rather
                than as a block of colour printed on it,
     the pulse  one ring that expands twice, a second after it appears, and then stops.
                Twice, not forever: the site this replaces ran two INFINITE marquees,
                and that is the tone this project is correcting. */
.concierge__toggle {
  position: relative;
  padding-inline: var(--space-s) var(--space-m);
  background: linear-gradient(135deg, var(--brand-600), var(--brand-500));
  /* White text measures 9.15:1 at the dark end and 7.59:1 at the light one. */
  color: var(--text-on-brand);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .92),
    0 12px 28px rgba(141, 11, 171, .40),
    0 3px 8px rgba(22, 22, 22, .18);
}
.concierge__toggle:hover {
  background: linear-gradient(135deg, var(--brand-700), var(--brand-600));
  transform: translateY(-2px);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .96),
    0 16px 34px rgba(141, 11, 171, .48),
    0 4px 10px rgba(22, 22, 22, .20);
}
.concierge__toggle .icon { flex: none; }

/* The attention ring. A pseudo-element, so it costs no markup and cannot be tabbed to. */
.concierge__toggle::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 2px solid var(--brand-300);
  opacity: 0;
  pointer-events: none;
  animation: concierge-attention 2.2s var(--ease) 1.2s 2;
}
/* Nothing pulses once the visitor has opened it — they have already found it. */
.concierge.is-open .concierge__toggle::after { animation: none; }

@keyframes concierge-attention {
  0%   { opacity: .9; transform: scale(1); }
  70%  { opacity: 0;  transform: scale(1.28); }
  100% { opacity: 0;  transform: scale(1.28); }
}

/* Open, the toggle is an X and nothing else, so the pill's horizontal padding would
   leave a lozenge around a 20px glyph. Square it off into a circle instead. */
.concierge__toggle--close {
  width: var(--tap-min); height: var(--tap-min);
  padding: 0; border-radius: 50%;
}

.concierge__panel {
  display: flex;
  flex-direction: column;
  width: min(24rem, calc(100vw - 2 * var(--space-s)));
  /* A REAL height, not just a cap. Sized to content the panel opened barely taller than
     the greeting, so the first reply made it jump and every reply after that scrolled a
     porthole; giving it a standing height means the conversation grows inside a box that
     does not move. Still capped against the VIEWPORT, because on a landscape phone a
     36rem panel is taller than the screen and the input goes below the fold — unusable
     at exactly the moment someone is typing into it. The 8rem subtracted is the toggle,
     the gap and the bottom offset above which the panel sits. */
  height: min(36rem, calc(100vh - 8rem));
  max-height: calc(100vh - 8rem);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* Deeper than --shadow-3. This panel floats over a whole page rather than sitting in
     one, and a card's elevation left it looking pasted on. */
  box-shadow: 0 20px 52px rgba(22, 22, 22, .24), 0 4px 14px rgba(22, 22, 22, .12);
  overflow: hidden;
  transform-origin: bottom right;
  /* Runs when `hidden` comes off, because display: none -> flex restarts an animation.
     It grows from the corner the launcher is in, so the panel visibly comes out of the
     button that was just pressed rather than appearing on top of it. */
  animation: concierge-open var(--dur-slow) var(--ease) both;
}

@keyframes concierge-open {
  from { opacity: 0; transform: translateY(12px) scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* --- Header -------------------------------------------------------------------
   Brand, not another grey bar. The panel opens over page content and needs an edge that
   says where it starts; an --ink-050 strip under a white page did not give it one. */
.concierge__head {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  /* Every band in this panel is flex: none, so THE LOG is the only thing that gives when
     the panel is short. Without it flexbox shrank all four in proportion: on a landscape
     phone the header lost the bottom half of "Answers come from HPN's own prices and
     schedule" — the one sentence that makes a quoted price believable — and the greeting
     ended up underneath the starter chips. */
  flex: none;
  padding: var(--space-xs) var(--space-s);
  background: linear-gradient(135deg, var(--brand-700), var(--brand-500));
  color: var(--text-on-brand);
}
.concierge__mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 2rem; height: 2rem;
  border-radius: 50%;
  background: rgba(255, 255, 255, .18);
  color: var(--ink-000);
}
.concierge__head-text { min-width: 0; }
.concierge__head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-0);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}
/* --brand-50 rather than a translucent white: 6.50:1 against the light end of the
   gradient, so the line saying where the answers come from is readable rather than
   decorative. It is the sentence that makes the whole widget trustworthy. */
.concierge__head p { margin: .1rem 0 0; font-size: var(--step--2); color: var(--brand-50); }

.concierge__log {
  flex: 1;
  /* A flex item defaults to min-height:auto, which refuses to shrink below its content.
     Without this the log pushes the form off the bottom of the fixed-height panel
     instead of scrolling. */
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-s);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  /* A tinted floor, so the assistant's bubbles are objects ON something. Both speakers
     used to sit on the same white, and the only thing separating a question from its
     answer was which side of the panel it was on. */
  background: var(--surface-muted);
  /* Contain the scroll so a flick inside the log does not scroll the page behind it. */
  overscroll-behavior: contain;
}
.concierge__turn {
  padding: var(--space-2xs) var(--space-xs);
  border-radius: 14px;
  font-size: var(--step--1);
  line-height: var(--leading-body);
  max-width: 88%;
  white-space: pre-wrap;      /* the model's paragraph breaks are meaningful */
  /* A URL has no spaces to wrap at, and pre-wrap will not invent one. Somebody
     asked "which venue?" pastes a link, and on a USER turn -- white text on the
     purple gradient -- the part that does not fit was painted outside the bubble,
     white on the light log behind it, i.e. invisible. Measured with a real venue
     PDF link: the widest line came to 406px inside a 284px bubble, 122px of it
     unreadable, and the log picked up 94px of sideways scroll with it. `anywhere`
     rather than `break-word` because it also lowers the min-content width, so the
     bubble may shrink instead of merely letting the text out of it. */
  overflow-wrap: anywhere;
}
/* The one square corner points back at its own speaker — what a bubble tail does,
   without the SVG a tail costs. */
.concierge__turn--assistant {
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom-left-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  align-self: flex-start;
}
.concierge__turn--user {
  background: linear-gradient(135deg, var(--brand-600), var(--brand-500));
  color: var(--text-on-brand);
  border-bottom-right-radius: var(--radius-sm);
  align-self: flex-end;
}
/* The links the concierge offers are the point of it — the conversation could name a
   page and then leave the visitor to find it. They have to look clickable in both
   bubbles, and inherit is not enough on a purple one. */
.concierge__link { color: var(--brand-ink-strong); font-weight: var(--weight-semibold); text-underline-offset: 2px; }
.concierge__turn--user .concierge__link { color: var(--ink-000); }

/* Three dots that actually move. The indicator was a literal "…", indistinguishable
   from a reply that opens with an ellipsis — at the one moment the visitor needs to
   know the difference between "thinking" and "finished". The animation is bounded by
   the pending turn: it exists only while a reply is on its way. */
.concierge__typing { display: inline-flex; align-items: center; gap: 4px; padding-block: .35rem; }
.concierge__typing i {
  width: 6px; height: 6px; border-radius: 50%;
  /* --ink-400 (3.95:1 on the white bubble), not --ink-300 (2.49:1). These dots carry
     status rather than decoration — WCAG 1.4.11 wants 3:1 on a graphic you have to see
     to understand what is happening — and they are read on a phone, outdoors, at 6px. */
  background: var(--text-muted);
  animation: concierge-dot 1.1s var(--ease) infinite;
}
.concierge__typing i:nth-child(2) { animation-delay: .15s; }
.concierge__typing i:nth-child(3) { animation-delay: .30s; }

/* The trough is .5, not .35: at 6px the dots spend most of the cycle at the LOW end,
   so that value is the one a visitor actually reads, and .35 of a 3.95:1 grey is not a
   dot any more. */
@keyframes concierge-dot {
  0%, 60%, 100% { opacity: .5; transform: translateY(0); }
  30%           { opacity: 1;  transform: translateY(-3px); }
}

.concierge__fallback { display: block; margin-top: .35rem; font-weight: var(--weight-bold); }

/* --- Starters -----------------------------------------------------------------
   A chat widget's hardest moment is the empty box. "Ask about your event" is an
   invitation, not a question anybody has ready — and the visitor cannot tell whether
   this thing knows HPN's prices or is a search box with manners. Three real openings
   answer both at once, and they disappear the moment anything is said. */
.concierge__starters {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  gap: var(--space-3xs);
  padding: 0 var(--space-s) var(--space-s);
  background: var(--surface-muted);
}
.concierge__starter {
  min-height: var(--tap-min);   /* the site's floor for anything tappable */
  padding: var(--space-3xs) var(--space-xs);
  /* --brand-400, not --brand-200. The border IS the control here — it is the only thing
     that says these three lines are buttons rather than a list — and WCAG 1.4.11 asks
     for 3:1 on that boundary. brand-200 measured 1.87:1 against the strip behind it and
     2.00:1 against the chip's own fill, so on a phone in daylight the pills disappeared
     and left three purple sentences. brand-400 is 4.28:1 and 4.59:1. */
  border: 1px solid var(--brand-400);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--brand-ink-strong);      /* 11.59:1 on the chip's white fill */
  font: inherit;
  font-size: var(--step--2);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.concierge__starter:hover { background: var(--brand-wash); border-color: var(--brand-ink); }

.concierge__form {
  display: flex; flex: none; gap: var(--space-2xs); align-items: center;
  padding: var(--space-2xs); border-top: 1px solid var(--border);
  background: var(--surface);
}
.concierge__form input {
  flex: 1; min-width: 0; min-height: var(--tap-min);
  padding: var(--space-3xs) var(--space-s);
  border: 1px solid var(--border-control); border-radius: var(--radius-pill);
  font: inherit; background: var(--surface-muted); color: var(--text);
}
.concierge__form input::placeholder { color: var(--text-muted); }
.concierge__form input:focus-visible {
  background: var(--surface);
  outline: 3px solid var(--brand-500); outline-offset: 1px;
}
/* Icon-only, so "you cannot send yet" can no longer be carried by the word changing to
   "Sending…". It has to be visible in the button itself. */
.concierge__send {
  flex: none;
  width: var(--tap-min); height: var(--tap-min);
  padding: 0; border-radius: 50%;
  background: linear-gradient(135deg, var(--brand-600), var(--brand-500));
}
.concierge__send:hover { background: linear-gradient(135deg, var(--brand-700), var(--brand-600)); }
.concierge__send:disabled { opacity: .45; cursor: not-allowed; }
.concierge__send:disabled:hover {
  background: linear-gradient(135deg, var(--brand-600), var(--brand-500));
}
.concierge__ended { flex: none; margin: 0; padding: var(--space-s); border-top: 1px solid var(--border); }

/* A landscape phone is SHORT, not narrow, and the panel is capped against the viewport:
   at 400px tall it is 272px, and the header, three 44px chips and the input add up to
   more than that on their own. The chips are the part that is optional — they are a
   shortcut past the empty box, and the box, the conversation and the send button are
   not — so below this height they go and the log gets the room back. */
@media (max-height: 34rem) {
  .concierge__starters { display: none; }
}

/* On a small phone the panel is the screen. Anchoring it to both edges stops it being a
   narrow column with the send button cramped against the input. */
@media (max-width: 30rem) {
  .concierge { left: var(--space-s); align-items: stretch; }
  .concierge__panel { width: auto; }
  /* stretch would otherwise pull both buttons the full width of the screen, dropping
     the round close button at the left edge — a long way from the panel corner the
     thumb reaches for. */
  .concierge__toggle { align-self: flex-end; }

  /* Room for the launcher at the end of the scroll. It is fixed, so at the very bottom
     of the page it parks on top of the last thing there — measured on a 390px viewport,
     that is the copyright line, half-covered with nowhere left to scroll to. Padding on
     the footer rather than the body: the launcher is the reason, the footer is the only
     place it lands, and putting it on the body would push a gap onto every short page
     that never scrolls at all. 44px button + the space-s inset it sits above. */
  .site-footer { padding-bottom: calc(44px + 2 * var(--space-s)); }
}

@media (prefers-reduced-motion: reduce) {
  .concierge__panel { animation: none; }
  .concierge__toggle::after { animation: none; }
  .concierge__toggle:hover { transform: none; }
  /* The dots stay and stop moving. Removing them would take away the only signal that
     a reply is on its way — and at full opacity, because a still dot has no bright part
     of a cycle to be read during. */
  .concierge__typing i { animation: none; opacity: 1; }
}


/* --- Your account ------------------------------------------------------------- */
/* Not the .auth-card the sign-in pages use. That is 28rem, which is right for one form
   and wrong for a page carrying three things at once — identity, account state, and a
   list of events. */
.profile { padding-block: clamp(var(--space-l), 6vh, var(--space-2xl)); }

/* .profile__messages retired — messages are a site-wide region now, .site-messages
   in chrome.css. Rendering them here as well showed every message twice. */

.profile__head {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  flex-wrap: wrap;
  padding-bottom: var(--space-m);
  margin-bottom: var(--space-l);
  border-bottom: 1px solid var(--border);
}
.profile__avatar {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--brand-500), var(--brand-700));
  color: var(--text-on-brand);
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  /* The letter is a glyph, not a word — centring it optically beats centring its box. */
  padding-top: 0.06em;
}
.profile__id { flex: 1 1 16rem; min-width: 0; }
.profile__name {
  margin: 0;
  font-size: var(--step-3);
  line-height: var(--leading-tight);
}
.profile__email {
  margin: var(--space-3xs) 0 0;
  color: var(--text-muted);
  /* An email address is one unbroken token and will overflow a narrow column. */
  overflow-wrap: anywhere;
}
.profile__since {
  margin: var(--space-3xs) 0 0;
  font-size: var(--step--1);
  color: var(--text-muted);
}
.profile__signout { flex: 0 0 auto; margin: 0; }

/* TWO columns, not auto-fit. auto-fit made three of them on a wide screen and there are
   only two small cards, so the row ended with a column of empty space — which reads as a
   card that failed to render rather than as a layout. The wide card spans both. */
.profile__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-m);
}
@media (min-width: 52rem) {
  .profile__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.profile__card {
  /* Stretch, not content-height: two cards of different lengths side by side left a
     visible step along the bottom edge of the row. */
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-m);
  box-shadow: var(--shadow-1);
}
.profile__card--wide { grid-column: 1 / -1; }
.profile__card-title {
  margin: 0 0 var(--space-s);
  font-size: var(--step-0);
  line-height: var(--leading-tight);
}

.profile__list { list-style: none; margin: 0; padding: 0; }
.profile__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  flex-wrap: wrap;
  padding-block: var(--space-xs);
  border-bottom: 1px solid var(--border);
}
.profile__row:last-child { border-bottom: 0; }
.profile__row-main { min-width: 0; overflow-wrap: anywhere; font-weight: var(--weight-medium); }
.profile__row--empty { color: var(--text-muted); font-weight: var(--weight-regular); }
.profile__tags { display: flex; gap: var(--space-3xs); flex: 0 0 auto; }

.profile__note {
  margin: var(--space-s) 0 0;
  font-size: var(--step--1);
  color: var(--text-muted);
  max-width: var(--width-prose);
}
/* An unverified address means the portal shows nothing at all, so this is not a hint —
   it is the reason the page below it is empty. */
.profile__note--warn {
  color: var(--text-strong);
  background: var(--note-warn-surface);
  border: 1px solid var(--note-warn-border);
  border-radius: var(--radius-md);
  padding: var(--space-xs) var(--space-s);
}
/* margin-top:auto pins the action to the bottom of a stretched card, so the two buttons
   in a row line up with each other instead of floating at different heights. */
.profile__actions { margin: var(--space-m) 0 0; padding-top: 0; margin-top: auto; }

/* --- Tag ---------------------------------------------------------------------- */
/* Status, not decoration: "Not confirmed" is the difference between seeing your event
   and seeing an empty portal. Both states carry a WORD as well as a colour — a colour-only
   difference is invisible to anyone who cannot distinguish these two hues. */
.tag {
  display: inline-flex;
  align-items: center;
  padding: 0.15em 0.6em;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-strong);
  font-size: var(--step--2);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
}
.tag--ok   { background: var(--tag-ok-surface);   color: var(--tag-ok-text); }
.tag--warn { background: var(--tag-warn-surface); color: var(--tag-warn-text); }

@media (max-width: 40rem) {
  .profile__head { gap: var(--space-s); }
  .profile__avatar { width: 3.5rem; height: 3.5rem; font-size: var(--step-2); }
  /* The sign-out button drops to its own row rather than squeezing the name. */
  .profile__signout { flex: 1 0 100%; }
  .profile__signout .btn { width: 100%; justify-content: center; }
}


/* "Often booked with X" — the combination line on an offering card. Quieter than the
   summary above it: it is a nudge, not the pitch, and it must not compete with the price
   or the call to action. The name inside it carries the weight. */
.card__pairs {
  margin: var(--space-2xs) 0 0;
  font-size: var(--step--2);
  color: var(--text-muted);
  line-height: var(--leading-snug, 1.4);
}
.card__pairs strong { color: var(--text-strong); font-weight: var(--weight-semibold); }


/* A page the concierge offered. It sits inside a sentence, so it is underlined rather
   than made a button — the reply is still prose, and a button mid-paragraph reads as the
   end of the answer.

   brand-600, not brand-300. The panel is a LIGHT surface (#f7f7f7) and brand-300 is a
   colour for dark ones: measured against the rendered pixels it came out at 2.87:1,
   which fails WCAG AA for text. brand-600 measures 7.08:1 on the same background. The
   underline carries the affordance regardless of colour, which is what makes this safe
   for anyone who cannot separate the two purples. */
.concierge__link {
  color: var(--brand-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
}
.concierge__link:hover { color: var(--brand-ink-strong); }

/* The way out of the conversation.

   The model is asked to end a reply that answered something with the page that goes
   with it. Rendered as an ordinary inline link that arrived as one underlined word on a
   line of its own, under a paragraph of prose — the shape of a footnote, not of an exit.
   The whole point of it is that a visitor who has just been told what their wedding
   costs can go and read the wedding page, so it is drawn as somewhere to go.

   Ghost-button language, matching the secondary buttons on the pages it leads to, and
   deliberately quieter than the send button: it is an offer at the end of an answer, not
   the thing the visitor came to do.

   display:flex with width:fit-content, NOT inline-flex. Inline-flex left the pill in the
   run of text, so it surfaced tucked against the final words of the last sentence
   ("...package up? [Weddings]") — which is the footnote problem again in a nicer shape.
   A block box breaks the line; fit-content stops it stretching across the bubble like a
   banner. */
.concierge__dest {
  display: flex;
  width: fit-content;
  align-items: center;
  gap: var(--space-3xs);
  margin-top: var(--space-2xs);
  padding: .45rem .7rem;
  border: 1px solid var(--brand-600);
  border-radius: var(--radius-pill, 999px);
  color: var(--brand-ink-strong);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  text-decoration: none;
  background: transparent;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.concierge__dest:hover,
.concierge__dest:focus-visible {
  background: var(--brand-600);
  color: #fff;
}
/* The arrow leans into the direction it promises. Transform only, so nothing reflows. */
.concierge__dest svg { transition: transform var(--dur-fast) var(--ease); }
.concierge__dest:hover svg { transform: translateX(2px); }
