/* ==========================================================================
   The Brick Ballroom — design tokens
   Color strategy: Committed. One brass/gold accent, grounded in the venue's
   own Art Deco lounge trim. Black + white from the real building, not a guess.
   ========================================================================== */

:root {
  /* Color — OKLCH */
  --color-bg: oklch(100% 0 0);
  --color-surface: oklch(97% 0.004 85);
  --color-surface-deep: oklch(93% 0.006 85);
  --color-ink: oklch(15% 0.01 85);
  --color-ink-soft: oklch(22% 0.01 85);
  --color-muted: oklch(40% 0.008 85);
  --color-border: oklch(90% 0.006 85);
  /* Idle state for small non-text marks (rail dots): >=3:1 on white per
     WCAG 1.4.11, where --color-border (1.35:1) is decorative only. */
  --color-mark-idle: oklch(64% 0.012 85);
  --color-border-on-dark: oklch(32% 0.01 85);
  /* Boundary of a CONTROL on a noir surface. WCAG 1.4.11 wants 3:1 for the
     visual boundary of a form field or a button, and --color-border-on-dark
     measures 1.58:1 on --color-noir and 1.43:1 on --color-noir-surface — it
     was the sole edge of all eleven form controls and the whole booking
     picker. This is the on-dark twin of --color-mark-idle (which exists for
     exactly the same reason on the light side, where --color-border is
     1.34:1). Measured 2026-07-29: 3.47:1 on noir, 3.15:1 on noir-surface.
     --color-border-on-dark keeps its 1.58 and stays what it always was: a
     DECORATIVE rule — section dividers, the footer hairline, the stats rule
     — where WCAG asks nothing. Do not use it to draw a control. */
  --color-border-on-dark-ui: oklch(51% 0.012 85);

  /* Error red. Was written out longhand at four separate sites, so a change
     to the error colour meant finding all four. It is the one semantic role
     the palette was missing. */
  --color-error: oklch(70% 0.16 30);
  /* Shadow ink. The two logo drop-shadows were the only sRGB rgba() left in an
     OKLCH system; same greys, expressed in the space everything else uses. */
  --shadow-ink-soft: oklch(0% 0 0 / 0.35);
  --shadow-ink: oklch(0% 0 0 / 0.45);

  --color-primary: oklch(56% 0.12 78);
  --color-primary-deep: oklch(44% 0.10 76);
  --color-primary-pale: oklch(93% 0.03 85);
  --color-on-primary: oklch(100% 0 0);
  /* Lighter gold for text set on solid dark/noir surfaces (not photos).
     --color-primary itself is too close to 4.5:1 on near-black to use as text. */
  --color-primary-on-dark: oklch(74% 0.11 80);

  /* near-black surface, for the lounge-inspired dark sections */
  --color-noir: oklch(14% 0.01 85);
  --color-noir-surface: oklch(20% 0.01 85);
  --color-on-noir: oklch(96% 0.004 85);
  --color-on-noir-muted: oklch(72% 0.01 85);

  /* Typography */
  --font-display: 'Petrona', 'Iowan Old Style', Georgia, serif;
  --font-body: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --text-display: clamp(2.75rem, 2rem + 3.2vw, 5.5rem);
  --text-display-sm: clamp(2.375rem, 1.9rem + 2.2vw, 4rem);
  --text-headline: clamp(2rem, 1.65rem + 1.6vw, 3rem);
  --text-title: clamp(1.375rem, 1.25rem + 0.5vw, 1.75rem);
  --text-lead: 1.1875rem;
  --text-body: 1rem;
  --text-small: 0.9375rem;
  --text-label: 0.8125rem;

  /* Spacing — 4pt scale */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;
  --space-4xl: 8rem;

  /* Layout */
  --container-max: 78rem;
  --container-narrow: 42rem;
  --measure: 62ch;

  /* Radius — Art Deco is geometric; keep corners honest, not soft */
  --radius-sm: 2px;
  --radius-md: 4px;

  /* Motion */
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 150ms;
  --duration-base: 250ms;
  --duration-slow: 450ms;

  /* z-index scale */
  --z-subnav: 90;   /* sticky in-page wayfinder — always under the header */
  --z-sticky: 100;
  --z-mobile-nav: 200;
  --z-modal: 250;
  --z-toast: 300;

  /* Fixed header's compact (is-scrolled) height at ≥48rem — the offset a
     sticky element needs to sit just below it. Measured 69px (Playwright,
     2026-07-26: 12px padding ×2 + 44px logo ×0.82 scale still reserves the
     unscaled 44px box + 1px border). Re-measure if the header's compact
     padding or logo sizing changes. */
  --header-stuck: 4.3125rem;
}

/* ==========================================================================
   Reset & base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  color-scheme: light;
  -webkit-text-size-adjust: 100%;
  /* Defensive: a transformed full-viewport panel (mobile nav) or a stray
     wide element shouldn't be able to force horizontal scroll on the page. */
  overflow-x: hidden;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.6;
  color: var(--color-ink);
  background: var(--color-bg);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  /* A single unbreakable token can be wider than the whole viewport once text
     scales: "brickballroom@gmail.com" measures 407px at 200% on a 390px
     phone. `break-word` only acts when a word genuinely doesn't fit, so it
     changes nothing at normal size — but it stops `html{overflow-x:hidden}`
     from quietly amputating the end of an email address nobody can then read
     or copy. */
  overflow-wrap: break-word;
}

img, picture, video, svg {
  display: block;
  max-width: 100%;
}
/* Responsive-image wrappers: where an img fills its media box
   with height:100%, the interposed <picture> must span the box too, or the
   percentage chain collapses to auto. */
.hero__media picture,
.downtown__media picture,
.quote-band__media picture,
.gallery-item picture {
  width: 100%;
  height: 100%;
}

a { color: inherit; text-decoration: none; }

button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }

input, textarea, select { font: inherit; color: inherit; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;
  line-height: 1.1;
  text-wrap: balance;
  letter-spacing: -0.01em;
}

p { text-wrap: pretty; }

ul, ol { list-style: none; padding: 0; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-sm);
  z-index: var(--z-toast);
  background: var(--color-ink);
  color: var(--color-on-noir);
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-sm);
  transition: top var(--duration-base) var(--ease-out-quart);
}
.skip-link:focus {
  top: var(--space-sm);
}

:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}

.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-md);
}

/* will-change promotes an element onto its own compositor layer and KEEPS it
   there for as long as the declaration applies. Written statically in a rule
   it never comes back down: the-venue.html was holding six full-size decoded
   photographs in permanently promoted layers, for a parallax that only runs
   while the section is crossing the viewport, and the hero title kept a layer
   for a reveal that finishes in under a second. js/main.js now adds this class
   for the life of the tween and takes it off again (one-shot tweens on
   onComplete, scrubbed ones on ScrollTrigger's onToggle). */
.is-animating { will-change: transform; }

/* --------------------------------------------------------------------------
   THE REFLOW FLOOR (root cause, found 2026-07-29 — read before adding a grid)

   A grid track written as `1fr` — or left implicit — has an automatic MINIMUM
   of min-content. It cannot shrink below the widest unbreakable thing inside
   it. Every dimension on this site is a rem, so at a 200% text setting the
   min-content of a quote, a phone number, an email address or a seven-column
   calendar grows past the phone that has to hold it, the track refuses to
   shrink, and `html { overflow-x: hidden }` above deletes the overflow instead
   of letting it scroll: content that is simply gone, with window.scrollX
   pinned at 0 and no scrollable ancestor to reach it.

   Eight grids failed this way at 390px/200%. The fix in every case is
   `minmax(0, …)`, which removes the min-content floor and lets the text wrap.
   Write `minmax(0, 1fr)`, never a bare `1fr`, on anything that holds copy.
   -------------------------------------------------------------------------- */
@media (min-width: 48rem) {
  .container { padding-inline: var(--space-2xl); }
}

.section {
  padding-block: var(--space-3xl);
}
@media (min-width: 64rem) {
  .section { padding-block: var(--space-4xl); }
}
.section--tight { padding-block: var(--space-2xl); }
@media (min-width: 64rem) {
  .section--tight { padding-block: var(--space-3xl); }
}
/* A page's first section when there's no photo hero above it: extra top
   padding so content clears the fixed (transparent) header. */
@media (min-width: 64rem) {}
.section--surface { background: var(--color-surface); }
.section--noir { background: var(--color-noir); color: var(--color-on-noir); }
.section--noir h2, .section--noir h3 { color: var(--color-on-noir); }
.section--noir p { color: var(--color-on-noir-muted); }

.section-head {
  max-width: var(--container-narrow);
  margin-bottom: var(--space-2xl);
}
/* That margin separates the head from the content it heads. Where the head IS
   the content — weddings' "Portraits, a block in any direction.", the one
   text-only section on the site — there is nothing under it to separate from,
   and the margin lands on top of the section's own padding: measured 96px of
   air above the block against 160px below, so a deliberately quiet interlude
   read as a paragraph someone forgot to finish. One instance sitewide today;
   written as a rule so the next solo head can't reintroduce it. */
.section-head:last-child { margin-bottom: 0; }
/* The crest rule is the brand's one ornament, so when a section head earns it
   the rule needs its own breathing room rather than sitting on the heading. */
.section-head .deco-rule { margin-bottom: var(--space-md); }
.section-head--center { margin-inline: auto; text-align: center; }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-primary-deep);
  margin-bottom: var(--space-sm);
}
.eyebrow::before, .eyebrow::after {
  content: '';
  width: 20px;
  height: 1px;
  background: currentColor;
  opacity: 0.6;
}
.section--noir .eyebrow { color: var(--color-primary-on-dark); }

/* A quieter alternative to .eyebrow for sections that have no h2 of their
   own to carry the label — sentence case, italic display font, no tracking,
   so the page isn't repeating the same tracked-caps move in every section. */
.section-caption {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-title);
  color: var(--color-muted);
  margin-bottom: var(--space-sm);
}
.section--noir .section-caption { color: var(--color-on-noir-muted); }

.rating-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  font-size: var(--text-small);
  font-weight: 500;
  color: var(--color-ink-soft);
}
.rating-badge__stars {
  color: var(--color-primary);
  letter-spacing: 0.05em;
}
.section--noir .rating-badge { color: var(--color-on-noir-muted); }
.section--noir .rating-badge__stars { color: var(--color-primary-on-dark); }

h2.section-title { font-size: var(--text-headline); margin-bottom: var(--space-md); }
/* The utility pages (privacy-policy, terms, 404) carry `.section-title` on an
   <h1> instead of an <h2>, because on those pages the section head IS the page
   head. The rule above is tag-qualified, so those h1s were falling through to
   the UA default (32px) while their own sibling h2s rendered at
   var(--text-headline) — measured 32px vs 46.88px at 1280, i.e. every section
   heading was 46% LARGER than the page title. Same size as an h2 head, so the
   three pages share one heading step with the rest of the site. */
h1.section-title { font-size: var(--text-headline); margin-bottom: var(--space-md); }

.lead {
  font-size: var(--text-lead);
  color: var(--color-muted);
  max-width: var(--measure);
}
/* Centered heads (.section-head--center) need the lead's own max-width
   re-centered under it; left-aligned heads inherit the block's margin. */
.section-head--center .lead {
  margin-inline: auto;
}
.contact__details .lead {
  margin-bottom: var(--space-xl);
}
.section--noir .lead { color: var(--color-on-noir-muted); }

/* Deco rule motif — echoes the crest's rule lines without repeating a card grid */
.deco-rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  color: var(--color-primary);
  max-width: 20rem;
  margin-inline: auto;
}
.deco-rule::before, .deco-rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.5;
}
.deco-rule__mark {
  width: 6px; height: 6px;
  background: currentColor;
  transform: rotate(45deg);
  flex-shrink: 0;
}
.deco-rule svg { flex-shrink: 0; }
.section--noir .deco-rule { color: var(--color-primary-on-dark); }

/* Delight: a one-time gold gleam that sweeps across the crest rule when it
   scrolls into view — light catching gilded Art Deco molding, timed to the
   "Voted #1" accolade. The left hairline brightens, the diamond glints as the
   light passes, then the right hairline brightens. JS adds .is-lit once;
   reduced-motion users never get the class and see the static rule. */
@keyframes deco-hairline-gleam {
  0%   { opacity: 0.5; }
  50%  { opacity: 1; }
  100% { opacity: 0.5; }
}
@keyframes deco-mark-glint {
  0%   { transform: rotate(45deg) scale(1);    box-shadow: 0 0 0 0 transparent; }
  45%  { transform: rotate(45deg) scale(1.55); box-shadow: 0 0 10px 2px var(--color-primary-on-dark); }
  100% { transform: rotate(45deg) scale(1);    box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: no-preference) {
  .deco-rule.is-lit::before      { animation: deco-hairline-gleam 0.7s var(--ease-out-quart) 0s; }
  .deco-rule.is-lit .deco-rule__mark { animation: deco-mark-glint 0.7s var(--ease-out-quart) 0.22s; }
  .deco-rule.is-lit::after       { animation: deco-hairline-gleam 0.7s var(--ease-out-quart) 0.38s; }
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  font-family: var(--font-body);
  font-size: var(--text-small);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  transition: background-color var(--duration-base) var(--ease-out-quart),
              border-color var(--duration-base) var(--ease-out-quart),
              color var(--duration-base) var(--ease-out-quart),
              box-shadow var(--duration-base) var(--ease-out-quart),
              transform var(--duration-fast) var(--ease-out-quart);
  min-height: 44px;
  /* No `white-space: nowrap`. At 200% text on a 390px phone it made every
     button 391-571px wide — wider than the viewport — and `html{overflow-x:
     hidden}` then deleted the overhang instead of letting anyone reach it.
     A two-line button is a smaller cost than an unreachable one. */
  text-wrap: balance;
}
.btn:active { transform: translateY(1px); }

.btn--primary {
  background: var(--color-primary);
  color: var(--color-on-primary);
  border-color: var(--color-primary);
  /* Deco double line: an inner hairline set into the fill, echoing the
     crest's rule-in-rule geometry. */
  box-shadow: inset 0 0 0 3px var(--color-primary), inset 0 0 0 4px oklch(100% 0 0 / 0.4);
}
.btn--primary:hover {
  background: var(--color-primary-deep);
  border-color: var(--color-primary-deep);
  box-shadow: inset 0 0 0 3px var(--color-primary-deep), inset 0 0 0 4px oklch(100% 0 0 / 0.35);
}

.btn--ghost-light {
  background: transparent;
  color: var(--color-on-noir);
  border-color: color-mix(in oklch, var(--color-on-noir) 45%, transparent);
}
.btn--ghost-light:hover { border-color: var(--color-on-noir); background: color-mix(in oklch, var(--color-on-noir) 10%, transparent); }

.btn--outline {
  background: transparent;
  color: var(--color-ink);
  border-color: var(--color-ink);
}
.btn--outline:hover { background: var(--color-ink); color: var(--color-on-noir); }

.btn--sm { padding: var(--space-2xs) var(--space-md); font-size: var(--text-label); min-height: 36px; }
/* The header's Request a Tour measured 38.8px tall — under the 44px bar this
   design sets itself — on all 11 pages. Growing the pill would re-proportion
   the header against the wordmark, so the PAINTED size stays and the hit area
   is extended instead, which is the thing the bar is actually about. Invisible,
   and it can't overlap anything: the header bar is taller than 44px. */
.site-header__actions .btn--sm { position: relative; }
.site-header__actions .btn--sm::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* ==========================================================================
   Header / Nav
   ========================================================================== */

.site-header {
  position: fixed;
  inset-inline: 0;
  top: 0;
  /* Belt-and-suspenders: on some engines a fixed element's percentage/inset
     width resolves against a containing block wider than the true visual
     viewport (observed as ~76px of overflow on mobile). Viewport units are
     measured against the real viewport regardless, so they win. */
  width: 100vw;
  width: 100dvw;
  z-index: var(--z-sticky);
  padding-block: var(--space-md);
  transition: background-color var(--duration-slow) var(--ease-out-quart),
              padding-block var(--duration-base) var(--ease-out-quart),
              box-shadow var(--duration-base) var(--ease-out-quart),
              border-color var(--duration-base) var(--ease-out-quart);
  border-bottom: 1px solid transparent;
}
.site-header.is-scrolled {
  background: color-mix(in oklch, var(--color-bg) 92%, transparent);
  backdrop-filter: blur(12px);
  padding-block: var(--space-xs);
  border-color: var(--color-border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.site-logo { display: flex; align-items: center; gap: var(--space-2xs); }
.site-logo img {
  height: 44px;
  width: auto;
  transform-origin: left center;
  transition: transform var(--duration-base) var(--ease-out-quart), filter var(--duration-base);
}
.site-header.is-scrolled .site-logo img { transform: scale(0.82); }
.site-header:not(.is-scrolled):not(.site-header--solid) .site-logo img {
  filter: brightness(0) invert(1) drop-shadow(0 1px 6px var(--shadow-ink-soft));
}

/* ---- Hero-less pages -----------------------------------------------------
   The header's resting state is designed to sit over a dark hero photo:
   white links, inverted logo, text-shadow. Pages with no hero (privacy,
   terms, 404) render that over a light background, i.e. white-on-white at
   1.12:1 — the nav and the mobile hamburger both vanish until the visitor
   happens to scroll. Those pages carry `site-header--solid`, which adopts the
   scrolled treatment from first paint. */
.site-header--solid {
  background: color-mix(in oklch, var(--color-bg) 92%, transparent);
  backdrop-filter: blur(12px);
  border-color: var(--color-border);
}
.site-header--solid .site-nav a { color: var(--color-ink-soft); text-shadow: none; }
.site-header--solid .nav-toggle { color: var(--color-ink); }

.site-nav {
  display: none;
  align-items: center;
  gap: var(--space-lg);
}
.site-nav ul {
  display: flex;
  align-items: center;
  gap: inherit;
  list-style: none;
}
@media (min-width: 64rem) {
  .site-nav { display: flex; }
}
/* Six items, not five (Client Portal added 2026-07-28). At 1024px the row is
   logo + nav + CTA, and the nav had exactly 618px for ~620px of content — so
   "The Venue", "Plan Your Day" and "Client Portal" each broke onto a second
   line and the header grew to 51px rows. Two fixes, both needed: labels never
   wrap mid-name, and the gap tightens through the narrow desktop band where
   the row is under pressure. Measured at 1024: 460px of labels + 5 gaps.
   Re-measure this band if a seventh item is ever added.

   KEEP THE NOWRAP. It was proposed for removal on 2026-07-29 on the grounds
   that a media-query `rem` resolves against the initial 16px rather than
   against :root, so this nav can still be the one on screen at a 200% text
   setting. Measured, and it isn't: at 390px with :root at 32px this whole
   element computes `display: none`, because 390 is still under the 1024px the
   breakpoint means. Removing the nowrap fixed nothing there and re-broke the
   1024px row described above — three labels back onto two lines. Wrapping the
   `ul` instead wrapped it to three rows at 1024. Both were reverted. */
.site-nav a { white-space: nowrap; }
/* Same 1024px squeeze, one element over. Dropping `white-space: nowrap` from
   `.btn` (it was making buttons wider than the viewport at 200% text) put the
   header's "Call / Text 479-888-2155" onto two lines here and grew the sticky
   bar from 44px to 60px. Restored for THIS button in THIS band only: below
   1024px — which is where the 200%-text failure actually lives, since a 390px
   viewport reports 390px to a media query however large the text is — it
   stays free to wrap. */
@media (min-width: 64rem) {
  .site-header__actions .btn { white-space: nowrap; }
}
@media (min-width: 64rem) and (max-width: 79.99rem) {
  .site-nav { gap: var(--space-md); }
}
.site-nav a {
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--color-on-noir);
  position: relative;
  padding-block: var(--space-3xs);
  transition: color var(--duration-base) var(--ease-out-quart);
  /* Only matters pre-scroll, when the header is transparent over the hero
     photo; harmless once is-scrolled swaps in the solid header background. */
  text-shadow: 0 1px 2px oklch(0% 0 0 / 0.5), 0 2px 12px oklch(0% 0 0 / 0.4);
}
.site-header.is-scrolled .site-nav a { color: var(--color-ink-soft); text-shadow: none; }
.site-nav a::after {
  content: '';
  position: absolute;
  left: 0; right: 100%;
  bottom: -2px;
  height: 1px;
  background: var(--color-primary);
  transition: right var(--duration-base) var(--ease-out-quart);
}
.site-nav a:hover::after, .site-nav a:focus-visible::after { right: 0; }
/* Current page: the gold underline stays put. */
.site-nav a[aria-current="page"]::after { right: 0; }
.site-nav a:hover, .site-nav a:focus-visible { color: var(--color-primary-deep); }
.site-header:not(.is-scrolled):not(.site-header--solid) .site-nav a:hover,
.site-header:not(.is-scrolled):not(.site-header--solid) .site-nav a:focus-visible { color: var(--color-primary-on-dark); }

.site-header__actions { display: flex; align-items: center; gap: var(--space-sm); }
/* Below the nav breakpoint, the header CTA and hamburger would sit side by
   side with no room to shrink. The mobile nav panel and the sticky mobile
   CTA bar both carry their own "Request a Tour", so the compact header one
   only needs to show once there's room for it alongside the full nav. */
.site-header__actions > .btn {
  display: none;
}
@media (min-width: 64rem) {
  .site-header__actions > .btn {
    display: inline-flex;
  }
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  color: var(--color-on-noir);
}
.site-header.is-scrolled .nav-toggle { color: var(--color-ink); }
@media (min-width: 64rem) {
  .nav-toggle { display: none; }
}
.nav-toggle svg { width: 22px; height: 22px; }

.mobile-nav {
  position: fixed;
  inset: 0;
  width: 100vw;
  width: 100dvw;
  z-index: var(--z-mobile-nav);
  background: var(--color-noir);
  color: var(--color-on-noir);
  display: flex;
  flex-direction: column;
  padding: var(--space-lg);
  transform: translateX(100%);
  visibility: hidden;
  transition: transform var(--duration-slow) var(--ease-out-expo), visibility 0s var(--duration-slow);
}
/* Translated-but-visible off-canvas panels can still register in the root
   scroller's width in some engines, and stay in the keyboard tab order even
   fully off-screen. visibility:hidden fixes both: no phantom horizontal
   scroll, and its links aren't reachable by Tab while the panel is closed. */
.mobile-nav.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--duration-slow) var(--ease-out-expo), visibility 0s;
}
.mobile-nav__top { display: flex; justify-content: flex-end; margin-bottom: var(--space-2xl); }
.mobile-nav__close { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; }
.mobile-nav__close svg { width: 22px; height: 22px; }
.mobile-nav ul { display: flex; flex-direction: column; gap: var(--space-lg); }
/* Scoped to the nav links only — the footer's .btn must keep its own type */
/* inline-flex + min-height, not extra gap: the drawer links measured 37px
   tall, so a thumb aimed between two of them hit neither. The 44px floor is
   the target, not the type size. */
.mobile-nav nav a {
  font-family: var(--font-display);
  font-size: var(--text-headline);
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.mobile-nav nav a[aria-current="page"] { color: var(--color-primary-on-dark); }
.mobile-nav__footer { margin-top: auto; padding-top: var(--space-xl); border-top: 1px solid var(--color-border-on-dark); }

@media (prefers-reduced-motion: reduce) {
  .mobile-nav { transition: transform 1ms; }
}

/* ==========================================================================
   Hero (homepage) + page hero (interior pages)
   ========================================================================== */

.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  color: var(--color-on-noir);
  overflow: hidden;
}
.hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
}
.hero__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 30%;
}
/* Default is a plain, fully-visible photo. The drift/zoom/vignette below only
   apply once js-scroll-fx is added (confirms GSAP + ScrollTrigger loaded and
   the user hasn't opted out of motion), so a failed or blocked script never
   leaves the hero photo stuck mid-transform or half-obscured. */
/* Resting zoom is a variable so a hero whose photograph needs more of its own
   frame can dial it down without forking the whole transform. The floor is ~1.0:
   the drift below moves the photo DOWN as the hero scrolls UP, and the resting
   overscale is the only slack keeping the top edge off-screen at small scroll
   values. 1.03 still clears it; 1.0 would seam. */
.hero.js-scroll-fx .hero__media img {
  transform: translateY(calc(var(--scroll, 0) * 26%))
             scale(calc(var(--hero-rest-scale, 1.12) + var(--scroll, 0) * 0.12));
}
.hero__vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 40%, oklch(0% 0 0 / 0.85) 100%);
  opacity: 0;
}
.hero.js-scroll-fx .hero__vignette {
  opacity: calc(var(--scroll, 0) * 0.4);
}
.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Two layers: a flat baseline wash so text stays legible even over the
     brightest part of the photo (blown-out windows), plus a bottom-heavy
     gradient for extra depth behind the stats/buttons. Verified against a
     worst-case bright photo patch, not just the token-vs-token math. */
  background:
    /* Localized left-side veil behind .hero__content: the text
       column lives in the left ~55% at desktop; this buys the subtitle its
       4.5:1 without re-darkening the whole photo. */
    linear-gradient(
      to right,
      oklch(10% 0.01 85 / 0.55) 0%,
      oklch(10% 0.01 85 / 0.38) 45%,
      oklch(10% 0.01 85 / 0.0) 78%
    ),
    linear-gradient(
      to top,
      oklch(10% 0.01 85 / 0.55) 0%,
      oklch(10% 0.01 85 / 0.32) 45%,
      oklch(10% 0.01 85 / 0.08) 75%,
      transparent 100%
    ),
    oklch(8% 0.01 85 / 0.38);
}
/* Hero focal point, per viewport shape (2026-07-27, first-dance photo).
   The hero photo is 3:2. On desktop the hero is WIDER than 3:2, so `cover`
   matches width and crops height — the Y in object-position is what steers,
   and 30% keeps the couple's heads clear of the headline.
   At tablet and phone widths the hero is TALLER than 3:2, so cover matches
   height and crops WIDTH instead: Y stops mattering and X takes over. Left
   at the default 50% the window lands on the brick pier between the couple
   and the dance reads as an unidentifiable blur. The two faces sit at ~30%
   and ~51% across the frame, so a ~41% focal point holds both in shot. */
/* the-venue's hero (draped ceiling ceremony) is an outlier: roughly 60% of the
   frame is drapery, chandelier and rafters, and the congregation sits in the
   bottom fifth. On desktop the hero is far wider than the photo, so `cover`
   matches width and crops height — at the sitewide 30% the visible band is all
   ceiling. 90% pushes the window down onto the windows, the arbor and the
   guests, which is what the section is actually about.

   Desktop only, deliberately: below 64em the hero becomes TALLER than the
   photo, cover crops width instead, Y stops steering, and the existing
   43%/37% X values take over. */
/* pricing's hero (the fabric installation) has the same shape of problem as
   the-venue's, and it is worse because that photo is a perfect SQUARE. The
   interior page hero is `clamp(24rem, 62vh, 34rem)` tall — 544px at desktop —
   so against a 1440px width the box is 2.6:1 while the photo is 1:1. `cover`
   matches width and keeps only ~38% of the frame's height, and the resting
   1.12 zoom takes that to ~34%. At the sitewide 30% that sliver lands on
   nothing but the hanging streamers: the long tables, the candles, the
   place settings — the entire reason this photo is on the PRICING page,
   where the claim is that tables, chairs and linens are included — sit below
   y≈0.70 and never appeared at all.

   70% moves the window down to roughly y 0.46–0.79: the fringe of the
   installation across the top, then the tables. Not 90% like the-venue, which
   would drop the drapery altogether — the installation is what makes the room
   look dressed, so the frame wants both, in that order.

   Desktop only, deliberately: below 64em the box turns TALLER than the square
   and cover crops width instead, so the whole height already shows and the
   phone framing — which the client says reads well — is untouched. */
@media (min-width: 64em) {
  .hero--focus-low .hero__media img { object-position: center 90%; }
  .hero--focus-tables .hero__media img { object-position: center 70%; }
}

@media (max-width: 63.99em) {
  .hero__media img { object-position: 43% 32%; }
}
@media (max-width: 44em) {
  .hero__media img { object-position: 37% 35%; }
}
/* Homepage hero (2026-07-29, the daylight twirl). The values above are the
   sitewide default, tuned for an earlier index photo and still right for the
   interior pages; this one steers itself rather than moving the default out
   from under seven other heroes.

   Frame geometry, measured off the file: groom x≈26–35%, bride x≈45–72% with
   the gown sweeping 33–75%, joined hands x≈42% y≈20%, the brick pier behind
   them 33–62%, pampas at both edges, floor from y≈83%.

   The thing to know before touching this: the homepage hero box is NOT the
   viewport. `min-height:100vh` is a floor, and the content — logo, eyebrow,
   two-line title, six-line subtitle, buttons, four stats — pushes it past it.
   Measured 1440×1120 at a 1440×900 viewport, so the box is 1.29:1 while the
   photo is 1.50:1. The box is TALLER than the photograph at every width we
   support, which means `cover` matches height and crops WIDTH — X steers and
   Y does nothing. (A previous version of this comment had that backwards.)

   So: X stays centred, because the photograph is symmetrical — window, pampas,
   brick pier, pampas, window — and shoving it sideways only breaks the frame
   the couple is standing in. The lever that matters is the resting zoom: at
   the sitewide 1.12 the crop ate the floor line and the top of the windows,
   which are the two edges that tell you the room is tall. 1.03 keeps both. */
.hero--focus-twirl { --hero-rest-scale: 1.03; }
/* The scrim is deliberately NOT lightened for this photo, and that was a close
   call worth recording. The photograph's quality is daylight — blown windows,
   pale brick, a lot of air — and the client named exactly that on the reveal
   call ("they have a soft focus to them... that puts into look what it is
   we're saying the venue is"), so the obvious move was to thin the flat wash
   from 0.38 and let more of it through. Measured at 1440×900 by sampling the
   served photo through the real overlay stack, worst pixel per element:

     wash   subtitle  stats   ghost btn  title(≥3)  eyebrow(gold)
     0.38   9.37      11.96   11.49      4.13       1.87
     0.30   8.29      11.00   10.49      3.37       1.51
     0.26   7.79      10.54   10.02      3.05       1.37

   Everything except the eyebrow has room to spare. The gold eyebrow does not:
   it fails at every setting, because oklch(0.74 …) gold sits near the middle of
   the luminance range, so darkening the bright window behind it walks the
   background TOWARD the text's own luminance and the ratio gets worse, not
   better. The previous hero measured 1.62 in that same box, so 0.38 keeps this
   swap a strict improvement (1.87) while 0.30 would have made a pre-existing
   failure worse. Aesthetics do not get to move a number that is already under
   the line — the air comes from the resting zoom above instead.
   Fixing the eyebrow properly means changing its colour or giving it its own
   plate, which is a brand decision across nine heroes, not a photo swap.
   Logged as a blocker. */
@media (max-width: 44em) {
  /* Phone: the hero measures 390×1331, so cover leaves only ~19% of the frame
     width — the two faces sit ~20% apart and cannot both fit, whatever X is.
     41% spends that slice on the bride's face, the joined hands and the gown,
     and lets the groom's dark suit run off the left edge, where it doubles as
     contrast under the title. Measured, not estimated: at 41% the visible band
     is x 0.333–0.525 of the frame. */
  .hero--focus-twirl { --hero-rest-scale: 1.02; }
  .hero--focus-twirl .hero__media img { object-position: 41% center; }
}
@media (min-width: 44.01em) and (max-width: 63.99em) {
  .hero--focus-twirl .hero__media img { object-position: 45% center; }
}
/* Tablet (44–64em): the text column runs ~85% of the width — past the
   desktop left veil's reach — so extend the veil and lift the bottom
   gradient's midpoint (measured: subtitle/stats sat at ~3.8–4:1 at 768px
   with the desktop stack alone). */
@media (min-width: 44.01em) and (max-width: 63.99em) {
  .hero__media::after {
    background:
      linear-gradient(
        to right,
        oklch(10% 0.01 85 / 0.55) 0%,
        oklch(10% 0.01 85 / 0.4) 60%,
        oklch(10% 0.01 85 / 0.0) 96%
      ),
      linear-gradient(
        to top,
        oklch(10% 0.01 85 / 0.62) 0%,
        oklch(10% 0.01 85 / 0.45) 55%,
        oklch(10% 0.01 85 / 0.15) 80%,
        transparent 100%
      ),
      oklch(8% 0.01 85 / 0.38);
  }
}
/* At phone widths the text spans nearly the full width and the eyebrow rides
   the top half of the photo: swap the left veil for a top-down veil and lift
   the bottom gradient's midpoint so subtitle and stats hold 4.5:1 without
   touching the 0.38 flat wash. */
@media (max-width: 44em) {
  .hero__media::after {
    background:
      linear-gradient(
        to bottom,
        oklch(10% 0.01 85 / 0.45) 0%,
        oklch(10% 0.01 85 / 0.12) 30%,
        transparent 45%
      ),
      linear-gradient(
        to top,
        oklch(10% 0.01 85 / 0.62) 0%,
        oklch(10% 0.01 85 / 0.45) 55%,
        oklch(10% 0.01 85 / 0.15) 80%,
        transparent 100%
      ),
      oklch(8% 0.01 85 / 0.38);
  }
}

.hero__content {
  position: relative;
  width: 100%;
  padding-block: var(--space-4xl) var(--space-2xl);
  /* Belt-and-suspenders legibility: the gradient scrim handles overall mood,
     this shadow guarantees contrast even over the brightest blown-out window
     in the photo, which no fixed overlay opacity can promise on its own.
     Inherited by every text descendant, including the ghost button label. */
  text-shadow: 0 1px 2px oklch(0% 0 0 / 0.5), 0 4px 24px oklch(0% 0 0 / 0.45);
}

/* Hero brand lockup: the wordmark logo announces the venue name up top,
   centered and large enough to read "Brick Ballroom · Siloam Springs".
   Inverted to white to read on the dark hero photo, like the header logo. */
.hero__logo {
  display: block;
  height: clamp(120px, 16vw, 190px);
  width: auto;
  margin: 0 auto var(--space-xl);
  filter: brightness(0) invert(1) drop-shadow(0 2px 10px var(--shadow-ink));
}

/* The gold eyebrow was the site's one unfixable contrast failure, and the
   long comment above the scrim explains why: oklch(74% …) gold sits in the
   MIDDLE of the luminance range, so darkening the photo behind it walks the
   background toward the text and the ratio gets worse. Measured over the
   served photos at 1440×900, worst background pixel per hero: 1.38–2.06:1
   against a 4.5:1 requirement. The text-shadow halo below genuinely helps a
   human read it, but WCAG gives shadows no credit, so the number never moved.

   Fixed 2026-07-29 by separating the two jobs the gold was doing at once.
   The TEXT carries the contrast and moves to --color-on-noir, the same white
   the hero subtitle already uses over these same photographs (measured 9.37:1
   in the scrim study above). The GOLD carries the brand and moves into
   flanking hairlines — which is not a new invention: it is exactly the
   treatment `.eyebrow` already uses on every section of the site, so the hero
   now speaks the page's own Deco vocabulary instead of a second one. Rules
   are pure decoration and 1.4.11 asks nothing of them. */
.hero__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-on-noir);
  font-weight: 600;
  margin-bottom: var(--space-md);
  /* Kept: it is why this reads well on a blown-out window in practice, even
     though it contributes nothing to the ratio. */
  text-shadow:
    0 0 2px oklch(0% 0 0 / 0.9),
    0 1px 3px oklch(0% 0 0 / 0.8),
    0 2px 14px oklch(0% 0 0 / 0.5);
}
.hero__eyebrow::before,
.hero__eyebrow::after {
  content: '';
  flex: 0 0 auto;
  width: 20px;
  height: 1px;
  background: var(--color-primary-on-dark);
  /* text-shadow doesn't reach a background, so the rule gets its own ground
     over a bright photo. */
  box-shadow: 0 1px 2px oklch(0% 0 0 / 0.65);
}
/* A hairline is a background-image in forced-colors mode and would simply
   vanish; a border is redrawn in the user's own palette. */
@media (forced-colors: active) {
  .hero__eyebrow::before,
  .hero__eyebrow::after {
    background: none;
    border-top: 1px solid currentColor;
    box-shadow: none;
  }
}

.hero__title {
  font-size: var(--text-display);
  letter-spacing: -0.02em;
  max-width: 20ch;
  margin-bottom: var(--space-md);
  text-wrap: pretty;
}
.hero__title-mask {
  display: block;
  overflow: hidden;
}
.hero__title-line {
  display: block;
}

.hero__subtitle {
  font-size: var(--text-lead);
  /* Full on-noir, not the muted tone: over photo zones the muted gray sat
     at ~2–2.5:1. The scrim's left gradient below does
     the rest of the work. */
  color: var(--color-on-noir);
  max-width: 46ch;
  margin-bottom: var(--space-xl);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-bottom: var(--space-2xl);
}

.hero__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));   /* reflow floor */
  gap: var(--space-lg) var(--space-xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-on-dark);
  max-width: 46rem;
  text-align: center;
}
@media (min-width: 40rem) {
  .hero__stats { grid-template-columns: repeat(4, minmax(0, auto)); }
}
/* Markup is valid dt→dd; the value still reads first visually.
   justify-content: flex-end packs content to the visual top of each cell,
   so the four values share one baseline even when a label wraps to a
   different line count than its neighbors. */
.hero__stats > div { display: flex; flex-direction: column-reverse; justify-content: flex-end; }
.hero__stat-value {
  /* Facts in the sans: stats are facts, not feeling — Work Sans, tabular
     numerals, per the one-voice-per-job rule. Weight held at 500 (not 600) with
     a hair of negative tracking so large numerals read tailored, not blunt. */
  font-family: var(--font-body);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.012em;
  font-size: clamp(1.375rem, 1.15rem + 0.9vw, 2rem);
  color: var(--color-primary-on-dark);
  display: block;
  line-height: 1;
  margin-bottom: var(--space-3xs);
}
.hero__stat-label {
  font-size: var(--text-label);
  /* On-noir, not muted: these sit over the photo like the subtitle. */
  color: var(--color-on-noir);
}
/* Elegant serif stat treatment (accepted via live iteration 2026-07-20).
   Deliberately overrides the facts-in-sans / one-voice-per-job rule at the
   client's explicit request: on a wedding venue the figures read warmer and
   less "product page" in the Petrona display face. Kept small and restrained
   so it stays a quiet detail, not a headline. */
.hero__stats--serif .hero__stat-value {
  font-family: var(--font-display);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: normal;
  font-size: clamp(1.15rem, 0.95rem + 0.6vw, 1.5rem);
}

/* Interior pages: same hero anatomy at a shorter height, so the fixed
   transparent header keeps working identically everywhere. */
.page-hero {
  min-height: clamp(24rem, 62vh, 34rem);
}
.page-hero .hero__title {
  font-size: var(--text-display-sm);
}
.page-hero .hero__content {
  padding-block: var(--space-4xl) var(--space-xl);
}
.page-hero .hero__subtitle { margin-bottom: 0; }

/* ==========================================================================
   What's included — quick strip
   Surfaces the "what's included" checklist right after the hero, since
   PRODUCT.md names that as the target persona's #1 priority. Otherwise it
   doesn't appear until the Pricing page.
   ========================================================================== */

.included-strip {
  background: var(--color-surface);
  border-block: 1px solid var(--color-border);
  padding-block: var(--space-lg);
}
.included-strip__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm) var(--space-2xl);
  font-size: var(--text-small);
  color: var(--color-ink-soft);
  font-weight: 500;
}
/* At desktop container width the 5 items can't fit one row (they sum wider
   than the container), so the flex wrap orphaned the last one 4+1.
   Constrain the list so it breaks as a balanced, centered 3+2 instead. */
@media (min-width: 64rem) {
  .included-strip__list { max-width: 52rem; margin-inline: auto; gap: var(--space-md) var(--space-2xl); }
}
.included-strip__list li {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}
.included-strip__icon {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  color: var(--color-primary);
}

/* ==========================================================================
   Checklist — two-column checkmark grid (venue page, pricing extras)
   ========================================================================== */

.love-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* reflow floor */
  gap: var(--space-md) var(--space-2xl);
  max-width: 56rem;
  margin-inline: auto;
}
@media (min-width: 48rem) {
  .love-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The closing argument under a love-list (pricing: the insurance gap). Shares
   the list's 56rem measure so it lines up with the column above it, separated
   by a hairline rather than a box — this is the section's last sentence, not a
   callout competing with it. Brass only on the two phrases that carry the
   claim; on noir that is enough emphasis without raising the volume. */
.savings-note {
  max-width: 56rem;
  margin: var(--space-xl) auto 0;
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border-on-dark);
  color: var(--color-on-noir-muted);
  text-wrap: pretty;
}
.savings-note strong {
  color: var(--color-primary-on-dark);
  font-weight: 600;
}
/* Even-spacing modifier (accepted via live mode 2026-07-20): equal-height rows
   so the gold checkmarks keep a uniform vertical rhythm even when an item wraps
   to a second line. Knob values baked in — row 20px, column 64px. */
.love-list--even {
  grid-auto-rows: 1fr;
  row-gap: 20px;
  column-gap: 64px;
}
.love-list li {
  display: flex;
  align-items: flex-start;
  /* Wrap, not nowrap: an inline element inside a flex item's text gets
     blockified into a flex item of its own, and on a nowrap row it then runs
     off the screen instead of wrapping with the sentence. */
  flex-wrap: wrap;
  gap: var(--space-sm);
  color: var(--color-ink-soft);
}
.love-list li > span { min-width: 0; flex: 1 1 12rem; }
.love-list__icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  color: var(--color-primary);
}
.section--noir .love-list li { color: var(--color-on-noir-muted); }
.section--noir .love-list__icon { color: var(--color-primary-on-dark); }
/* Inline links inside checklist items (e.g. the lodging cross-link) */
.love-list a { text-decoration: underline; text-underline-offset: 2px; transition: color var(--duration-base); }
.love-list a:hover { color: var(--color-primary-deep); }
.section--noir .love-list a:hover { color: var(--color-primary-on-dark); }

/* ==========================================================================
   Intro / story feature — split photo + prose (Good Bones, Meet Heather)
   ========================================================================== */

.intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* reflow floor */
  gap: var(--space-2xl);
  align-items: center;
}
@media (min-width: 56rem) {
  .intro {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr);
    gap: var(--space-4xl);
  }
  .intro--reverse { grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr); }
  .intro--reverse .intro__media { order: 2; }
}

.intro__media { position: relative; overflow: hidden; border-radius: var(--radius-md); }
.intro__media img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius-md);
}
.intro__media-frame {
  position: absolute;
  inset: var(--space-md);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-md);
  pointer-events: none;
  z-index: 1;
}
/* Double-rule Deco frame — two concentric gold hairlines echoing the button
   inset + crest divider. Accepted via live mode 2026-07-20 on the homepage
   intro photo; knob values baked in (inset 12px, gold strength 0.75). */
.intro__media-rule {
  position: absolute;
  inset: 12px;
  border: 1px solid var(--color-primary);
  border-radius: 2px;
  opacity: 0.75;
  pointer-events: none;
  z-index: 1;
}
.intro__media-rule--inner {
  inset: 19px;
  opacity: 0.51;
}

.intro h2 { margin-bottom: var(--space-md); }
.intro p + p { margin-top: var(--space-md); }
.intro p { color: var(--color-muted); max-width: var(--measure); }

.intro__actions { margin-top: var(--space-lg); display: flex; flex-wrap: wrap; gap: var(--space-md); }

/* ==========================================================================
   Four spaces mosaic — image-behind-text link tiles (homepage)
   ========================================================================== */

.spaces {
  display: grid;
  gap: var(--space-sm);
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: clamp(11rem, 24vw, 19rem);
}
.space-link {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius-md);
  color: var(--color-on-noir);
  isolation: isolate;
}
.space-link:nth-child(1) { grid-row: span 2; }
/* The fourth tile runs the full bottom row, so the mosaic closes cleanly
   instead of orphaning a tile beside an empty cell. */
.space-link:nth-child(4) { grid-column: span 2; }
.space-link img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
  transition: transform 0.7s var(--ease-out-expo);
}
.space-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to top, oklch(13% 0.008 75 / 0.78), oklch(13% 0.008 75 / 0.05) 55%);
}
.space-link:hover img, .space-link:focus-visible img { transform: scale(1.045); }
.space-link:focus-visible { outline-offset: -3px; }

.space-body {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: var(--space-md);
  text-shadow: 0 1px 2px oklch(0% 0 0 / 0.45);
}
.space-body::before {
  content: '';
  display: block;
  width: 4.5rem;
  height: 1px;
  background: var(--color-primary-on-dark);
  margin-bottom: var(--space-xs);
  /* Grown via transform, not width, so the hover never triggers layout */
  transform: scaleX(0.555);
  transform-origin: left center;
  transition: transform 0.4s var(--ease-out-expo);
}
.space-link:hover .space-body::before,
.space-link:focus-visible .space-body::before { transform: scaleX(1); }
.space-body .space-title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 500;
  line-height: 1.15;
}
.space-body .space-note {
  display: block;
  margin-top: var(--space-3xs);
  font-size: var(--text-small);
  letter-spacing: 0.04em;
  color: color-mix(in oklch, var(--color-on-noir) 88%, transparent);
}
@media (max-width: 44em) {
  .spaces { grid-template-columns: 1fr; grid-auto-rows: 15rem; }
  .space-link:nth-child(1) { grid-row: auto; }
  .space-link:nth-child(4) { grid-column: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .space-link:hover img, .space-link:focus-visible img { transform: none; }
}

/* In-page jump nav for long index pages (the venue's five spaces).

   Wayfinder behaviour (2026-07-26 enhancement pass): at ≥48rem — where the
   bar is a single row — it sticks just below the fixed header and a gold
   hairline tracks which space you're walking through (JS toggles .is-active
   + aria-current="location"; see the scrollspy block in main.js). The
   hairline is the SAME gesture as the main nav's hover/current underline
   (grows left→right via `right`), so the bar extends an existing vocabulary
   instead of inventing one. Without JS this is exactly the static bar it
   always was — the active state is additive information, never structure.
   On phones the bar wraps to two rows, so it stays in the flow (a 126px
   sticky band would eat a third of the screen). */
.page-subnav {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding-block: var(--space-sm);
}
@media (min-width: 48rem) {
  .page-subnav {
    position: sticky;
    /* Clears the fixed header in its compact is-scrolled state; by the
       time the bar can stick, the header has always already shrunk. The
       header overlays, so a small mismatch is cosmetic, not structural. */
    top: var(--header-stuck, 4.3125rem);
    z-index: var(--z-subnav);
  }
}
.page-subnav ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2xs) var(--space-lg);
}
.page-subnav a {
  display: inline-flex;
  align-items: center;
  min-height: 40px; /* tap target */
  font-size: var(--text-small);
  font-weight: 500;
  color: var(--color-muted);
  padding-block: var(--space-2xs);
  margin-block: calc(var(--space-2xs) * -1); /* keep the bar's visual rhythm */
  position: relative;
  transition: color var(--duration-base) var(--ease-out-quart);
}
/* Same hairline as .site-nav a::after — one vocabulary, two homes. */
.page-subnav a::after {
  content: '';
  position: absolute;
  left: 0; right: 100%;
  bottom: 6px; /* tucked under the text inside the 40px tap box */
  height: 1px;
  background: var(--color-primary);
  transition: right var(--duration-base) var(--ease-out-quart);
}
.page-subnav a:hover::after,
.page-subnav a:focus-visible::after,
.page-subnav a.is-active::after { right: 0; }
.page-subnav a:hover, .page-subnav a:focus-visible { color: var(--color-primary-deep); }
.page-subnav a.is-active { color: var(--color-primary-deep); }
@media (prefers-reduced-motion: reduce) {
  /* State still updates — it's information — it just stops travelling. */
  .page-subnav a::after { transition: none; }
}

/* Anchored jumps on a page with a stuck wayfinder need to land below BOTH
   fixed bars, not just the header. Scoped with :has() so plan-your-day's
   subnav-less plan items keep the tighter 6rem landing. */
@media (min-width: 48rem) {
  main:has(.page-subnav) .plan-item { scroll-margin-top: 8.75rem; }
}

/* ==========================================================================
   Plan Your Day / venue spaces — asymmetric index, not a card grid
   ========================================================================== */

.plan-list {
  display: flex;
  flex-direction: column;
}

.plan-item {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  padding-block: var(--space-2xl);
  border-top: 1px solid var(--color-border);
  /* Anchored from the four-spaces tiles: land with the row below the fixed
     header instead of underneath it. */
  scroll-margin-top: 6rem;
  /* GSAP's slide-in from-state (x offset) briefly widens scrollWidth at
     phone widths; clip it here so the root overflow-x:hidden isn't the only
     thing standing between the animation and a horizontal scrollbar. */
  overflow-x: clip;
}
.plan-item:last-child { border-bottom: 1px solid var(--color-border); }

@media (min-width: 56rem) {
  .plan-item { grid-template-columns: 1.1fr 1fr; align-items: start; gap: var(--space-3xl); }
  .plan-item:nth-child(even) .plan-item__media { order: 2; }
}

/* (.plan-item--wide lived here: the full-width treatment the alley used while it
   had no photograph. The client sent alley photos on 2026-07-28, so the alley is
   a normal two-up row again and the modifier had no remaining users.) */

.plan-item__title { font-size: var(--text-headline); margin-bottom: var(--space-sm); }

.plan-item__body { color: var(--color-muted); max-width: var(--measure); margin-bottom: var(--space-md); }
.plan-item__body a { text-decoration: underline; text-underline-offset: 2px; transition: color var(--duration-base); }
.plan-item__body a:hover { color: var(--color-primary-deep); }
.plan-item__body + .plan-item__body { margin-top: calc(-1 * var(--space-2xs)); }

.plan-item__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
}
.plan-item__facts li {
  font-size: var(--text-small);
  color: var(--color-ink-soft);
  padding-left: var(--space-sm);
  position: relative;
}
.plan-item__facts li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.55em;
  width: 4px; height: 4px;
  background: var(--color-primary);
  transform: rotate(45deg);
}

.plan-item__media {
  overflow: hidden;
  border-radius: var(--radius-md);
}
.plan-item__media img {
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  border-radius: var(--radius-md);
}
.plan-item--portrait .plan-item__media img { aspect-ratio: 4/5; }

/* ==========================================================================
   Gallery — asymmetric grid, not uniform tiles
   ========================================================================== */

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: clamp(7rem, 22vw, 9.5rem);
  grid-auto-flow: dense;
  gap: var(--space-sm);
}
@media (min-width: 40rem) {
  .gallery-grid { grid-template-columns: repeat(4, 1fr); grid-auto-rows: clamp(6rem, 9vw, 8.5rem); gap: var(--space-md); }
}

.gallery-item {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--color-surface-deep);
}
.gallery-item__trigger {
  display: block;
  width: 100%;
  height: 100%;
  position: relative;
  cursor: zoom-in;
}
/* The button fills its .gallery-item, which clips overflow for the masonry
   grid — the default outline-offset:3px would push the focus ring outside
   that box and get clipped invisible. Pull it inward instead. */
.gallery-item__trigger:focus-visible {
  outline-offset: -3px;
}
.gallery-item img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out-quart);
}
.gallery-item:hover img, .gallery-item__trigger:focus-visible img { transform: scale(1.04); }

.gallery-item__expand {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(oklch(10% 0.01 85 / 0), oklch(10% 0.01 85 / 0.35));
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out-quart);
}
.gallery-item__expand svg {
  width: 32px;
  height: 32px;
  color: var(--color-on-noir);
  filter: drop-shadow(0 1px 4px oklch(0% 0 0 / 0.5));
}
.gallery-item:hover .gallery-item__expand,
.gallery-item__trigger:focus-visible .gallery-item__expand {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .gallery-item:hover img, .gallery-item__trigger:focus-visible img { transform: none; }
}

/* Row spans are the masonry mechanism; aspect-ratio is not used here since it
   conflicts with spanning rows of a fixed track size. */
.gallery-item--tall { grid-row: span 3; }
.gallery-item--wide { grid-column: span 2; grid-row: span 2; }
.gallery-item--square { grid-row: span 2; }
@media (max-width: 39.99rem) {
  .gallery-item--wide { grid-column: span 2; grid-row: span 2; }
}

/* Homepage gallery: two 3-row talls can't auto-pack with 2-row spans in a
   4-column grid without leaving a hole. Stack the talls
   in the last column explicitly and hand-place the rest — a perfect 6-row
   fit for this page's fixed set of 8 photos. */
@media (min-width: 40rem) {
  .gallery-grid--home > :nth-child(1) { grid-column: 1 / 3; grid-row: 1 / 3; }
  .gallery-grid--home > :nth-child(2) { grid-column: 3; grid-row: 1 / 3; }
  .gallery-grid--home > :nth-child(3) { grid-column: 1; grid-row: 3 / 5; }
  .gallery-grid--home > :nth-child(4) { grid-column: 3; grid-row: 5 / 7; }
  .gallery-grid--home > :nth-child(5) { grid-column: 4; grid-row: 1 / 4; }
  .gallery-grid--home > :nth-child(6) { grid-column: 4; grid-row: 4 / 7; }
  .gallery-grid--home > :nth-child(7) { grid-column: 2 / 4; grid-row: 3 / 5; }
  .gallery-grid--home > :nth-child(8) { grid-column: 1 / 3; grid-row: 5 / 7; }
}

/* Weddings gallery: same explicit-placement treatment — its 6 photos'
   natural spans sum to 17 cells, which no auto-flow can pack into 4 columns
   without a hole. Hand-placed 4×5 pack: tall, a 2×3 feature, squares and a
   wide closing the bottom row flush. */
@media (min-width: 40rem) {
  .gallery-grid--weddings > :nth-child(1) { grid-column: 1; grid-row: 1 / 4; }
  .gallery-grid--weddings > :nth-child(2) { grid-column: 2 / 4; grid-row: 1 / 4; }
  .gallery-grid--weddings > :nth-child(3) { grid-column: 4; grid-row: 1 / 3; }
  .gallery-grid--weddings > :nth-child(4) { grid-column: 4; grid-row: 3 / 6; }
  .gallery-grid--weddings > :nth-child(5) { grid-column: 1 / 3; grid-row: 4 / 6; }
  .gallery-grid--weddings > :nth-child(6) { grid-column: 3; grid-row: 4 / 6; }
}

/* Phone (2-col) flow, both galleries: explicit packs so dense auto-flow
   can never strand an empty cell. Wides run full width; the single columns
   are balanced so both close on the same row. */
@media (max-width: 39.99rem) {
  .gallery-grid--home > :nth-child(1) { grid-column: 1 / 3; grid-row: 1 / 3; }
  .gallery-grid--home > :nth-child(2) { grid-column: 1; grid-row: 3 / 5; }
  .gallery-grid--home > :nth-child(3) { grid-column: 1; grid-row: 5 / 7; }
  .gallery-grid--home > :nth-child(4) { grid-column: 1; grid-row: 7 / 9; }
  .gallery-grid--home > :nth-child(5) { grid-column: 2; grid-row: 3 / 6; }
  .gallery-grid--home > :nth-child(6) { grid-column: 2; grid-row: 6 / 9; }
  .gallery-grid--home > :nth-child(7) { grid-column: 1 / 3; grid-row: 9 / 11; }
  .gallery-grid--home > :nth-child(8) { grid-column: 1 / 3; grid-row: 11 / 13; }

  .gallery-grid--weddings > :nth-child(1) { grid-column: 1; grid-row: 1 / 4; }
  .gallery-grid--weddings > :nth-child(2) { grid-column: 1 / 3; grid-row: 6 / 8; }
  .gallery-grid--weddings > :nth-child(3) { grid-column: 2; grid-row: 1 / 3; }
  .gallery-grid--weddings > :nth-child(4) { grid-column: 2; grid-row: 3 / 6; }
  .gallery-grid--weddings > :nth-child(5) { grid-column: 1 / 3; grid-row: 8 / 10; }
  .gallery-grid--weddings > :nth-child(6) { grid-column: 1; grid-row: 4 / 6; }
}

.gallery-cta { text-align: center; margin-top: var(--space-2xl); }

/* ==========================================================================
   Packages / pricing
   ========================================================================== */

.pricing {
  display: grid;
  gap: var(--space-2xl);
}
/* Anchor target for "Get the Brochure" links elsewhere on the page. */
#brochure { scroll-margin-top: 5rem; }
@media (min-width: 56rem) {
  /* 1.3fr/1fr, not 1fr/1fr: the price is the one thing this section's own
     headline ("Simple pricing") promises to make unmistakable — an even
     split let the terms list compete with it for first glance. */
  .pricing { grid-template-columns: 1.3fr 1fr; align-items: start; gap: var(--space-4xl); }
}

.pricing__card {
  background: var(--color-noir);
  color: var(--color-on-noir);
  border-radius: var(--radius-md);
  padding: var(--space-2xl);
  position: relative;
  overflow: hidden;
}
/* Deco double hairline along the card's top edge — the CTA band's signature,
   carried into the one other place the brand goes full noir. */
.pricing__card::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: var(--color-primary);
}
.pricing__card::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 5px;
  height: 1px;
  background: color-mix(in oklch, var(--color-primary) 35%, transparent);
}
.pricing__from { font-size: var(--text-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-on-noir-muted); margin-bottom: var(--space-2xs); }
/* Facts in the sans: the amount is the site's most
   load-bearing fact — Work Sans 600, tabular numerals. */
.pricing__amount { font-family: var(--font-body); font-weight: 600; font-variant-numeric: tabular-nums; font-size: clamp(2.5rem, 1.9rem + 2.6vw, 4.5rem); color: var(--color-primary-on-dark); line-height: 1; margin-bottom: var(--space-md); letter-spacing: -0.01em; }
.pricing__amount sup { font-size: 0.35em; top: -1.4em; }
.pricing__includes { display: flex; flex-direction: column; gap: var(--space-sm); margin-block: var(--space-lg); }
.pricing__includes li { display: flex; gap: var(--space-sm); font-size: var(--text-small); color: var(--color-on-noir-muted); }
.pricing__includes svg { flex-shrink: 0; width: 18px; height: 18px; color: var(--color-primary-on-dark); margin-top: 2px; }
.pricing__note {
  font-size: var(--text-small);
  color: var(--color-on-noir-muted);
  max-width: 44ch;
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-on-dark);
}
.pricing__actions { display: flex; flex-wrap: wrap; gap: var(--space-md); margin-top: var(--space-lg); }

/* The brochure capture inside this card is our own form — see #brochure-form
   further down. It was a High Level iframe until 2026-07-29; that embed and
   all of its sizing/handshake CSS are gone with it. */

/* The clipped-CTA fix, part 2: at narrow phones give the card's content
   more room and let any remaining action wrap instead of slicing. */
@media (max-width: 30rem) {
  .pricing__card { padding: var(--space-lg); }
  .pricing__actions .btn { white-space: normal; width: 100%; text-align: center; }
}

/* h2 for a sound heading outline; styled at title size on purpose
   so the hierarchy change is invisible. */
.pricing__terms h2 { margin-bottom: var(--space-md); font-size: var(--text-title); }
.terms-list { display: flex; flex-direction: column; }
.terms-list > div {
  padding-block: var(--space-md);
  border-top: 1px solid var(--color-border);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3xs);
}
.terms-list > div:last-child { padding-bottom: 0; }
.terms-list dt { font-weight: 600; font-size: var(--text-small); }
.terms-list dd { color: var(--color-muted); font-size: var(--text-small); }

/* ==========================================================================
   Spec sheet — the hard numbers as a double-ruled table (pricing page).
   Facts stay in the sans, right-aligned like a ledger.
   ========================================================================== */

.spec-sheet {
  max-width: var(--container-narrow);
  margin-inline: auto;
  width: 100%;
  border-collapse: collapse;
  border-top: 2px solid var(--color-ink);
  border-bottom: 2px solid var(--color-ink);
}
.spec-sheet caption {
  caption-side: top;
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-title);
  color: var(--color-muted);
  padding-bottom: var(--space-md);
  text-align: left;
}
.spec-sheet th, .spec-sheet td {
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border);
  font-size: var(--text-small);
  vertical-align: baseline;
}
.spec-sheet tbody tr:first-child th, .spec-sheet tbody tr:first-child td { border-top: none; }
.spec-sheet th {
  text-align: left;
  font-weight: 500;
  color: var(--color-ink-soft);
  padding-right: var(--space-md);
}
.spec-sheet td {
  text-align: right;
  font-weight: 600;
  color: var(--color-ink);
  font-variant-numeric: tabular-nums;
}
/* The value column used to be `white-space: nowrap`, with a max-width: 22.5rem
   override to release it on small phones. That override could never fire in
   the case it was written for: a media-query `rem` always resolves against the
   initial 16px, never against :root, so at 200% text it still reported a
   390px viewport as 390px — not 195px — and the nowrap stayed on. The value
   now wraps at every size, and the scroll region below catches whatever is
   still too wide. */

/* A labelled, focusable scroll region around the table. At very large text the
   spec sheet can outgrow a phone even with everything wrapping, and
   `html{overflow-x:hidden}` turns that overflow into silently deleted
   content — the entire value column (capacity, square footages, from $3,800,
   the 30/40/30 schedule) unreachable, with window.scrollX pinned at 0.
   tabindex makes it scrollable by keyboard; role+label say what it is. */
.spec-sheet__scroll {
  max-width: var(--container-narrow);
  margin-inline: auto;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.spec-sheet__scroll:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 4px;
}

/* ==========================================================================
   Booking steps — a real ordered sequence, so the numbers carry meaning
   ========================================================================== */

.steps {
  counter-reset: step;
  display: grid;
  gap: var(--space-lg);
  max-width: var(--container-narrow);
  margin-inline: auto;
}
@media (min-width: 48rem) {
  .steps { grid-template-columns: repeat(2, 1fr); gap: var(--space-xl) var(--space-2xl); }
  /* Three-step sequences (lodging) fill a row instead of orphaning step 03 */
  .steps--three { grid-template-columns: repeat(3, 1fr); gap: var(--space-xl) var(--space-xl); }
}
.steps--three { max-width: 60rem; }
.steps li {
  counter-increment: step;
  position: relative;
  padding-left: 3.25rem;
}
.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: -0.15em;
  font-family: var(--font-display);
  font-size: var(--text-title);
  color: var(--color-primary-deep);
  font-variant-numeric: tabular-nums;
}
.steps li::after {
  content: '';
  position: absolute;
  left: 0;
  top: 2.1rem;
  width: 2rem;
  height: 1px;
  background: var(--color-primary);
  opacity: 0.5;
}
.steps .steps__title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 500;
  margin-bottom: var(--space-2xs);
  color: var(--color-ink);
}
.steps p { font-size: var(--text-small); color: var(--color-muted); max-width: 42ch; }

/* Delight (lodging): the weekend, drawn as a timeline — bound to the scroll.

   This list is the page's thesis ("the wedding that starts the night before"),
   so at three-column widths the three orphan tick rules become one gilded
   molding line above the row with a Deco diamond standing on it at each step.
   The line sweeps left to right and each step's numeral and diamond pop as the
   sweep reaches it. Same crest-and-diamond vocabulary as the rail dots and the
   tour marks, so no new gesture is invented.

   The whole sequence is SCRUBBED TO SCROLL POSITION, like the accolade gleam on
   the homepage: scrolling down sweeps it, scrolling back up reverses it. That is
   why nothing here is a @keyframes animation. GSAP cannot address a pseudo-
   element, so it drives two custom properties instead and CSS decides what they
   mean: `--sweep` (0-1) scales the line, and a per-step `--pop` (0-1, allowed to
   overshoot) scales and fades the numeral and its diamond. All the timing lives
   in main.js.

   Two mistakes are buried in this component's history, both worth keeping:
   (1) the first version animated the 32px ticks in place — a 1px hairline at 50%
   opacity growing 32px, plus a glint between two golds twelve lightness points
   apart. It fired correctly and was invisible. Amplitude is the point.
   (2) the second used an even 60/440/820ms stagger against a front-loaded
   ease-out, which lit steps two and three long after the line had passed them.
   The scrubbed sweep is linear (`ease: 'none'`), so arrival is now simply
   proportional to position and that class of bug can't come back.

   Visible by default: both properties default to 1, so with GSAP dead, JS dead,
   reduced motion on, or a headless render, the line is fully drawn and every
   numeral and diamond sits at full size and opacity. The animation can only
   ever take something away from a finished state, never leave it blank. */
.steps--reveal { --timeline-gap: 3rem; --sweep: 1; }
.steps--reveal li { --pop: 1; }

/* The numerals pop at every width — on a phone the steps stack, and each one
   still arrives on its own. The numeral takes the overshoot at FULL amplitude,
   same as its diamond: an earlier `calc(0.8 + 0.2 * var(--pop))` compressed a
   1.15 overshoot down to 1.03, which is the same "technically animating,
   visually nothing" mistake this component already made once. clamp() floors
   the scale so a numeral never collapses to a point, and caps it so a future
   ease with a wilder overshoot can't blow the layout out. */
.steps--reveal li::before {
  scale: clamp(0.6, var(--pop), 1.3);
  opacity: clamp(0, var(--pop), 1);
}

/* And every step's tick rule draws left to right on the same `--pop`. This is
   the general form of the gesture, and it's what carries it to grids the
   sweeping line can't serve: a four-step list is a 2x2 at most widths, and a
   single line has no one row to travel along. The numeral bounces, its rule
   draws, both scrubbed to scroll — same vocabulary, no layout change required.
   Fully drawn at the `--pop: 1` default, as ever. */
.steps--reveal li::after {
  transform: scaleX(clamp(0, var(--pop), 1));
}

@media (min-width: 48rem) {
  /* The sweeping line is scoped to `.steps--three`, the only list that is
     guaranteed to be a single row here. Applying it to any `.steps--reveal`
     put a full-width rule above a 2x2 grid with diamonds floating over the
     first row only. */
  .steps--reveal.steps--three { position: relative; padding-top: var(--timeline-gap); }
  .steps--reveal.steps--three::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: var(--color-primary);
    opacity: 0.45;
    transform-origin: left center;
    transform: scaleX(var(--sweep));
  }
  /* Each step's tick leaves the gutter and becomes a mark on the line. The
     mark scales as a whole (`scale`), so the general rule's horizontal-only
     `transform: scaleX()` is cleared — otherwise the two compose and the
     diamond gets squashed on one axis. */
  .steps--reveal.steps--three li::after {
    top: calc(-1 * var(--timeline-gap) - 3px);
    width: 7px;
    height: 7px;
    rotate: 45deg;
    transform: none;
    scale: var(--pop);
    opacity: clamp(0, var(--pop), 1);
  }
}

/* ==========================================================================
   Social proof — press + stat + testimonials
   ========================================================================== */

.press-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xl) var(--space-2xl);
  align-items: center;
  justify-content: center;
  padding-block: var(--space-lg);
}
/* Below the desktop breakpoint the four mentions wrapped unevenly (1-2-1 on
   phones, 3+1 at 768); a 2x2 grid keeps the row
   balanced without shrinking the type. One row of four fits from 64em up. */
@media (max-width: 63.99em) {
  .press-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));   /* reflow floor */
    gap: var(--space-lg) var(--space-md);
  }
}
.press-row__item {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-title);
  color: var(--color-muted);
  text-align: center;
  line-height: 1.35;
}
.press-row__item strong { display: block; font-style: normal; font-family: var(--font-body); font-size: var(--text-label); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-ink); margin-top: var(--space-3xs); }
.section--noir .press-row__item { color: var(--color-on-noir-muted); }
.section--noir .press-row__item strong { color: var(--color-primary-on-dark); }

.center { text-align: center; }

/* The accolade band's one big line */
.accolade-title {
  font-size: var(--text-headline);
  margin-top: var(--space-md);
  color: var(--color-on-noir);
}
/* Delight: a scroll-linked gold gleam swept across the accolade headline.

   REBUILT 2026-07-29. This used to be `background-clip: text` +
   `-webkit-text-fill-color: transparent` — gradient text, one of the design
   system's absolute bans, and here it was a genuine hazard rather than a
   matter of taste: the letters' RESTING STATE was `transparent`, legible only
   because a background-image was painting through them. In Windows High
   Contrast the browser strips background-images and `transparent` can
   survive, so "Voted #1 venue in Arkansas." was one forced-colors setting
   away from being invisible. The stylesheet had no forced-colors guard
   anywhere to catch it.

   The mechanism now: the text is ALWAYS opaque --color-on-noir, and the gleam
   is a separate overlay in ::after with `mix-blend-mode: darken`. On this
   band that blend does exactly what's wanted and nothing else — per channel
   it takes the darker of the two, so the near-black background (11,9,5) is
   already darker than any gold and comes back untouched, while the light
   letters (245,244,240) are pulled down to gold (207,163,84) wherever the
   hotspot sits. No clipping to glyphs, no transparent text, and the worst
   case if the overlay never paints is a plain white headline. */
.accolade-title {
  position: relative;
  /* Driven by GSAP (scrub). The default leaves the hotspot parked off the
     left edge, so a dead script means "gleam hasn't happened", not "headline
     is broken". */
  --shine-pos: 0%;
}
@media (prefers-reduced-motion: no-preference) {
  .accolade-title.is-shine::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: linear-gradient(100deg,
      #fff 0%, #fff 36%,
      var(--color-primary-on-dark) 43%, var(--color-primary) 50%, var(--color-primary-on-dark) 57%,
      #fff 64%, #fff 100%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    background-position: var(--shine-pos) 0;
    /* White is the identity for `darken`, so everything outside the gold
       hotspot leaves the headline exactly as it was. */
    mix-blend-mode: darken;
  }
}
/* Forced colours: the user's palette is the point, and a blended gold overlay
   is decoration that would only get in its way. The headline underneath is
   opaque text, so removing this loses nothing but the gleam. */
@media (forced-colors: active) {
  .accolade-title.is-shine::after { display: none; }
}
.accolade-source {
  color: var(--color-on-noir-muted);
  font-size: var(--text-small);
  margin-top: var(--space-2xs);
}

.testimonials {
  display: grid;
  /* Reflow floor: the implicit track resolved to the longest quote's
     min-content — 431px inside a 294px column at 200% text. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
}
@media (min-width: 56rem) {
  .testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.testimonial {
  padding: var(--space-lg);
  border-top: 2px solid var(--color-primary);
  background: var(--color-bg);
  /* Quotes run 3 or 4 lines, so attributions were landing on three different
     baselines across the row. Letting the quote absorb the slack pins every
     cite to a common line. */
  display: flex;
  flex-direction: column;
  height: 100%;
}
.testimonial blockquote { flex: 1; }
.section--surface .testimonial { background: var(--color-bg); }
.testimonial blockquote { font-size: var(--text-lead); font-family: var(--font-display); font-style: italic; margin-bottom: var(--space-md); }
.testimonial cite {
  display: flex;
  align-items: center;
  /* The five stars are one unbreakable token; on a nowrap row they pushed
     73px off a phone at 200% text. Let the rating drop to its own line. */
  flex-wrap: wrap;
  gap: var(--space-xs);
  font-style: normal;
  font-size: var(--text-small);
  font-weight: 600;
  color: var(--color-muted);
}
.testimonial__stars { color: var(--color-primary); letter-spacing: 0.05em; }

/* Single large closing quote (tour page) */
.quote-solo {
  max-width: var(--container-narrow);
  margin-inline: auto;
  text-align: center;
}
.quote-solo blockquote {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-title);
  color: var(--color-ink);
  margin-block: var(--space-md);
}
.quote-solo cite {
  font-style: normal;
  font-size: var(--text-small);
  font-weight: 600;
  color: var(--color-muted);
}

/* ==========================================================================
   404
   Wayfinding, not decoration: a visitor here wanted something specific, so the
   page's job is to name the five places that thing could be. Hairline rows
   rather than cards — this is a directory, not a set of offers.
   ========================================================================== */

.notfound .container { max-width: var(--container-narrow); }
.notfound__links { margin: var(--space-2xl) 0 0; padding: 0; list-style: none; }
.notfound__links li { border-top: 1px solid var(--color-border); }
.notfound__links li:last-child { border-bottom: 1px solid var(--color-border); }

.notfound__links a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-md);
  padding: var(--space-md) 0;
  min-height: 2.75rem; /* 44px tap target */
  transition: color var(--duration-base);
}
/* Wrapping flex gave every row a different description start-x (measured
   116–196px at 1280) and dropped only the fifth description onto its own
   line, so a five-row directory read as five differently-built rows. A
   two-track grid aligns the descriptions into a column, which is the whole
   point of a directory.
   The first track is a FIXED 12rem, not max-content: each <a> is its own grid
   container, so a content-sized track resolves per row and aligns nothing
   (tried it, measured it, it changed nothing). 12rem clears the longest label
   — "Plan Your Day" at the --text-title ceiling of 28px measures ~182px — and
   leaves 328px for the descriptions, enough for the longest on one line.
   Below 42rem the container is narrower than that budget, so it stays flex
   and stacks label over description, which is correct on a phone. */
@media (min-width: 42rem) {
  .notfound__links a {
    display: grid;
    grid-template-columns: 12rem 1fr;
    align-items: baseline;
    column-gap: var(--space-md);
  }
}
/* One step under the page title, and on a display line-height. At
   --text-headline these were 46.88px Petrona inheriting the *body* 1.6
   line-height (<strong> is not a heading, so the h1–h4 line-height:1.1 never
   reached it) — five 75px-tall rows of display serif in what is meant to be a
   quiet directory. --text-title + 1.15 makes each row a destination, not a
   headline, and the row height comes back to the 44px tap floor honestly. */
.notfound__links strong {
  font-family: var(--font-display);
  font-size: var(--text-title);
  line-height: 1.15;
  font-weight: 500;
}
.notfound__links span { color: var(--color-muted); font-size: var(--text-small); }
.notfound__links a:hover strong,
.notfound__links a:focus-visible strong { color: var(--color-primary-deep); }

.notfound__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-2xl);
}
.notfound__help { margin-top: var(--space-lg); color: var(--color-muted); font-size: var(--text-small); }
.notfound__help a { text-decoration: underline; text-underline-offset: 2px; }

/* ==========================================================================
   Legal pages (privacy-policy, terms)
   Utility pages, deliberately quiet: one narrow column, generous leading,
   no ornament. They exist to be read and to satisfy the SMS-consent links
   on the brochure form — not to sell anything.
   ========================================================================== */

.legal .container { max-width: var(--container-narrow); }
.legal__head { margin-bottom: var(--space-2xl); }
/* The timestamp is a subtitle OF the title, so it belongs in the same tight
   group. h1.section-title carries the shared --space-md heading gap, which
   with the timestamp's own margin-top opened 36px between the two — a section
   separation where an 12px sibling gap belongs. */
.legal__head h1 { margin-bottom: 0; }
/* Must out-specify `.legal p` below (0-1-1) — this IS a <p>, and without the
   element qualifier the prose colour rule would repaint the timestamp too. */
.legal p.legal__updated {
  font-family: var(--font-body);
  font-size: var(--text-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
  margin-top: var(--space-xs);
  margin-bottom: 0;
}
/* Section headings sit ONE step under the page title, not above it: a legal
   document's hierarchy is h1 > h2 > prose, and --text-headline here put every
   "What we collect" at 46.88px against a 32px page title. --text-title (26.4px
   at 1280) gives a 46.9 / 26.4 / 16 three-step scale that reads at a squint. */
.legal h2 {
  font-size: var(--text-title);
  margin-top: var(--space-2xl);
  margin-bottom: var(--space-sm);
}
/* Prose is prose, not caption. --color-muted is the site's secondary role;
   setting 100% of a page's reading copy in it is the "elegant light gray body"
   tell, and these two pages are nothing BUT reading copy. */
.legal p, .legal li { color: var(--color-ink-soft); }
.legal p { margin-bottom: var(--space-md); }
.legal ul { margin: 0 0 var(--space-md) var(--space-lg); }
.legal li { margin-bottom: var(--space-xs); list-style: disc; }
.legal strong { color: var(--color-ink); font-weight: 600; }
.legal a { text-decoration: underline; text-underline-offset: 2px; }
.legal a:hover { color: var(--color-primary-deep); }

/* ==========================================================================
   The film (homepage)
   Native <video controls> inside the same gold-hairline frame the .intro
   media uses, so the film reads as part of the same system rather than an
   embed dropped on the page. The rule is a sibling overlay with
   pointer-events:none so it can never eat a click on the video controls.
   ========================================================================== */

/* The overture above the film (2026-07-27): the section head and a hand-drawn
   looped arrow pointing down at the frame. All motion is expressed through
   custom properties that DEFAULT TO 1, so the resting state is the finished
   drawing — GSAP (see main.js) can only ever wind it back and let scroll
   redraw it. --pop fades/rises the title (smooth, no overshoot — the title is
   a section head, not a numeral); --draw-tail/--draw-head run the pen along
   each path via pathLength="1" + dashoffset, so no measuring is ever needed. */
.film-intro {
  --draw-tail: 1;
  --draw-head: 1;
  max-width: var(--container-narrow);
  margin-inline: auto;
  margin-bottom: var(--space-md);
  text-align: center;
}

.film-intro__title {
  --pop: 1;
  font-size: var(--text-headline);
  opacity: min(var(--pop), 1);
  transform: translateY(calc((1 - var(--pop)) * 28px));
}

/* The one place the site allows itself a doodle: gold, loop mid-tail like a
   signature flourish. overflow visible so the round caps never clip at the
   viewBox edge. The opacity ramp (fully opaque by 8% drawn) hides the round-
   cap dot that an undrawn dashed path can paint at its start point. */
.film-intro__arrow {
  display: block;
  width: clamp(3.5rem, 4rem + 2vw, 6rem);
  height: auto;
  margin: var(--space-xs) auto 0;
  overflow: visible;
  color: var(--color-primary);
}

.film-intro__arrow path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1 1;
}

.film-intro__arrow-tail {
  stroke-dashoffset: calc(1 - var(--draw-tail));
  opacity: min(calc(var(--draw-tail) * 12), 1);
}

.film-intro__arrow-head {
  stroke-dashoffset: calc(1 - var(--draw-head));
  opacity: min(calc(var(--draw-head) * 12), 1);
}

/* A MAT, not an overlay. The first version drew the gold hairline *on top of
   the footage* at inset var(--space-md): measured at 1280 it crossed the
   couple mid-frame and ran straight through the native control bar, which is
   why it needed a hover-fade hack to stop competing with the scrubber — a
   fade that quietly admitted the rule was in the wrong place. It now sits in a
   noir mat around the film, so it frames the picture the way a mount frames a
   print: warm page → noir plate → gold rule → film, three concentric edges,
   and the rule can never touch the image or the controls in any play state.
   The hover rule is gone with the problem it was patching. */
.film__frame {
  position: relative;
  max-width: 62rem;
  margin-inline: auto;
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  background: var(--color-noir);
}

.film__video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  /* `width`/`height` attrs are present to reserve space and stop layout shift;
     with both axes non-auto, aspect-ratio is silently ignored unless height
     is released. Same trap documented in DESIGN.md for the lightbox thumbs. */
}

/* Sits inside the mat: 10px in from the plate edge, 6px out from the film.
   Weighted toward the picture, which is how a mat line is cut. */
.film__frame-rule {
  position: absolute;
  inset: 10px;
  border: 1px solid var(--color-primary-on-dark);
  border-radius: var(--radius-sm);
  pointer-events: none;
  z-index: 1;
  opacity: 0.55;
}

@media (min-width: 48rem) {
  .film__frame { padding: var(--space-lg); }
  .film__frame-rule { inset: 20px; }
}

.film__fallback { color: var(--color-on-noir); padding: var(--space-md); }
.film__fallback a { color: var(--color-primary-on-dark); text-decoration: underline; }

/* (The former `@media (max-width: 47.99rem) { .film__frame-rule }` override is
   gone: the mat is now mobile-first — base inset 10px inside a --space-sm mat,
   stepping up to 20px inside a --space-lg mat at 48rem.) */

/* ==========================================================================
   Afterward — the two post-wedding gestures (plan-your-day)
   Two text blocks, hairline-divided, no card chrome (cards would make two
   human gestures read as two product features). Lives on a noir band, so the
   rule uses the on-dark gold per DESIGN.md's gold-on-dark rule.
   ========================================================================== */

.afterward {
  max-width: var(--container-narrow);
  margin-inline: auto;
  display: grid;
  gap: var(--space-xl);
}

/* Selector must out-specify `.section--noir h3` (0-1-1), which would otherwise
   repaint this gold label white. Third time this class of bug has bitten this
   build — see the .stay-card__specs and .stay-row__specs notes in DESIGN.md. */
.section--noir .afterward__title {
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-primary-on-dark);
  margin-bottom: var(--space-2xs);
}

.afterward p { max-width: var(--measure); }

/* Stacked: keep the gesture. Without this the "hairline-divided" idea only
   existed on desktop and the phone got two paragraphs and a gap. */
.afterward__item + .afterward__item {
  padding-top: var(--space-xl);
  border-top: 1px solid color-mix(in oklch, var(--color-on-noir) 18%, transparent);
}

/* Two columns only once there is room for two real measures.
   This used to split at 48rem inside a --container-narrow (42rem) band, and
   --container-narrow is sized for ONE column of prose: measured at 1280 the
   second column resolved to 191px of text = 19.2ch, i.e. three words a line
   for nine lines, while the first ran four lines — the pair read as a mistake
   rather than a pairing. The band widens to 58rem for the two-column case
   (each column ~384px / ~38ch measured) and the gutter is split evenly on
   both sides of the rule instead of hanging entirely off the second item,
   so the two gestures are the same width as each other. */
@media (min-width: 64rem) {
  .afterward { max-width: 58rem; grid-template-columns: repeat(2, 1fr); gap: 0; }
  .afterward__item:first-child { padding-right: var(--space-2xl); }
  .afterward__item + .afterward__item {
    padding-top: 0;
    border-top: 0;
    padding-left: var(--space-2xl);
    border-left: 1px solid color-mix(in oklch, var(--color-on-noir) 18%, transparent);
  }
}

/* ==========================================================================
   FAQ
   ========================================================================== */

.faq-list { max-width: var(--container-narrow); margin-inline: auto; }
.faq-item { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-2xs); }
.faq-item:first-child { border-top: 1px solid var(--color-border); }

.faq-item__trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-lg);
  text-align: left;
  font-size: var(--text-title);
  font-family: var(--font-display);
}
.faq-item__icon {
  flex-shrink: 0;
  width: 28px; height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.faq-item__icon::before, .faq-item__icon::after {
  content: '';
  position: absolute;
  background: var(--color-primary);
  transition: transform var(--duration-base) var(--ease-out-quart), opacity var(--duration-base);
}
.faq-item__icon::before { width: 14px; height: 1px; }
.faq-item__icon::after { width: 1px; height: 14px; }
.faq-item__trigger[aria-expanded="true"] .faq-item__icon::after { transform: rotate(90deg); opacity: 0; }

.faq-item__panel {
  overflow: hidden;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--duration-slow) var(--ease-out-quart);
}
.faq-item__panel.is-open { grid-template-rows: 1fr; }

/* A zero-height row hides an answer visually but leaves it in the
   accessibility tree, so a screen reader read all thirteen answers straight
   through while `aria-expanded="false"` claimed otherwise. `visibility` is
   what actually removes it. Delayed by the collapse duration on close so the
   animation still plays; instant on open. The no-JS override lives in each
   FAQ page's <noscript> block — collapsing must never make content
   unreachable when the accordion can't be operated. */
.faq-item__panel-inner {
  overflow: hidden;
  visibility: hidden;
  transition: visibility 0s linear var(--duration-slow);
}
.faq-item__panel.is-open .faq-item__panel-inner {
  visibility: visible;
  transition-delay: 0s;
}
.faq-item__panel p { color: var(--color-muted); max-width: var(--measure); padding-top: var(--space-2xs); padding-bottom: var(--space-lg); }

/* Parking map inside an FAQ answer.
   The source is a 478px-wide photograph of a printed map — the only copy that
   exists. max-width is pinned to its true pixel width so it is never stretched
   past its resolution; the answer text carries the actual fact, and the map is
   corroboration rather than the primary way of reading it. When a bigger scan
   arrives, raise the max-width and nothing else changes. */
.faq-map {
  margin: 0;
  padding-bottom: var(--space-lg);
  max-width: 478px;
}
.faq-map img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.faq-map figcaption {
  margin-top: var(--space-2xs);
  font-size: var(--text-label);
  color: var(--color-muted);
}
/* The paragraph above the map already carries the section's bottom spacing. */
.faq-item__panel .faq-map + p,
.faq-item__panel p:has(+ .faq-map) { padding-bottom: var(--space-sm); }

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

/* ==========================================================================
   Full-bleed photo bands — downtown feature & quote interludes
   ========================================================================== */

/* The band's height follows its copy rather than a fixed 32rem. That constant
   was wrong in both directions at once: at 1280 the content is short, so 217px
   of empty photo opened above text pinned to the bottom edge; at 390 the same
   copy runs nine lines and squeezed the photograph down to 70px, 14% of the
   band. Padding now sets the composition — a deliberate strip of photograph
   above the text, less beneath it — so the proportion holds at every width. */
.downtown {
  position: relative;
  display: grid;
  /* Reflow floor: without this the implicit track sat at the copy's
     min-content (455px inside a 390px phone at 200% text). */
  grid-template-columns: minmax(0, 1fr);
  align-items: end;
  overflow: hidden;
  min-height: 24rem;
}

@media (min-width: 44em) {
  /* Wider viewports get a taller band, but driven by ratio rather than a
     constant, so the photograph keeps its presence without opening a gap. */
  .downtown__content { padding-block: var(--space-3xl) var(--space-2xl); }
}
.downtown__media { position: absolute; inset: 0; z-index: -1; }
.downtown__media img { width: 100%; height: 100%; object-fit: cover; }
.downtown__media::after {
  content: '';
  position: absolute; inset: 0;
  /* Flat wash at 0.68: the lead paragraph crosses bright dress/bouquet
     patches in the photo, and the muted token can't survive them — pixel-
     sampled ≥4.5:1 vs the brightest patch behind the content block. */
  background:
    linear-gradient(to top, oklch(10% 0.01 85 / 0.55) 0%, oklch(10% 0.01 85 / 0.15) 55%, transparent 100%),
    oklch(8% 0.01 85 / 0.68);
}
/* The desktop scrim is weighted for a wide composition; at phone widths the
   lead paragraph crosses the photo's brightest patches. A heavier
   flat wash keeps body text at 4.5:1 across the whole band. */
@media (max-width: 44em) {
  .downtown__media::after {
    background:
      linear-gradient(to top, oklch(10% 0.01 85 / 0.5) 0%, oklch(10% 0.01 85 / 0.2) 60%, transparent 100%),
      oklch(8% 0.01 85 / 0.68);
  }
}
.downtown__content {
  position: relative;
  /* More above than below: the text should sit in the lower third of the
     photograph, not float in the middle or hug the edge. */
  padding-block: var(--space-2xl) var(--space-xl);
  padding-inline: var(--space-md);
  color: var(--color-on-noir);
  text-shadow: 0 1px 2px oklch(0% 0 0 / 0.5), 0 4px 24px oklch(0% 0 0 / 0.45);
}
/* Full on-noir, not muted: this paragraph sits over a photograph, and the
   muted token only clears 4.5:1 on solid noir surfaces. */
.downtown__content p { color: var(--color-on-noir); max-width: var(--measure); }

/* Centered quote over a photo (weddings page interlude) */
.quote-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  place-items: center;
  text-align: center;
  min-height: 30rem;
  padding-block: var(--space-3xl);
  color: var(--color-on-noir);
}
.quote-band__media { position: absolute; inset: 0; z-index: -2; }
.quote-band__media img { width: 100%; height: 100%; object-fit: cover; }
.quote-band::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  /* 0.7: deep enough that the cite line holds 4.5:1 over the photo's
     brightest patch (pixel-sampled), not just over its average. */
  background: oklch(13% 0.008 75 / 0.7);
}
.quote-band blockquote {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-headline);
  max-width: 22ch;
  margin-inline: auto;
  text-wrap: balance;
  text-shadow: 0 1px 2px oklch(0% 0 0 / 0.5), 0 4px 24px oklch(0% 0 0 / 0.4);
}
.quote-band cite {
  display: block;
  margin-top: var(--space-md);
  font-style: normal;
  font-family: var(--font-body);
  font-size: var(--text-small);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* On-noir, not muted: like the downtown paragraph, this sits over a
     photograph — the muted token is reserved for solid noir surfaces. */
  color: var(--color-on-noir);
}

/* ==========================================================================
   CTA band — every page (except the tour page) ends at the same door
   ========================================================================== */

.cta-band {
  background: var(--color-noir);
  color: var(--color-on-noir);
  text-align: center;
  padding-block: var(--space-3xl);
  position: relative;
  border-top: 1px solid var(--color-primary);
}
@media (min-width: 64rem) {
  .cta-band { padding-block: var(--space-4xl); }
}
/* Deco double hairline */
.cta-band::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 5px;
  height: 1px;
  background: color-mix(in oklch, var(--color-primary) 35%, transparent);
}
.cta-band__title {
  font-size: clamp(2.25rem, 1.8rem + 2.4vw, 4rem);
  max-width: 16em;
  margin-inline: auto;
}
.cta-band__lead {
  font-size: var(--text-lead);
  color: var(--color-on-noir-muted);
  max-width: 38em;
  margin: var(--space-md) auto 0;
}
.cta-band__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-md);
  margin-top: var(--space-xl);
}
.cta-band__contact {
  margin-top: var(--space-xl);
  font-size: var(--text-small);
  letter-spacing: 0.04em;
  color: var(--color-on-noir-muted);
}
.cta-band__contact a {
  color: var(--color-primary-on-dark);
  transition: color var(--duration-base);
  /* An 18px-tall tel: link is the secondary conversion on every page's last
     band. Padded to 44px without changing the line's look; the negative
     margin keeps the sentence's own rhythm. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-block: -0.75rem;
}
.cta-band__contact a:hover { color: var(--color-on-noir); }

/* ==========================================================================
   Tour request / contact
   ========================================================================== */

.contact {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* reflow floor */
  gap: var(--space-2xl);
  /* Single column (phones): details → form → map/social, in source order.
     The form outranks the map — it's the page's whole job. */
}
@media (min-width: 56rem) {
  .contact { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-2xl) var(--space-4xl); }
  .contact__details { grid-column: 1; grid-row: 1; }
  .contact__form-col { grid-column: 2; grid-row: 1 / 3; }
  .contact__aside { grid-column: 1; grid-row: 2; }
}

.contact__details dl { display: flex; flex-direction: column; gap: var(--space-lg); }
.contact__details dt { font-size: var(--text-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-on-noir-muted); margin-bottom: var(--space-3xs); }
/* Quiet role hint so "which number do I use?" answers itself — gold, sentence
   case, so it reads as a note rather than part of the tracked label. */
.contact__details dt .dt-note { text-transform: none; letter-spacing: 0; color: var(--color-primary-on-dark); font-weight: 400; }
/* Facts in the sans (fix pass 2026-07-15): address, phones, email, hours are
   facts — Work Sans 600 with tabular figures, not the display serif. */
.contact__details dd { font-size: var(--text-lead); font-family: var(--font-body); font-weight: 600; font-variant-numeric: tabular-nums; }
.contact__details a { transition: color var(--duration-base); }
.contact__details a:hover { color: var(--color-primary-on-dark); }

.contact__map {
  border: 1px solid var(--color-border-on-dark);
  border-radius: var(--radius-md);
  overflow: hidden;
  /* Designed ground for the OSM iframe: until (unless) the embed paints,
     the box used to be a transparent hole showing raw noir — the critique's
     "empty-box flash". A tonal surface + one centred Deco diamond (the same
     mark as the tour-expect crests above) makes the waiting state read as a
     deliberate plate. The loaded map paints over it completely. */
  position: relative;
  background: var(--color-noir-surface);
}
.contact__map::before {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 6px;
  height: 6px;
  transform: rotate(45deg);
  background: var(--color-primary-on-dark);
  opacity: 0.7;
}
.contact__map iframe {
  display: block;
  width: 100%;
  height: 240px;
  border: 0;
  /* Positioned so it paints above the ::before mark once tiles arrive. */
  position: relative;
  /* The `filter: saturate(.85) contrast(.95)` that used to sit here is gone
     (2026-07-29). A filter on an iframe forces a composited layer around a
     whole cross-origin document and makes the browser re-composite it on
     every scroll — an expensive way to buy a barely visible tint. The frame,
     the radius and the noir plate around it already do the work of sitting
     the light OSM map on a dark section. */
}

.contact__social { display: flex; gap: var(--space-md); margin-top: var(--space-xl); }
/* 44px, not 40: the design's own touch-target bar, and these are the only
   way to reach the venue's socials from the tour page. */
.contact__social a {
  width: 44px; height: 44px;
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color var(--duration-base), background-color var(--duration-base);
}
.contact__social a:hover { border-color: var(--color-primary); background: color-mix(in oklch, var(--color-primary) 15%, transparent); }
.contact__social svg { width: 18px; height: 18px; }

.form-field { margin-bottom: var(--space-lg); }
.form-field label {
  display: block;
  font-size: var(--text-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-on-noir-muted);
  margin-bottom: var(--space-2xs);
}
.form-field input, .form-field textarea {
  width: 100%;
  background: transparent;
  border: none;
  /* The field's only boundary — 1.4.11 territory, not decoration. */
  border-bottom: 1px solid var(--color-border-on-dark-ui);
  color: var(--color-on-noir);
  padding-block: var(--space-sm);
  font-size: var(--text-body);
  transition: border-color var(--duration-base) var(--ease-out-quart);
}
.form-field input::placeholder, .form-field textarea::placeholder { color: color-mix(in oklch, var(--color-on-noir) 55%, transparent); }
/* Focus affordance: thicken the underline via box-shadow for any focus, and
   let the global 2px :focus-visible outline through for keyboard users —
   no outline:none. */
.form-field input:focus, .form-field textarea:focus {
  border-color: var(--color-primary);
  box-shadow: 0 1px 0 0 var(--color-primary);
}
.form-field input:focus-visible, .form-field textarea:focus-visible {
  /* Both form contexts (tour page, pricing card) sit on noir: use the
     contrast-safe on-dark gold per the gold-on-dark rule. */
  outline: 2px solid var(--color-primary-on-dark);
  outline-offset: 2px;
}
.form-field textarea { resize: vertical; min-height: 6rem; }

/* ==========================================================================
   OVERDRIVE — the gilded form (tour page)
   The site's one sanctioned overdrive moment, spent on the conversion rather
   than on decoration. Three ideas, all state, none of them ornament:
     1. the underline is DRAWN from the caret outward instead of switching on
     2. a field that has been answered keeps a small gold Deco diamond
     3. the submit button becomes a plate that reports what actually happened
   `linear()` here is a critically damped spring — it accelerates and settles
   like a physical thing but never overshoots, because DESIGN.md allows the
   site exactly one overshoot and the steps numerals already hold it.
   ========================================================================== */

:root {
  --ease-spring-settle: linear(
    0, 0.0088 1.4%, 0.0344 2.9%, 0.1195 5.8%, 0.2465 8.9%, 0.4029 12.2%,
    0.5688 15.6%, 0.7259 19%, 0.8604 22.5%, 0.9648 26.1%, 1.0362 29.8%,
    1.0765 33.7%, 1.0908 38.1%, 1.0862 43.1%, 1.0708 49.2%, 1.0345 60%,
    1.0088 72%, 1
  );
  --ease-draw: cubic-bezier(0.16, 1, 0.3, 1);
}

.form-field { position: relative; }

/* The drawn underline. A separate layer from the input's own border so the
   border can stay put as the resting state and this can animate over it. */
.form-field input,
.form-field textarea { position: relative; z-index: 1; }

.form-field__line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--color-primary-on-dark);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--duration-slow) var(--ease-draw),
              background-color var(--duration-base) linear;
  pointer-events: none;
}
/* textarea is resizable, so its line tracks the control's own bottom edge */
.form-field:has(textarea) .form-field__line { bottom: auto; top: 0; }

.form-field:focus-within .form-field__line { transform: scaleX(1); }

/* Answered and valid: hold a hairline of gold so a completed form reads as
   a filled ledger rather than a set of empty slots. */
.form-field.is-valid .form-field__line {
  transform: scaleX(1);
  height: 1px;
  background: color-mix(in oklch, var(--color-primary-on-dark) 55%, transparent);
}
.form-field.is-invalid .form-field__line {
  transform: scaleX(1);
  background: var(--color-error);
}

/* The mark: the crest diamond, miniaturised — the same ornament as
   .deco-rule and .tour-expect__mark, so a validated field belongs to the
   brand rather than borrowing a generic checkmark. */
.form-field__mark {
  position: absolute;
  right: 0;
  top: 0;
  width: 7px;
  height: 7px;
  rotate: 45deg;
  background: var(--color-primary-on-dark);
  opacity: 0;
  scale: 0.4;
  transition: opacity var(--duration-base) var(--ease-draw),
              scale var(--duration-slow) var(--ease-spring-settle);
  pointer-events: none;
}
.form-field.is-valid .form-field__mark { opacity: 1; scale: 1; }

/* The submit plate. The button reports the real outcome — it never morphs
   into a success it hasn't earned. */
#tour-form button[type="submit"],
#brochure-form button[type="submit"] {
  transition: background-color var(--duration-base) var(--ease-draw),
              color var(--duration-base) var(--ease-draw),
              border-color var(--duration-base) var(--ease-draw);
}
#tour-form button[type="submit"][data-state="sending"],
#brochure-form button[type="submit"][data-state="sending"] { opacity: 0.75; cursor: progress; }

/* The brochure form sits inside the pricing card, which is already a tight
   noir panel — so the fields breathe less than they do on tour.html, and the
   button owns the full width because it is the card's single command. */
#brochure-form { margin-top: var(--space-lg); }
#brochure-form .form-field { margin-bottom: var(--space-md); }
#brochure-form button[type="submit"] { width: 100%; }

/* Consent by submission: quiet, but never below the error text's size, and
   never a colour that reads as decoration — it is the sentence the SMS
   program stands on. */
.form-consent-note {
  margin-top: var(--space-xs);
  font-size: var(--text-label);
  line-height: 1.5;
  color: color-mix(in oklch, var(--color-on-noir) 72%, transparent);
}
.form-consent-note a { color: var(--color-primary-on-dark); text-decoration: underline; text-underline-offset: 2px; }
.form-consent-note a:hover { color: var(--color-on-noir); }

@media (prefers-reduced-motion: reduce) {
  .form-field__line,
  .form-field__mark,
  #tour-form button[type="submit"] { transition: none; }
}
.form-row { display: grid; gap: var(--space-lg); }
@media (min-width: 30rem) {
  .form-row--split { grid-template-columns: 1fr 1fr; }
}

.form-field__error {
  display: block;
  font-size: var(--text-label);
  color: var(--color-error);
  margin-top: var(--space-3xs);
  min-height: 1.2em;
}

/* Character budget for the message box. Silent by design until the cap is
   close: a running count under an empty textarea is noise, but a `maxlength`
   that truncates without a word is the form quietly editing what somebody
   wrote. Appears in the last 200 characters and not before. */
.form-field__count {
  display: block;
  font-size: var(--text-label);
  color: color-mix(in oklch, var(--color-on-noir) 72%, transparent);
  margin-top: var(--space-3xs);
  text-align: right;
}
.form-field__count:empty { display: none; }

/* Persistent field hint. A placeholder is not a hint: it vanishes exactly when
   somebody starts typing, which is when they need it. */
.form-field__hint {
  display: block;
  font-size: var(--text-label);
  color: color-mix(in oklch, var(--color-on-noir) 72%, transparent);
  margin-top: var(--space-3xs);
}

/* SMS consent. Sits between the message field and the plate, deliberately
   quiet — it's a permission, not a sales line — but never smaller than the
   error text, because whether a carrier audit stands up turns on whether
   this sentence was legible when it was agreed to. */
.form-consent {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2xs);
  align-items: start;
  margin-block: var(--space-lg) var(--space-md);
  font-size: var(--text-label);
  line-height: 1.5;
  color: color-mix(in oklch, var(--color-on-noir) 78%, transparent);
  cursor: pointer;
}
/* Native box, brand accent: keeps the platform's focus ring, keyboard
   behaviour and forced-colors handling, all of which a redrawn span loses. */
.form-consent input[type="checkbox"] {
  accent-color: var(--color-primary-on-dark);
  width: 1rem;
  height: 1rem;
  margin-top: 0.15em; /* optical alignment to the first line, not the box */
}
/* The sentence is no longer a <label> element (it holds links), so the tap
   target it used to give the checkbox is restored here plus a click handler
   in js/main.js. Links inside it keep their own cursor. */
.form-consent__text { cursor: pointer; }
.form-consent__text a { cursor: pointer; }
.form-consent a { color: var(--color-primary-on-dark); text-decoration: underline; text-underline-offset: 2px; }
.form-consent a:hover { color: var(--color-on-noir); }

/* The error sits under the sentence, not under the box, so it reads as a
   note on what was asked rather than a label for the tick. */
.form-consent .form-field__error { grid-column: 2; min-height: 0; }
.form-consent .form-field__error:empty { display: none; }
/* Required and unticked: lift the sentence to full strength and mark the box.
   Colour alone can't carry this — the message above says what's wrong. */
.form-consent.is-invalid { color: var(--color-on-noir); }
.form-consent.is-invalid input[type="checkbox"] { outline: 2px solid var(--color-error); outline-offset: 2px; }

.form-status {
  margin-top: var(--space-md);
  font-size: var(--text-small);
  color: var(--color-primary-on-dark);
  min-height: 1.4em;
}
.form-status[data-state="error"] { color: var(--color-error); }
/* Designed conditions (e.g. endpoint not wired yet) are notices, not
   failures: neutral light tone, no alarm red. */
.form-status[data-state="notice"] { color: var(--color-on-noir); }

/* Copy-to-clipboard fallback button (tour form, unwired-endpoint state).
   The [hidden] rule is load-bearing: .btn's display:inline-flex would
   otherwise override the UA's [hidden] { display: none }. */
#copy-request { margin-block: var(--space-xs) var(--space-2xs); }
#copy-request[hidden] { display: none; }

.form-fallback {
  margin-top: var(--space-sm);
  font-size: var(--text-label);
  color: var(--color-on-noir-muted);
}
.form-fallback a { color: var(--color-primary-on-dark); text-decoration: underline; text-underline-offset: 2px; }
.form-fallback a:hover { color: var(--color-on-noir); }

/* ==========================================================================
   Tour page — booking calendar
   Our own calendar over the venue's live High Level availability. Everything
   here is drawn in the site's own palette on purpose: the alternative was
   High Level's booking iframe, which arrives in its own colours on another
   origin and can't be reached from this stylesheet at all.
   ========================================================================== */
.booking__intro { max-width: 44rem; margin-bottom: var(--space-xl); }

.booking__panel {
  /* Capped so the calendar and the times stay in conversation. Left to run
     the container's full width, the two columns end up at opposite ends of a
     thousand pixels with a void between them. */
  max-width: 56rem;
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: var(--radius-md);
  background: var(--color-noir-surface);
  /* The outer min() caps the gutter at 8% of the screen. The padding is in
     rem, so at a 200% text setting it was eating 96 of a phone's 390 pixels
     and starving the seven-column calendar inside it; the cap is inert at
     every normal text size (24px at 390/100%, 48px on desktop, unchanged). */
  padding: min(clamp(var(--space-md), 4vw, var(--space-xl)), 8vw);
}

.booking__loading,
.booking__empty {
  margin: 0;
  font-size: var(--text-small);
  color: var(--color-on-noir-muted);
}

.booking__grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-xl); }
/* Capped, because the day cells are square: let the calendar run the full
   width of a wide panel and every date becomes a hundred-pixel tile, which
   reads as a dashboard rather than a calendar.

   `min-width: 0` + `container-type` is the P0 fix (found 2026-07-29). Every
   dimension in this calendar used to be a rem, so at a 200% text setting the
   seven-column grid measured 444px inside a 294px panel and the page's
   `html{overflow-x:hidden}` deleted the overflow rather than letting it
   scroll: Thu/Fri/Sat, the "Next month" button and two of the three bookable
   dates were unreachable, with window.scrollX pinned at 0. Making the
   calendar a size container lets the type below scale against THE PANEL
   instead of the root font size, so the month still fits the screen at 200%
   — reachable by scrolling was the weaker fix; this one keeps it usable. */
.booking__calendar {
  max-width: 24rem;
  width: 100%;
  min-width: 0;
  container-type: inline-size;
}
@media (min-width: 52rem) {
  /* Both columns fixed and pushed apart: a list of six times has no business
     stretching to half the page, and neither does September. */
  .booking__grid {
    grid-template-columns: minmax(0, 24rem) 15rem;
    justify-content: start;
    gap: var(--space-2xl);
  }
}

.booking__monthbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
  /* Two 44px arrows plus a month name is more than a phone holds at 200%
     text; let the row break rather than push "Next month" off the screen. */
  flex-wrap: wrap;
}
.booking__month {
  margin: 0;
  min-width: 0;
  flex: 1 1 auto;
  text-align: center;
  font-family: var(--font-display);
  /* Capped against the calendar, not the root: "September 2026" at a 200%
     root font size is wider than the panel on its own. */
  font-size: min(var(--text-h4, 1.25rem), 9cqi);
  color: var(--color-on-noir);
}
.booking__nav {
  display: inline-grid;
  place-items: center;
  /* 44px, not 36px: this is the calendar's primary month navigation, tapped
     with a thumb on a phone, and it sat under the design's own 44px bar.
     Capped at 3.5rem-equivalent so a 200% text setting can't grow the two
     arrows past the width of the month they navigate. */
  width: clamp(44px, 2.75rem, 56px);
  height: clamp(44px, 2.75rem, 56px);
  flex: 0 0 auto;
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-on-noir);
  cursor: pointer;
  transition: border-color var(--duration-base) var(--ease-out-quart),
              color var(--duration-base) var(--ease-out-quart);
}
.booking__nav svg { width: 1.1rem; height: 1.1rem; }
.booking__nav:hover:not(:disabled):not([aria-disabled="true"]) { border-color: var(--color-primary-on-dark); color: var(--color-primary-on-dark); }
/* aria-disabled, not disabled: a real `disabled` attribute drops focus to
   <body> the moment the arrow you are standing on reaches the end of the
   range, which throws a keyboard user out of the calendar. The click handler
   no-ops at the ends instead, so this only has to look spent. */
/* aria-disabled only: `disabled` is never set on these, deliberately — a truly
   disabled button drops focus to <body> the moment you arrow onto the first
   month. `step()` no-ops at the ends instead. */
.booking__nav[aria-disabled="true"] { opacity: 0.3; cursor: default; }

/* Offered beside the "this month didn't load" message, so a refused
   availability read is recoverable without reloading the page. */
.booking__retry {
  margin-top: var(--space-sm);
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: var(--radius-sm);
  background: none;
  padding: var(--space-3xs) var(--space-sm);
  font-family: var(--font-body);
  font-size: var(--text-label);
  color: var(--color-on-noir);
  cursor: pointer;
}
.booking__retry:hover { border-color: var(--color-primary-on-dark); color: var(--color-primary-on-dark); }

/* minmax(0, …), not a bare 1fr: a bare `1fr` floors each track at the day
   cell's min-content, so seven cells could never shrink below seven whole
   numerals plus their padding, and the grid pushed straight off the screen at
   large text settings.

   The seven columns live on the WEEK now, not on the month. #booking-days is
   role="grid" and each week is a real role="row" box (see js/main.js) — a row
   has to be an element the accessibility tree keeps, which rules out
   `display: contents`. Visually identical: same tracks, same gap. */
.booking__weekdays,
.booking__week {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--space-3xs);
}
.booking__days {
  display: grid;
  gap: var(--space-3xs);
}
.booking__weekdays {
  margin-bottom: var(--space-2xs);
  /* cqi caps the label against the calendar's own width. A track is roughly
     13cqi wide, so a three-letter weekday needs about 3.4cqi of type to sit
     inside it; below that the abbreviations collide. */
  font-size: min(var(--text-label), 4.6cqi);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-on-noir-muted);
  text-align: center;
}

/* Height is floored rather than tied to the width. Seven square columns
   inside a phone leaves a 36px target, smaller than a thumb deserves — and
   forcing the square back up to 44 pushes the row wider than the screen. So
   the cell keeps the grid's width and stands 44px tall everywhere. */
.booking__day {
  /* Floored at the 44px touch bar, capped so a 200% text setting can't turn
     a twelve-row month into a scroll of its own. */
  min-height: 2.75rem;
  min-height: clamp(44px, 2.75rem, 3.75rem);
  min-width: 0;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  font-family: var(--font-body);
  /* Two numerals inside a ~13cqi track: 5cqi is the largest that still leaves
     the border and the selected-state fill room to read. */
  font-size: min(var(--text-small), 6.4cqi);
  /* A date with nothing open is still a date — it reads as unavailable, not
     as missing, so the month keeps its shape. */
  color: color-mix(in oklch, var(--color-on-noir) 30%, transparent);
  cursor: default;
  transition: background-color var(--duration-base) var(--ease-out-quart),
              border-color var(--duration-base) var(--ease-out-quart),
              color var(--duration-base) var(--ease-out-quart);
}
.booking__day--blank { border: 0; }
.booking__day.has-slots {
  color: var(--color-on-noir);
  border-color: var(--color-border-on-dark-ui);
  cursor: pointer;
}
.booking__day.has-slots:hover { border-color: var(--color-primary-on-dark); color: var(--color-primary-on-dark); }
/* Today gets a mark, not a fill — a fill would compete with the selection. */
.booking__day.is-today { text-decoration: underline; text-underline-offset: 3px; }
.booking__day.is-selected,
.booking__day.is-selected:hover {
  background: var(--color-primary-on-dark);
  border-color: var(--color-primary-on-dark);
  color: var(--color-noir);
}

.booking__times-title {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--text-h4, 1.25rem);
  color: var(--color-on-noir);
}
.booking__slots { display: grid; gap: var(--space-2xs); }
.booking__slot {
  padding: var(--space-2xs) var(--space-sm);
  /* Two pixels short of the 44px bar as drawn; named rather than left to the
     padding, so a type-scale change can't quietly take it back under. */
  min-height: 44px;
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: var(--radius-sm);
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-small);
  color: var(--color-on-noir);
  cursor: pointer;
  transition: background-color var(--duration-base) var(--ease-out-quart),
              border-color var(--duration-base) var(--ease-out-quart),
              color var(--duration-base) var(--ease-out-quart);
}
.booking__slot:hover { border-color: var(--color-primary-on-dark); color: var(--color-primary-on-dark); }
.booking__slot[aria-pressed="true"] {
  background: var(--color-primary-on-dark);
  border-color: var(--color-primary-on-dark);
  color: var(--color-noir);
}
.booking__tz {
  margin-top: var(--space-sm);
  font-size: var(--text-label);
  color: var(--color-on-noir-muted);
}

/* The details form, revealed under the picker once a time is in hand. */
.booking__form {
  margin-top: var(--space-xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-on-dark);
}
.booking__form[hidden] { display: none; }
/* Specific enough to beat `.section--noir p`, which would otherwise mute the
   one line on the panel that has to read as settled. */
.section--noir .booking__chosen {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm);
  margin: 0 0 var(--space-lg);
  font-family: var(--font-display);
  font-size: var(--text-h4, 1.25rem);
  color: var(--color-primary-on-dark);
}
.booking__change {
  border: 0;
  background: none;
  /* Padded to a 44px target rather than resized: the link should still LOOK
     like a quiet inline "Change", but at 46.8x20.8 it was under half the
     design's own minimum, on the one control that undoes a chosen time. */
  padding: 0 var(--space-2xs);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-body);
  font-size: var(--text-label);
  color: var(--color-on-noir-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.booking__change:hover { color: var(--color-on-noir); }
.label-note { color: var(--color-on-noir-muted); font-weight: 400; }
/* Stated once at the top of a form on which every field is required — the
   counterpart to the "(optional)" label note above. */
.form-required-note {
  margin-bottom: var(--space-md);
  font-size: var(--text-label);
  color: var(--color-on-noir-muted);
}

.booking__done { text-align: center; padding-block: var(--space-lg); }
.booking__done[hidden] { display: none; }
.booking__done-mark {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  margin: 0 auto var(--space-md);
  border: 1px solid var(--color-primary-on-dark);
  border-radius: 50%;
}
.booking__done-mark span {
  width: 0.6rem;
  height: 0.6rem;
  rotate: 45deg;
  background: var(--color-primary-on-dark);
}
.booking__done-title {
  margin: 0 0 var(--space-2xs);
  font-family: var(--font-display);
  color: var(--color-on-noir);
}
.section--noir .booking__done-when { margin: 0 0 var(--space-sm); color: var(--color-primary-on-dark); }
.booking__done-note { margin: 0; font-size: var(--text-small); color: var(--color-on-noir-muted); }
.booking__done-note a { color: var(--color-primary-on-dark); text-decoration: underline; text-underline-offset: 2px; }

.booking__fallback {
  margin-top: var(--space-md);
  font-size: var(--text-small);
  color: var(--color-on-noir-muted);
}
.booking__fallback[hidden] { display: none; }
.booking__fallback a { color: var(--color-primary-on-dark); text-decoration: underline; text-underline-offset: 2px; }
.booking__fallback a:hover { color: var(--color-on-noir); }
/* When availability can't be read at all, the line stops being a footnote to
   a calendar and becomes the only instruction on the panel. */
.section--noir .booking__fallback--sole { font-size: var(--text-body); color: var(--color-on-noir); }
.booking__panel[data-state="unavailable"] { display: none; }

#booking-form button[type="submit"] {
  transition: background-color var(--duration-base) var(--ease-draw),
              color var(--duration-base) var(--ease-draw),
              border-color var(--duration-base) var(--ease-draw);
}
#booking-form button[type="submit"][data-state="sending"] { opacity: 0.75; cursor: progress; }

@media (prefers-reduced-motion: reduce) {
  .booking__day,
  .booking__slot,
  .booking__nav,
  #booking-form button[type="submit"] { transition: none; }
}

/* ==========================================================================
   Tour page — "What a tour looks like" reassurance row
   Three quiet columns (icon → title → line), used once, no card chrome.
   Stacks with hairline dividers on phones; centered columns on wider screens.
   ========================================================================== */

.tour-expect {
  display: grid;
  gap: 0;
  max-width: var(--container-max);
  margin-inline: auto;
}
@media (min-width: 48rem) {
  .tour-expect { grid-template-columns: repeat(3, 1fr); gap: var(--space-2xl); }
}
.tour-expect__item {
  padding-block: var(--space-lg);
  border-top: 1px solid var(--color-border);
}
.tour-expect__item:last-child { border-bottom: 1px solid var(--color-border); }
@media (min-width: 48rem) {
  .tour-expect__item {
    padding-block: 0;
    border: 0;
    text-align: center;
    max-width: 34ch;
    margin-inline: auto;
  }
  .tour-expect__item:last-child { border-bottom: 0; }
}
/* Brand mark instead of stock icons: a miniature of the crest rule
   (hairline · diamond · hairline) — the venue's one ornament, echoed from
   .deco-rule and the closing quote, so the three items read as one system. */
.tour-expect__mark {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2xs);
  color: var(--color-primary);
  margin-bottom: var(--space-md);
}
@media (min-width: 48rem) { .tour-expect__mark { justify-content: center; } }
.tour-expect__mark::before,
.tour-expect__mark::after {
  content: '';
  width: 22px;
  height: 1px;
  background: currentColor;
  opacity: 0.5;
}
.tour-expect__diamond {
  width: 7px;
  height: 7px;
  background: currentColor;
  transform: rotate(45deg);
  flex-shrink: 0;
}
.tour-expect__mark::before,
.tour-expect__mark::after { transition: opacity var(--duration-base) var(--ease-out-quart); }

/* Delight: as the band scrolls in, a gold gleam sweeps mark-to-mark (JS adds
   .is-lit staggered) — light travelling across gilded molding, the same move
   as the crest rule. And each mark glints on hover. Reuses the shared
   deco-hairline-gleam keyframe; a light-surface glint lands its own glow. */
@keyframes tour-mark-glint {
  0%   { transform: rotate(45deg) scale(1);   box-shadow: 0 0 0 0 transparent; }
  45%  { transform: rotate(45deg) scale(1.55); box-shadow: 0 0 12px 2px color-mix(in oklch, var(--color-primary) 55%, transparent); }
  100% { transform: rotate(45deg) scale(1);   box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: no-preference) {
  .tour-expect__mark.is-lit::before      { animation: deco-hairline-gleam 0.7s var(--ease-out-quart) 0s; }
  .tour-expect__mark.is-lit .tour-expect__diamond { animation: tour-mark-glint 0.7s var(--ease-out-quart) 0.18s; }
  .tour-expect__mark.is-lit::after       { animation: deco-hairline-gleam 0.7s var(--ease-out-quart) 0.34s; }

  .tour-expect__diamond { transition: transform var(--duration-base) var(--ease-out-quart), box-shadow var(--duration-base) var(--ease-out-quart); }
  .tour-expect__item:hover .tour-expect__diamond { transform: rotate(45deg) scale(1.4); box-shadow: 0 0 10px 1px color-mix(in oklch, var(--color-primary) 55%, transparent); }
  .tour-expect__item:hover .tour-expect__mark::before,
  .tour-expect__item:hover .tour-expect__mark::after { opacity: 0.95; }
}
.tour-expect__title {
  font-size: var(--text-title);
  margin-bottom: var(--space-2xs);
}
.tour-expect p { color: var(--color-muted); max-width: 42ch; }
@media (min-width: 48rem) {
  .tour-expect p { margin-inline: auto; }
}

/* ==========================================================================
   Tour page — the noir request section, warmed
   A faint, heavily-veiled photo of the venue's own black-and-gold Art Deco
   lounge sits behind the form: real depth and a warm sconce glow instead of
   flat black, while staying dark enough for the form to stay legible.
   ========================================================================== */

.tour-contact { position: relative; overflow: hidden; isolation: isolate; }
.tour-contact__scene {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    /* Keep the form's zone (upper-right at ≥56rem) darkest so fields stay
       crisp; everything else is only moderately dimmed so the lounge — its
       gold molding and warm sconces — genuinely reads and fills what used to
       be dead black space. Body text here clears AA with huge headroom
       (labels 8:1, inputs 18:1), so the lighter veil is safe. */
    radial-gradient(120% 95% at 84% 16%,
      var(--color-noir) 20%,
      color-mix(in oklch, var(--color-noir) 40%, transparent) 60%,
      transparent 100%),
    linear-gradient(to bottom,
      color-mix(in oklch, var(--color-noir) 52%, transparent) 0%,
      color-mix(in oklch, var(--color-noir) 60%, transparent) 100%),
    url("../assets/photos/lounge-art-deco-first-dance-1600.webp");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center 22%;
}
/* On single-column (phones) the form stacks below the details, so let the
   lounge read behind the heading/details up top and deepen to solid noir
   toward the form — the photo survives instead of going flat black. */
@media (max-width: 55.99rem) {
  .tour-contact__scene {
    background:
      linear-gradient(to bottom,
        color-mix(in oklch, var(--color-noir) 46%, transparent) 0%,
        color-mix(in oklch, var(--color-noir) 66%, transparent) 34%,
        var(--color-noir) 60%),
      url("../assets/photos/lounge-art-deco-first-dance-1600.webp");
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center top;
  }
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
  background: var(--color-noir);
  color: var(--color-on-noir-muted);
  padding-block: var(--space-2xl) var(--space-lg);
  border-top: 1px solid color-mix(in oklch, var(--color-primary) 35%, transparent);
}
.site-footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: var(--space-2xl) var(--space-xl);
  align-items: start;
  padding-bottom: var(--space-2xl);
  border-bottom: 1px solid var(--color-border-on-dark);
}
.site-footer__brand img { height: 72px; width: auto; filter: brightness(0) invert(1); opacity: 0.92; }
.site-footer__brand p { margin-top: var(--space-md); max-width: 26ch; font-size: var(--text-small); }
.site-footer h2 {
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-primary-on-dark);
  margin-bottom: var(--space-md);
}
/* minmax + anywhere: the track's min-content was `brickballroom@gmail.com`
   as one unbreakable token — 334px inside a 294px column at 200% text. */
.site-footer ul { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); font-size: var(--text-small); }
.site-footer ul a { overflow-wrap: anywhere; }
.site-footer a:hover { color: var(--color-on-noir); }
/* Tap targets: pad footer links to the design's own 44px bar (they measured
   41px); the negative block margin cancels the padding in layout, so nothing
   moves visually and only the hit area grows. */
.site-footer ul a,
.site-footer__social a {
  display: inline-block;
  padding-block: 0.75rem;
  margin-block: -0.75rem;
}
.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  flex-direction: column;
  gap: var(--space-sm);
  padding-top: var(--space-lg);
  font-size: var(--text-label);
}
@media (min-width: 56rem) {
  .site-footer__bottom { flex-direction: row; justify-content: space-between; }
}
/* Four social links in a nowrap row need 305px; at 320px the footer column is
   272px, so this row — and only this row — pushed the whole document to a 329px
   scrollWidth on all ten pages. Pre-existing; wrapping costs nothing above
   360px, where the row still fits on one line. */
.site-footer__social { display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-md); }
.site-footer__social a:hover { color: var(--color-on-noir); }

/* ==========================================================================
   Sticky mobile CTA — the site's one job, always one tap away
   ========================================================================== */

.mobile-cta { display: none; }
@media (max-width: 63.99rem) {
  .mobile-cta {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    justify-content: center;
    /* The bar's height was already tracked at 200% via --mobile-cta-h, but the
       horizontal axis was never given the same treatment. "Request a Tour" and
       its "· free & no pressure" note are two flex items on what was a nowrap
       row (the note is a <span>, so flex blockifies it into an item of its
       own), and at large text they could only break INSIDE each item — three
       ragged lines. Wrapping breaks between them instead, where the sentence
       actually divides. */
    flex-wrap: wrap;
    text-align: center;
    gap: 0.6rem;
    background: var(--color-primary-deep);
    color: var(--color-on-primary);
    font-size: 0.9rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.95rem 1rem;
    padding-bottom: max(0.95rem, env(safe-area-inset-bottom));
    /* Deco double hairline instead of a drop shadow, per flat-by-default */
    box-shadow: inset 0 1px 0 var(--color-primary), inset 0 4px 0 oklch(100% 0 0 / 0.25);
    transition: transform var(--duration-slow) var(--ease-out-expo), visibility 0s;
  }
  /* Stowed while the hero's own gold CTA is still on screen (JS-managed via
     IntersectionObserver) so two identical commands never stack at first
     paint. Visible by default: without JS the class is never added and the
     bar behaves as always-on. visibility:hidden keeps the off-screen bar
     out of the tab order. */
  .mobile-cta.is-stowed {
    transform: translateY(110%);
    visibility: hidden;
    transition: transform var(--duration-slow) var(--ease-out-expo), visibility 0s var(--duration-slow);
  }
  @media (prefers-reduced-motion: reduce) {
    .mobile-cta, .mobile-cta.is-stowed { transition: none; }
  }
  .mobile-cta span { color: oklch(100% 0 0 / 0.72); font-weight: 400; letter-spacing: 0.04em; text-transform: none; }
  /* Room so the bar never covers the last line of the footer.

     Measured, not guessed: the bar wraps to two and then three lines as text
     scales, reaching 153px at 200% — nearly three times the 3.4rem this used
     to hard-code, so the footer's last lines sat underneath it with no way to
     scroll further. js/main.js keeps --mobile-cta-h in step with the real
     rendered height; the 3.4rem stays as the no-JS fallback, which is exactly
     what a no-JS visitor had before. */
  body:has(.mobile-cta) { padding-bottom: var(--mobile-cta-h, 3.4rem); }
  /* Step aside while an overlay owns the screen */
  body:has(.lightbox:not([hidden])) .mobile-cta,
  body:has(.mobile-nav.is-open) .mobile-cta { display: none; }
}

/* ==========================================================================
   Scroll reveals
   Fully visible by default — GSAP (js/main.js) owns all entrance animation
   via ScrollTrigger and sets its own "from" states with gsap.set(), only
   once it has successfully loaded. If the GSAP CDN is blocked or fails,
   every section simply stays at this visible default. No CSS opacity:0
   gating exists here on purpose.
   ========================================================================== */

.reveal {
  opacity: 1;
  transform: none;
}

/* ==========================================================================
   Stay rows + photo rails (lodging)

   Two full-width alternating rows, one per stay, composed like .plan-item:
   media takes the 1.15fr track, copy 1fr, and the even row mirrors left/right.

   The swipe itself is CSS — the viewport is a scroll-snap container — so it
   survives this script and GSAP both being dead. JS only adds the arrows, the
   Deco-diamond dots, the "n / 8 · room" readout and the lightbox handoff, all
   gated behind .js-rail so no dead control ever renders.
   ========================================================================== */

.stay-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  padding-block: var(--space-2xl);
  border-top: 1px solid var(--color-border);
  scroll-margin-top: 6rem;
}
.stay-row:last-of-type { border-bottom: 1px solid var(--color-border); }

@media (min-width: 56rem) {
  .stay-row { grid-template-columns: 1.15fr 1fr; align-items: center; gap: var(--space-3xl); }
  /* Mirror the even row by REASSIGNING GRID AREAS, never with `order`. With
     `order` the media lands in the second track and the 1.15fr meant to favour
     photos silently favours the text instead — that bug composed the Hidden
     Loft at half scale (229x305) and read as the lesser property. */
  .stay-row:nth-of-type(2n) { grid-template-columns: 1fr 1.15fr; }
  .stay-row:nth-of-type(2n) .stay-row__media { grid-area: 1 / 2; }
  .stay-row:nth-of-type(2n) .stay-row__body { grid-area: 1 / 1; }
}

.stay-row__body { max-width: var(--measure); }
.stay-row__title { font-size: var(--text-headline); margin-bottom: var(--space-2xs); }
/* Prefixed with .stay-row on purpose: bare `.stay-row__specs` (0,1,0) loses to
   `.stay-row__body p` (0,1,1) and the most decision-relevant fact on the row
   silently renders as ordinary muted body text. Same bug as the old
   .stay-card__specs one; it has now been introduced twice. */
.stay-row .stay-row__specs {
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-primary-deep);
  margin-bottom: var(--space-sm);
}
.stay-row__body p { color: var(--color-muted); }

/* --- The rail: centre-stage carousel ------------------------------------
   The active photo sits mid-track at full size and full colour; its
   neighbours peek in from both sides at 93% scale behind a --color-bg veil,
   so there is always exactly one photo in front.

   Slide width and the centring side-padding are both measured in `cqw`
   against the rail, NEVER percentages: percentage padding shrinks the
   scrollport's content box and a percentage slide width then resolves against
   *that*, which silently produced three full-width photos and no front at all.
   Do not reintroduce a negative margin to reclaim the padding either — it made
   the scrollport 723px wide inside a 548px track and bled photos outside the
   column. */
.rail { position: relative; container-type: inline-size; --slide-w: 68cqw; }
.rail--portrait { --slide-w: 62cqw; }
@media (max-width: 39.99rem) {
  .rail { --slide-w: 80cqw; }
  .rail--portrait { --slide-w: 68cqw; }
}

.rail__viewport {
  display: flex;
  gap: var(--space-xs);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding: 2px calc((100cqw - var(--slide-w)) / 2);
}
/* Scoped to .js-rail so a no-JS rail keeps its scrollbar — without JS that
   scrollbar is the ONLY affordance a desktop pointer has for photos 2-8. */
.js-rail .rail__viewport { scrollbar-width: none; -ms-overflow-style: none; }
.js-rail .rail__viewport::-webkit-scrollbar { display: none; }

.rail__slide {
  flex: 0 0 var(--slide-w);
  scroll-snap-align: center;
  margin: 0;
  position: relative;
  border: 0;
  padding: 0;
  background: none;
  cursor: zoom-in;
  border-radius: var(--radius-md);
  display: block;
  scale: 0.93;
  transition: scale var(--duration-slow) var(--ease-out-quart);
}
.rail__slide.is-active { scale: 1; }
.rail__slide::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  background: var(--color-bg);
  opacity: 0.45;
  pointer-events: none;
  transition: opacity var(--duration-slow) var(--ease-out-quart);
}
.rail__slide.is-active::after { opacity: 0; }
/* Without JS nothing tracks which slide is active, so never dim or shrink any
   of them — a veiled row with no "front" would be worse than a plain strip. */
.rail:not(.js-rail) .rail__slide { scale: 1; }
.rail:not(.js-rail) .rail__slide::after { opacity: 0; }

.rail__slide img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-md);
}
/* Each set keeps its own natural ratio — Penthouse Perch 3:2 landscape, the
   Hidden Loft 3:4 portrait. Forcing one frame would crop the loft's trusses
   and skylights away. */
.rail--portrait .rail__slide img { aspect-ratio: 3 / 4; }

@media (hover: hover) and (pointer: fine) {
  .rail__slide.is-active:hover { scale: 1.02; }
}
.rail__slide:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }

.rail__nav {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 1;
  display: none;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-bg);
  color: var(--color-ink);
  cursor: pointer;
  transition: background var(--duration-base), color var(--duration-base),
              border-color var(--duration-base), opacity var(--duration-base);
}
.rail__nav svg { width: 20px; height: 20px; }
.rail__nav--prev { left: var(--space-xs); }
.rail__nav--next { right: var(--space-xs); }
.rail__nav:hover { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary); }
.rail__nav:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* aria-disabled, NOT [disabled]: a truly disabled button drops keyboard focus
   to <body> when you arrow onto the last slide. Target the ARIA state or the
   end-of-rail arrows keep looking live. */
.rail__nav[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.rail__nav[aria-disabled="true"]:hover { background: var(--color-bg); border-color: var(--color-border); color: var(--color-ink); }
/* Arrows are a fine-pointer affordance only; touch gets the swipe plus the
   peeking neighbour. */
@media (hover: hover) and (pointer: fine) {
  .js-rail .rail__nav { display: grid; }
}

/* Readout above, dots below. Measured: the longest readout,
   "1 / 8 · Under the skylight", needs 212px on one line, so a dots-plus-
   counter row needs 352 + 16 + 212 = 580px — wider than the widest media
   track this layout ever gives it (548px at 1440). Side by side, the counter
   is the flex item that can shrink, so it collapsed to 60px and stacked onto
   four lines at 1024. The dots get their own row instead. */
.rail__meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  margin-top: var(--space-sm);
}
/* The readout is one line or it isn't a readout. */
.rail__count { white-space: nowrap; }

/* Wrapping, because the dot row is a fixed 44px per photo and the stays run
   to twelve: at a 480px viewport twelve dots measured 528px, and since
   nothing clips them that became 24px of horizontal scroll on the whole
   document. Wrapping keeps every target at its full 44px instead of
   shrinking them under WCAG 2.5.5. */
.rail__dots { display: flex; flex-wrap: wrap; justify-content: center; max-width: 100%; }
@container (max-width: 24rem) {
  /* Narrower than the dot row itself: on a phone the gesture IS the swipe and
     the counter already names the room in words, so drop the redundant control
     rather than shrinking targets back under 44px. */
  .js-rail .rail__dots { display: none; }
}
.rail__dot {
  /* A 7px mark inside a true 44x44 target: 7 + 2*18.5 = 44 both ways. The
     negative block margin keeps the row visually tight without shrinking the
     hit area — an earlier 18px/6px pad gave 43x19, which failed WCAG 2.5.5
     on both axes while a comment claimed otherwise. */
  appearance: none;
  border: 0;
  background: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  margin-block: -18px;
  padding: 0;
  cursor: pointer;
  line-height: 0;
}
/* --color-mark-idle, not --color-border: idle dots on white must clear 3:1 for
   non-text UI (WCAG 1.4.11). --color-border measured 1.34:1 — eight of nine
   marks were effectively invisible. This measures 3.36:1. */
.rail__dot::before {
  content: '';
  display: block;
  width: 7px;
  height: 7px;
  rotate: 45deg;
  background: var(--color-mark-idle);
  transition: background var(--duration-base), scale var(--duration-base);
}
.rail__dot[aria-current="true"]::before { background: var(--color-primary); scale: 1.15; }
.rail__dot:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -12px; }

.rail__count {
  font-family: var(--font-body);
  font-size: var(--text-label);
  letter-spacing: 0.06em;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}
/* No JS means no position tracking, so the whole readout row would be lying. */
.rail:not(.js-rail) .rail__meta { display: none; }

@media (prefers-reduced-motion: reduce) {
  .rail__viewport { scroll-behavior: auto; }
  .rail__slide, .rail__slide::after { transition: none; }
  .rail__slide.is-active:hover { scale: 1; }
}

.section-note {
  font-size: var(--text-small);
  color: var(--color-muted);
  max-width: 44ch;
  margin-inline: auto;
  margin-top: var(--space-xl);
  text-align: center;
}
/* Feature variant: for a closing note that carries a real offer rather than
   fine print. The base .section-note is deliberately small and muted — the
   Weekender line is the one sentence on the lodging page that should sell,
   so it takes the display face, full ink contrast, and title sizing. */
.section-note--feature {
  font-family: var(--font-display);
  font-size: var(--text-title);
  line-height: 1.35;
  color: var(--color-ink);
  max-width: 34ch;
}

.lightbox {
  position: fixed;
  inset: 0;
  width: 100vw;
  width: 100dvw;
  height: 100vh;
  height: 100dvh;
  z-index: var(--z-modal);
  background: oklch(6% 0.005 85 / 0.96);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
}
.lightbox[hidden] { display: none; }

.lightbox__figure {
  /* --lightbox-frame is set from the photo's natural width on load, so the
     caption never runs wider than the image it describes. Falls back to the
     old cap if the script never gets there. */
  max-width: min(90vw, var(--lightbox-frame, 1200px));
  max-height: 84vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
}
.lightbox__image {
  max-width: 100%;
  max-height: 74vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-sm);
  box-shadow: 0 24px 64px oklch(0% 0 0 / 0.5);
}
.lightbox__caption {
  color: var(--color-on-noir-muted);
  font-size: var(--text-small);
  text-align: center;
  max-width: 62ch;
}
.lightbox__count {
  position: absolute;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%);
  color: var(--color-on-noir-muted);
  font-size: var(--text-label);
  letter-spacing: 0.04em;
}

/* Buttons over the lightbox scrim: control boundaries, so the 3:1 token. */
.lightbox__close,
.lightbox__nav {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  color: var(--color-on-noir);
  border: 1px solid var(--color-border-on-dark-ui);
  border-radius: 50%;
  transition: border-color var(--duration-base) var(--ease-out-quart), background-color var(--duration-base) var(--ease-out-quart), transform var(--duration-fast) var(--ease-out-quart);
}
.lightbox__close:hover, .lightbox__nav:hover {
  border-color: var(--color-primary);
  background: color-mix(in oklch, var(--color-primary) 15%, transparent);
}
.lightbox__close:active, .lightbox__nav:active { transform: scale(0.94); }
.lightbox__close svg, .lightbox__nav svg { width: 20px; height: 20px; }

.lightbox__close { top: var(--space-lg); right: var(--space-lg); }
.lightbox__nav--prev { left: var(--space-lg); top: 50%; transform: translateY(-50%); }
.lightbox__nav--next { right: var(--space-lg); top: 50%; transform: translateY(-50%); }
.lightbox__nav--prev:active { transform: translateY(-50%) scale(0.94); }
.lightbox__nav--next:active { transform: translateY(-50%) scale(0.94); }

@media (max-width: 40rem) {
  .lightbox { padding: var(--space-md); }
  /* 44px minimum touch target, even though the visual circle is compact. */
  .lightbox__close { top: var(--space-md); right: var(--space-md); width: 44px; height: 44px; }
  .lightbox__nav { width: 44px; height: 44px; }
  .lightbox__nav--prev { left: var(--space-3xs); }
  .lightbox__nav--next { right: var(--space-3xs); }
}

/* =========================================================================
   CLIENT PORTAL (client-portal.html)

   A gated, post-booking page. Everything here is namespaced .portal-* so it
   can never leak into the marketing pages, and the page otherwise reuses the
   sitewide header, footer, rail, gallery-grid and lightbox components.
   ====================================================================== */

/* ---- The gate ---------------------------------------------------------
   Sits above every other layer, including the mobile nav (200) and the
   lightbox (250) — nothing on this page should ever paint over the gate.
   The visibility toggle itself lives in the page's own <style> block so it
   applies before main.css has finished loading and the contents can never
   flash. This file only dresses it.
   --------------------------------------------------------------------- */
.portal-gate {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* reflow floor */
  place-items: center;
  padding: var(--space-lg);
  background: var(--color-noir);
  overflow-y: auto;
}
.portal-gate__plate {
  width: min(28rem, 100%);
  text-align: center;
  padding-block: var(--space-lg);
}
.portal-gate__logo {
  width: 8.5rem;
  height: auto;
  margin: 0 auto var(--space-lg);
  /* The crest is dark-on-transparent; invert it onto the noir plate the same
     way the header does over a dark hero. */
  filter: invert(1) brightness(1.6);
}
.portal-gate__title {
  font-family: var(--font-display);
  font-size: var(--text-headline);
  font-weight: 500;
  color: var(--color-on-noir);
  margin-bottom: var(--space-xs);
}
.portal-gate__lead {
  color: var(--color-on-noir-muted);
  margin-bottom: var(--space-xl);
}
.portal-gate__form { text-align: left; }
.portal-gate__form .btn { width: 100%; }
.portal-gate__error {
  color: var(--color-primary-on-dark);
  font-size: var(--text-small);
  margin-top: calc(var(--space-lg) * -1 + var(--space-2xs));
  margin-bottom: var(--space-md);
}
.portal-gate__nojs {
  display: none;              /* revealed by the page's <noscript> block */
  color: var(--color-on-noir-muted);
  margin-bottom: var(--space-lg);
}
.portal-gate__nojs a, .portal-gate__help a, .portal-gate__back a { color: var(--color-primary-on-dark); }
.portal-gate__help {
  font-size: var(--text-small);
  color: var(--color-on-noir-muted);
  margin-top: var(--space-xl);
}
.portal-gate__back { margin-top: var(--space-md); font-size: var(--text-small); }
.portal-gate__back a { color: var(--color-on-noir-muted); }

/* ---- Download card ---------------------------------------------------- */
.portal-doc {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-md);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  text-decoration: none;
  color: inherit;
  max-width: 34rem;
  transition: border-color var(--duration-base) var(--ease-out-quart),
              transform var(--duration-base) var(--ease-out-quart);
}
.portal-doc:hover { border-color: var(--color-primary); transform: translateY(-2px); }
.portal-doc__thumb {
  flex: 0 0 5.5rem;
  border: 1px solid var(--color-border);
  background: var(--color-surface-deep);
}
.portal-doc__thumb img { display: block; width: 100%; height: auto; }
.portal-doc__body { display: flex; flex-direction: column; gap: var(--space-3xs); }
.portal-doc__title { font-family: var(--font-display); font-size: var(--text-title); }
.portal-doc__meta { font-size: var(--text-label); color: var(--color-muted); letter-spacing: 0.04em; text-transform: uppercase; }
.portal-doc__cta {
  margin-top: var(--space-2xs);
  font-weight: 600;
  color: var(--color-primary-deep);
}
.portal-doc__cta::after { content: " \2193"; }

/* ---- Two-column body -------------------------------------------------- */
.portal-cols {
  display: grid;
  gap: var(--space-2xl);
}
/* 56em, not 48em: at 768px each column is ~340px, and a checklist item
   wrapped to six lines there. Two columns only once each can actually hold
   a line of prose. */
@media (min-width: 56em) {
  .portal-cols { grid-template-columns: 1fr 1fr; gap: var(--space-3xl); }
}
.portal-subtitle {
  font-family: var(--font-display);
  font-size: var(--text-title);
  margin-bottom: var(--space-md);
}
.portal-subtitle--lead { font-size: var(--text-headline); }

/* ---- Counted spec list ------------------------------------------------ */
.portal-specs { list-style: none; padding: 0; margin: 0; }
.portal-specs li {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  border-bottom: 1px solid var(--color-border);
}
.portal-specs li:first-child { border-top: 1px solid var(--color-border); }
.portal-specs__count {
  flex: 0 0 2.5rem;
  font-family: var(--font-display);
  font-size: var(--text-title);
  color: var(--color-primary-deep);
  text-align: right;
}
.portal-specs__label em { color: var(--color-muted); font-style: normal; }

/* Its own rule rather than .section-note: that one centres itself with
   margin-inline:auto for full-width bands, which reads as an accident
   inside a half-width column. */
.portal-note {
  font-size: var(--text-small);
  color: var(--color-muted);
  max-width: 52ch;
  margin-top: var(--space-lg);
  text-align: left;
}

/* A plain stacked checklist. .love-list is a two-column component sized for
   a full-width band and collapses to unreadable measures in a column. */
.portal-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-sm); }
.portal-list li { display: flex; gap: var(--space-sm); align-items: flex-start; }
.portal-list__icon {
  flex: 0 0 1.25rem;
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.2em;
  color: var(--color-primary-deep);
}

/* ---- Figures ---------------------------------------------------------- */
.portal-figure { margin: var(--space-2xl) 0 0; }
.portal-figure--flush { margin-top: var(--space-lg); }
.portal-figure img { display: block; width: 100%; height: auto; border-radius: var(--radius-md); }
.portal-figure figcaption {
  margin-top: var(--space-sm);
  font-size: var(--text-small);
  color: var(--color-muted);
}

/* ---- Cards (arbors, permits) ------------------------------------------ */
.portal-cards {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
}
@media (min-width: 34em) { .portal-cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62em) { .portal-cards { grid-template-columns: repeat(4, 1fr); } }
.portal-cards--two { grid-template-columns: 1fr; }
@media (min-width: 48em) { .portal-cards--two { grid-template-columns: repeat(2, 1fr); } }

.portal-card {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.portal-card__media { aspect-ratio: 4 / 5; background: var(--color-surface-deep); }
/* <picture> is display:inline by default, so the img's height:100% had no
   containing block to resolve against and a landscape photo letterboxed
   inside the 4:5 frame. Make the picture the block that fills the media box. */
.portal-card__media picture { display: block; width: 100%; height: 100%; }
.portal-card__media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.portal-card__body { padding: var(--space-md); }
.portal-card__title { font-family: var(--font-display); font-size: var(--text-title); margin-bottom: var(--space-sm); }

/* On the noir permits band the cards invert: the surrounding section already
   flips h2/h3/p, so only the plate itself needs restating. */
.section--noir .portal-card {
  background: var(--color-noir-surface);
  border-color: var(--color-border-on-dark);
}
.section--noir .portal-card p { color: var(--color-on-noir-muted); }
.section--noir .portal-card strong { color: var(--color-on-noir); }
.portal-card--doc .portal-card__body { padding: var(--space-lg); }
/* Multi-paragraph card copy: the base p margin is collapsed inside cards,
   so instructions ran together as one block of text. */
.portal-card--doc .portal-card__body p + p { margin-top: var(--space-sm); }
.portal-card--doc .portal-address { margin-top: var(--space-md); }
.portal-card--doc .btn { margin-top: var(--space-lg); }

.portal-dims { margin: 0; display: grid; gap: var(--space-2xs); }
.portal-dims > div { display: flex; justify-content: space-between; gap: var(--space-sm); border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-2xs); }
.portal-dims dt { font-size: var(--text-label); letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-muted); }
.portal-dims dd { margin: 0; font-weight: 500; }

/* A full hairline box with a faint tint, not a 2px gold stripe down one side
   (rewritten 2026-07-29 — side-stripe borders are an absolute ban in this
   design system, and a stripe was never what this needed anyway). What the
   element actually is: an address a couple has to copy onto an envelope. A
   closed plate says "this is a block to transcribe"; a stripe just said
   "something over here is important". */
.portal-address {
  font-style: normal;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-on-dark);
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--color-primary) 7%, transparent);
}
.portal-contact-note {
  margin-top: var(--space-xl);
  text-align: center;
  color: var(--color-on-noir-muted);
}
.portal-contact-note a { color: var(--color-primary-on-dark); }

/* The portal's rails carry floor plans, not photographs: a plan has to be
   readable, so let it sit on white with room around it rather than being
   cropped to a photo-shaped frame. */
.portal-page .rail__slide img { object-fit: contain; background: var(--color-bg); }
/* The ceremony plans are landscape (about 5:4) and the reception plans are
   portrait. Framing both as 3:4 opened ~160px of dead space above every
   ceremony plan, so each set gets the shape it actually is. */
.rail--plan-wide .rail__slide img { aspect-ratio: 5 / 4; }

/* ==========================================================================
   Print — the pricing page and the spec sheet
   A couple comparing venues prints the numbers and takes them to a partner.
   What survives is the facts: the spec table, the inclusions, the payment
   schedule. What goes is everything that only makes sense on a screen —
   the fixed header, the sticky CTA bar, the nav, the photography, the forms
   that can't be posted, and the scroll region around the spec sheet (on paper
   there is nothing to scroll).
   ========================================================================== */
@media print {
  .site-header,
  .mobile-nav,
  .mobile-cta,
  .site-footer,
  .skip-link,
  .hero__media,
  .hero__vignette,
  .lightbox,
  .cta-band__actions,
  .pricing__form,
  #brochure-form,
  .form-fallback,
  .tour-contact__scene,
  .contact__map,
  .contact__social,
  #book,
  #tour-form { display: none !important; }

  html, body { background: #fff !important; color: #000 !important; }
  body { padding-bottom: 0 !important; }
  .section, .section--noir, .section--surface {
    background: #fff !important;
    color: #000 !important;
    padding-block: 0.75rem !important;
  }
  .section--noir h2, .section--noir h3, .section--noir p,
  .section--noir li, .section--noir dt, .section--noir dd { color: #000 !important; }

  /* The scroll region exists because the table can outgrow a phone. Paper has
     no viewport to outgrow, and a clipped table is a lost table. */
  .spec-sheet__scroll { overflow: visible !important; }
  .spec-sheet { width: 100% !important; }
  .spec-sheet th, .spec-sheet td { color: #000 !important; }

  /* Never split a fact across a page break. */
  .spec-sheet tr, .love-list li, .terms-list > div, .pricing__card { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }

  /* A printed page loses every href, so put the important ones back. */
  .pricing__terms a[href^="http"]::after,
  .savings-note a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }

  a { text-decoration: underline; }
}
