/* =========================================================================
   VILLAGE POINT — DESIGN SYSTEM v1
   Production CSS component library
   Source of truth: village-point-design-system-and-component-library-v1.md
                     village-point-brand-id-v7b.html
   ========================================================================= */

/* ---------------------------------------------------------------------
   1. TOKENS
   --------------------------------------------------------------------- */
:root{

  /* -- colour -- */
  --vp-white:       #FFFFFF;
  --vp-paper:       #F7F4EC;
  --vp-ink:         #1F1F1F;
  --vp-ink-soft:    #6B6B6B;
  --vp-gold-deep:   #B3A24A;
  --vp-gold:        #CDBF63;
  --vp-gold-light:  #E3D98F;
  --vp-gilt:        #8C7D33;
  --vp-clay:        #E8A480;
  --vp-clay-ink:    #5F3A28;
  --vp-brick:       #8B3A2E;
  --vp-line:        rgba(31,31,31,0.14);
  --vp-line-soft:   rgba(31,31,31,0.08);
  --vp-parchment:   #F8F4EC; /* light warm ivory/stone card tone — Heritage card only; lightened off the beige/parchment tones of earlier passes per feedback */

  /* dark-moment (bounded charcoal sections only) */
  --vp-dark-bg:     #1F1F1F;
  --vp-dark-text:   #F5F1E8;
  --vp-dark-text-soft: #C9C2B4;
  --vp-dark-text-mute: #93897A;
  /* footer only — a warmer, slightly lighter dark than --vp-dark-bg's
     near-black, so the closing footer reads as a soft premium material
     rather than a harsh solid-black block. Deliberately its own token
     rather than changing --vp-dark-bg itself, since that's shared with
     other dark-moment sections/dark mode this shouldn't affect. */
  --vp-footer-bg:   #26241F;

  /* -- type families -- */
  --font-serif:   "EB Garamond", "Adobe Garamond Pro", Garamond, Georgia, serif;
  --font-sans:    "Inter", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
  --font-mono:    "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;
  --font-wordmark:"Josefin Sans", "Inter", sans-serif;

  /* -- type scale (desktop; clamps down for mobile automatically) -- */
  --text-display-xl: clamp(2.6rem, 1.7rem + 3.6vw, 5.25rem);   /* hero H1 68-84px */
  --text-display-l:  clamp(2rem,   1.5rem + 2.2vw, 3.5rem);    /* major H2 44-56px */
  --text-display-m:  clamp(1.75rem,1.4rem + 1.5vw, 2.625rem);  /* section H2 34-42px */
  --text-heading-s:  clamp(1.3rem, 1.15rem + 0.6vw, 1.75rem);  /* card title 22-28px */
  --text-body-l:      1.125rem;   /* 18px intro paragraph */
  --text-body:        1rem;       /* 16px normal copy */
  --text-small:       0.875rem;   /* 14px captions/meta */
  --text-eyebrow:     0.6875rem;  /* 11px labels */

  /* -- spacing scale -- */
  --space-2xs: 4px;
  --space-xs:  8px;
  --space-sm:  12px;
  --space-md:  16px;
  --space-lg:  24px;
  --space-xl:  32px;
  --space-2xl: 48px;
  --space-3xl: 72px;
  --space-4xl: 96px;
  --space-5xl: 128px;

  /* -- section rhythm -- */
  --section-pad-y:        clamp(56px, 8vw, 112px);
  --section-pad-y-major:  clamp(72px, 11vw, 160px);
  --section-pad-y-tight:  clamp(32px, 4vw, 48px);

  /* -- layout -- */
  --container-max:      1240px;
  --container-pad:      clamp(20px, 4vw, 56px);
  --container-copy:     680px;
  --container-intro:    820px;

  /* -- radii -- */
  --radius-btn: 10px;     /* primary CTA — soft rounded rectangle, not a pill */
  --radius-field: 10px;
  --radius-card: 0px;

  /* -- motion -- */
  --ease: cubic-bezier(.4,0,.2,1);
  --dur: .2s;
}

/* ---------------------------------------------------------------------
   2. RESET / BASE
   --------------------------------------------------------------------- */
*, *::before, *::after{ box-sizing: border-box; }
html{ -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body{
  margin: 0;
  background: var(--vp-white);
  color: var(--vp-ink);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--text-body);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
img{ max-width: 100%; display: block; }
ul, ol{ margin: 0; padding: 0; list-style: none; }
h1,h2,h3,h4,h5,h6{ margin: 0; font-weight: 600; }
p{ margin: 0; }
a{ color: inherit; }
button{ font: inherit; background: none; border: none; cursor: pointer; }
input, textarea, select{ font: inherit; color: inherit; }

/* visible, consistent focus ring everywhere (accessibility) */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible{
  outline: 2px solid var(--vp-gold);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------
   3. TYPOGRAPHY
   --------------------------------------------------------------------- */
.eyebrow{
  display: inline-block;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--vp-gilt);
}
.eyebrow--on-dark{ color: var(--vp-gold); opacity: .9; }

.display-xl{
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-display-xl);
  line-height: 1.02;
  letter-spacing: -0.01em;
}
.display-l{
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-display-l);
  line-height: 1.08;
}
.display-m{
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-display-m);
  line-height: 1.15;
}
.heading-s{
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-heading-s);
  line-height: 1.25;
}
.heading-s--sans{
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: var(--text-heading-s);
  line-height: 1.3;
}
.body-l{ font-size: var(--text-body-l); line-height: 1.6; color: var(--vp-ink); }
.body{ font-size: var(--text-body); line-height: 1.7; }
.body-soft{ font-size: var(--text-body); line-height: 1.7; color: var(--vp-ink-soft); }
.small{ font-size: var(--text-small); line-height: 1.5; color: var(--vp-ink-soft); }
.mono-label{
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--vp-gilt);
}

/* quiet label — used for small meta/detail text (footer headings, captions, dt's) */
.quiet-label{
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--vp-ink-soft);
}

/* wordmark — brushed-gold treatment, Josefin Sans, wide tracking */
.wordmark{
  display: inline-flex;
  align-items: center;
  font-family: var(--font-wordmark);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-right: -0.2em;
  white-space: nowrap;
  text-decoration: none;
}
.monogram{
  display: inline-flex;
  align-items: center;
  font-family: var(--font-wordmark);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-right: -0.1em;
  text-decoration: none;
}
.gold-text{
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.35 0 0 0 0 0.30 0 0 0 0 0.10 0 0 0 0.22 0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)'/%3E%3C/svg%3E"),
    linear-gradient(104deg, var(--vp-gold-deep) 0%, var(--vp-gold) 30%, var(--vp-gold-light) 52%, var(--vp-gold) 68%, var(--vp-gold-deep) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* pull quote (Garamond) */
.pull-quote{
  font-family: var(--font-serif);
  font-weight: 600;
  font-style: italic;
  font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem);
  line-height: 1.4;
  color: var(--vp-ink);
}

/* underlined inline link — gold underline */
.text-link{
  color: var(--vp-ink);
  text-decoration: underline;
  text-decoration-color: var(--vp-gilt);
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
.text-link:hover{ text-decoration-color: var(--vp-gold); }

/* ---------------------------------------------------------------------
   4. LAYOUT / GRID
   --------------------------------------------------------------------- */
.container{
  max-width: var(--container-max);
  margin: 0 auto;
  padding-left: var(--container-pad);
  padding-right: var(--container-pad);
}
.container-copy{ max-width: var(--container-copy); margin: 0 auto; }
.container-intro{ max-width: var(--container-intro); margin: 0 auto; }

.section{ padding-top: var(--section-pad-y); padding-bottom: var(--section-pad-y); }
.section--major{ padding-top: var(--section-pad-y-major); padding-bottom: var(--section-pad-y-major); }
.section--tight{ padding-top: var(--section-pad-y-tight); padding-bottom: var(--section-pad-y-tight); }
.section--paper{ background: var(--vp-paper); }
.section--dark{ background: var(--vp-dark-bg); color: var(--vp-dark-text); }
.section--bordered{ border-top: 1px solid var(--vp-line-soft); border-bottom: 1px solid var(--vp-line-soft); }

/* 12-col grid */
.grid-12{
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-lg);
}
.col-3{ grid-column: span 3; }
.col-4{ grid-column: span 4; }
.col-5{ grid-column: span 5; }
.col-6{ grid-column: span 6; }
.col-7{ grid-column: span 7; }
.col-8{ grid-column: span 8; }
.col-12{ grid-column: span 12; }

/* simple auto grids */
.grid-2{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-xl); }
.grid-3{ display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-xl); }
.grid-4{ display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-lg); }

/* section heading block */
.section-head{ margin-bottom: var(--space-2xl); max-width: var(--container-intro); }
.section-head .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.section-head h2{ margin-bottom: var(--space-sm); }
.section-head p{ color: var(--vp-ink-soft); font-size: var(--text-body-l); max-width: 62ch; }
.section-head--center{ margin-left: auto; margin-right: auto; text-align: center; }
/* .section-head p's own max-width (62ch) can end up narrower than the
   section-head block itself — without this, the paragraph box stays
   flush-left inside the centered section-head (text-align only centers
   text WITHIN the box, not the box itself), which reads as "off-centre"
   on longer paragraphs. Scoped to --center so left-aligned section-heads
   elsewhere are unaffected. */
.section-head--center p{ margin-left: auto; margin-right: auto; }

/* quiet divider with gold diamond mark */
.divider{ display: flex; align-items: center; gap: 14px; margin: var(--space-3xl) 0; }
.divider::before, .divider::after{ content:""; flex:1; height:1px; background: var(--vp-gilt); opacity:.4; }
.divider .mark{ width:6px; height:6px; background: var(--vp-gilt); transform: rotate(45deg); flex: none; }

/* ---------------------------------------------------------------------
   5. BUTTONS
   --------------------------------------------------------------------- */
.btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 46px;
  padding: 14px 26px;
  border-radius: var(--radius-btn);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  text-decoration: none;
  border: 1px solid transparent;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  white-space: nowrap;
}
.btn:hover{ opacity: .88; transform: translateY(-1px); }
.btn:active{ transform: translateY(0); opacity: .82; }

/* leading calendar icon, shared across every "Check Availability" body
   button (hero, Wedding Spaces, Final CTA) — same glyph and stroke
   treatment as the mobile floating CTA's own icon (.mobile-cta-bar__icon),
   just sized to sit comfortably inside the smaller body-button height.
   currentColor means it automatically matches each button's text
   colour (white on .btn-primary) with no extra overrides needed. */
.btn__icon{ flex: none; width: 18px; height: 18px; color: currentColor; }
.btn__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

.btn-primary{ background: var(--vp-ink); color: var(--vp-white); border-color: var(--vp-ink); }
.btn-on-dark{ background: var(--vp-white); color: var(--vp-ink); border-color: var(--vp-white); }
.btn-ghost{ background: transparent; color: var(--vp-ink); border-color: var(--vp-gilt); }
.btn-ghost:hover{ background: var(--vp-gilt); color: var(--vp-white); opacity: 1; }

.btn-block{ width: 100%; }

/* secondary text link with motion arrow */
.link-arrow{
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 500;
  color: var(--vp-ink);
  text-decoration: none;
  border-bottom: 1px solid var(--vp-gilt);
  padding-bottom: 2px;
  transition: border-color var(--dur) var(--ease), gap var(--dur) var(--ease);
}
.link-arrow:hover{ border-color: var(--vp-gold); gap: 12px; }

.link-row{ display: flex; align-items: center; gap: var(--space-xl); flex-wrap: wrap; }

/* sheen CTA — a quiet glint rather than a slide-to-unlock stripe.
   Narrow, low-opacity and slow: it should read as "there's a light
   catching this button" on close attention, not announce itself as
   an animation. Plus a mobile pop-up-from-below entrance. */
.btn-sheen{ position: relative; overflow: hidden; isolation: isolate; }
.btn-sheen::after{
  content: "";
  position: absolute;
  top: 0;
  left: -60%;
  width: 20%;
  height: 100%;
  background: linear-gradient(75deg,
    transparent 0%,
    rgba(255,255,255,0.08) 45%,
    rgba(255,255,255,0.16) 50%,
    rgba(255,255,255,0.08) 55%,
    transparent 100%);
  transform: skewX(-20deg);
  animation: btn-sheen-sweep 6s ease-in-out infinite;
  pointer-events: none;
}
.btn-sheen .btn-arrow{ display: inline-block; transition: transform var(--dur) var(--ease); }
.btn-sheen:hover .btn-arrow{ transform: translateX(3px); }

@keyframes btn-sheen-sweep{
  0%   { left: -60%; }
  32%  { left: 140%; }
  100% { left: 140%; }
}
@keyframes btn-pop-in{
  from{ opacity: 0; transform: translateY(28px); }
  to{ opacity: 1; transform: translateY(0); }
}
@media (max-width: 720px){
  .btn-sheen.hero-cta{ animation: btn-pop-in .6s var(--ease) .3s both; }
  .btn-sheen.hero-cta::after{ animation: btn-sheen-sweep 6s ease-in-out 1s infinite; }
}

/* ---------------------------------------------------------------------
   6. FORMS
   --------------------------------------------------------------------- */
.field{ margin-bottom: var(--space-lg); }
.field label{
  display: block;
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;
  color: var(--vp-ink);
  margin-bottom: 8px;
}
.field .hint{ display:block; margin-top:6px; font-size: 13px; color: var(--vp-ink-soft); }
.field .required{ color: var(--vp-brick); }

.field input[type="text"],
.field input[type="email"],
.field input[type="tel"],
.field input[type="date"],
.field input[type="number"],
.field select,
.field textarea{
  width: 100%;
  background: var(--vp-white);
  border: 1px solid rgba(31,31,31,0.16);
  border-radius: var(--radius-field);
  min-height: 48px;
  padding: 12px 14px;
  font-family: var(--font-sans);
  font-size: 16px;
  color: var(--vp-ink);
}
/* selects get the system's own chevron instead of the browser default —
   appearance:none strips the native arrow, the SVG background paints a
   matching one so it never looks like an unstyled form control. */
.field select{
  appearance: none;
  -webkit-appearance: none;
  padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B6B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 18px 18px;
  cursor: pointer;
}
.field select:invalid{ color: rgba(31,31,31,0.35); } /* placeholder option showing */
.field textarea{ min-height: 120px; resize: vertical; }
.field input:focus,
.field select:focus,
.field textarea:focus{
  outline: 2px solid var(--vp-gold);
  outline-offset: 2px;
  border-color: var(--vp-gilt);
}
.field input::placeholder,
.field textarea::placeholder{ color: rgba(31,31,31,0.35); }

.form-row{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md) var(--space-lg); }
.form-row--3{ grid-template-columns: 1fr 1fr 1fr; }

.form-panel{
  background: var(--vp-paper);
  border: 1px solid var(--vp-line-soft);
  padding: var(--space-2xl);
}

/* ---------------------------------------------------------------------
   7. IMAGE WELLS  (editorial asset containers — never dashed placeholders)
   --------------------------------------------------------------------- */
.asset-well{
  position: relative;
  background: var(--vp-paper);
  border: 1px solid var(--vp-line);
  overflow: hidden;
  background-image:
    repeating-linear-gradient(0deg, rgba(31,31,31,0.018) 0 1px, transparent 1px 3px);
}
.asset-well img{
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* small gold corner tick — signals "considered container", not "missing image" */
.asset-well::before{
  content: "";
  position: absolute;
  top: 14px;
  left: 14px;
  width: 7px;
  height: 7px;
  background: var(--vp-gold);
  transform: rotate(45deg);
  z-index: 2;
}
.asset-well--dark{
  background: var(--vp-dark-bg);
  background-image: none;
}
.asset-well--dark::before{ background: var(--vp-gold); }

.asset-caption{
  margin-top: 10px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--vp-ink-soft);
}

/* ratio modifiers */
.ratio-hero{ aspect-ratio: 16 / 9; }
.ratio-wide{ aspect-ratio: 21 / 9; }
.ratio-card{ aspect-ratio: 4 / 5; }
.ratio-portrait{ aspect-ratio: 3 / 4; }
.ratio-square{ aspect-ratio: 1 / 1; }
.ratio-landscape{ aspect-ratio: 4 / 3; }

/* ---------------------------------------------------------------------
   8. CARDS
   --------------------------------------------------------------------- */
.card{
  background: var(--vp-white);
  border: 1px solid var(--vp-line-soft);
  border-top: 3px solid var(--vp-gold);
  padding: var(--space-xl) var(--space-lg) var(--space-lg);
}
.card .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.card h3, .card h4{ font-family: var(--font-serif); font-weight: 600; font-size: 1.35rem; margin-bottom: var(--space-xs); }
.card p{ font-size: var(--text-small); color: var(--vp-ink-soft); margin-bottom: var(--space-lg); }
.card .card-link{
  font-family: var(--font-sans);
  font-size: 13px;
  letter-spacing: 0.02em;
  font-weight: 600;
  color: var(--vp-gilt);
  text-decoration: none;
}
.card .card-link:hover{ color: var(--vp-gold-deep); text-decoration: underline; }

/* event routing — one horizontal row, equal-size cards, scroll-snap.
   Matches the FigJam wireframe: simple routes, not information blocks. */
.event-row{
  display: flex;
  gap: var(--space-lg);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: var(--space-sm);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.event-row::-webkit-scrollbar{ display: none; }
.event-card{
  flex: 0 0 248px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: var(--vp-ink);
}
.event-card .asset-well{ aspect-ratio: 4/5; margin-bottom: var(--space-md); }
.event-card__title{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: 1.05rem;
  line-height: 1.25;
  white-space: nowrap;
}
.event-card__title::after{
  content: "→";
  font-family: var(--font-sans);
  font-size: 14px;
  color: var(--vp-gilt);
  transition: transform var(--dur) var(--ease);
}
.event-card:hover .event-card__title::after{ transform: translateX(4px); }

/* room / venue card (Ballroom / Lodge) */
.room-card{ background: var(--vp-white); border: 1px solid var(--vp-line); }
.room-card .asset-well{ aspect-ratio: 4/3; border: none; border-bottom: 1px solid var(--vp-line); }
.room-card__body{ padding: var(--space-xl); }
.room-card__capacity{ margin-bottom: var(--space-sm); }
.room-card h3{ font-family: var(--font-serif); font-weight: 600; font-size: var(--text-display-m); margin-bottom: var(--space-sm); }
.room-card p{ color: var(--vp-ink-soft); margin-bottom: var(--space-lg); }
.room-card ul{ margin-bottom: var(--space-lg); }
.room-card li{ position: relative; padding-left: 18px; margin-bottom: 8px; font-size: var(--text-small); color: var(--vp-ink); }
.room-card li::before{ content:""; position:absolute; left:0; top:8px; width:5px; height:5px; background: var(--vp-gilt); transform: rotate(45deg); }

/* amenity tile — thin-line icon + label + short line */
.amenity{ padding: var(--space-lg) var(--space-md); border: 1px solid var(--vp-line-soft); text-align: left; }
.amenity__icon{ width: 26px; height: 26px; margin-bottom: var(--space-md); color: var(--vp-gilt); }
.amenity__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.amenity .mono-label{ display:block; margin-bottom: var(--space-xs); }
.amenity p{ font-size: var(--text-small); color: var(--vp-ink-soft); }

/* ---------------------------------------------------------------------
   9. TESTIMONIALS — credible review widgets, matching the trust strip
   --------------------------------------------------------------------- */
.testimonial-widget{
  background: var(--vp-white);
  border: 1px solid var(--vp-line);
  border-radius: var(--radius-btn);
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.testimonial-widget__head{ display: flex; align-items: center; gap: 12px; }
.testimonial-widget__avatar{
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--vp-paper);
  border: 1px solid var(--vp-line);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 13px;
  color: var(--vp-ink);
}
.testimonial-widget__name{ display: block; font-family: var(--font-sans); font-weight: 600; font-size: 14px; color: var(--vp-ink); }
.testimonial-widget__meta{ display: block; font-family: var(--font-sans); font-size: 12px; color: var(--vp-ink-soft); }
.testimonial-widget__head .review-widget__icon{ margin-left: auto; width: 26px; height: 26px; font-size: 12px; }
.testimonial-widget__text{ font-family: var(--font-sans); font-size: 14px; line-height: 1.6; color: var(--vp-ink); }

/* ---------------------------------------------------------------------
   11. CTA BANNER
   --------------------------------------------------------------------- */
.cta-banner{
  text-align: center;
  padding: var(--space-4xl) var(--space-lg);
  background: var(--vp-paper);
  border-top: 1px solid var(--vp-line-soft);
  border-bottom: 1px solid var(--vp-line-soft);
}
.cta-banner .eyebrow{ display: block; margin-bottom: var(--space-md); }
.cta-banner h2{ margin-bottom: var(--space-sm); }
.cta-banner p{ color: var(--vp-ink-soft); max-width: 46ch; margin: 0 auto var(--space-xl); }
.cta-banner--dark{ background: var(--vp-dark-bg); color: var(--vp-dark-text); border: none; }
.cta-banner--dark p{ color: var(--vp-dark-text-soft); }

/* ---------------------------------------------------------------------
   12. FAQ ACCORDION
   --------------------------------------------------------------------- */
.faq-list{ width: 100%; }
.faq-item{ border-bottom: 1px solid var(--vp-line-soft); }
.faq-item:first-child{ border-top: 1px solid var(--vp-line-soft); }
.faq-question{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  padding: 22px 4px;
  text-align: left;
  font-family: var(--font-sans);
  font-size: 1rem;
  font-weight: 500;
  color: var(--vp-ink);
}
.faq-question .icon{
  position: relative;
  flex: none;
  width: 16px;
  height: 16px;
}
.faq-question .icon::before,
.faq-question .icon::after{
  content: "";
  position: absolute;
  background: var(--vp-gilt);
  transition: transform var(--dur) var(--ease);
}
.faq-question .icon::before{ left:0; top:7px; width:16px; height:1.5px; }
.faq-question .icon::after{ left:7px; top:0; width:1.5px; height:16px; }
.faq-item.is-open .faq-question .icon::after{ transform: rotate(90deg); opacity: 0; }
.faq-answer{ max-height: 0; overflow: hidden; transition: max-height .32s var(--ease); }
.faq-item.is-open .faq-answer{ max-height: 400px; } /* raised from 300px — the weddings-landing.html FAQ answers run a little longer than the component-library sample */
/* was max-width:62ch (~600px) — reasonable as a line-length rule on
   its own, but the FAQ container is full-page-width (see task #15,
   widened deliberately), so against that width a 600px answer read
   as accidentally half-width/cramped rather than a deliberate
   measure. A fixed px value in the middle of a normal readable range
   keeps it clearly intentional without stretching all the way across
   the very wide container. */
.faq-answer p{ padding: 0 4px 22px; color: var(--vp-ink-soft); font-size: var(--text-body); max-width: 780px; }

/* ---------------------------------------------------------------------
   13. CONTACT / LOCATION — access-focused, not a lead-capture form
   --------------------------------------------------------------------- */
.contact-grid{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2xl); align-items: stretch; }

.location-access{ display: flex; flex-direction: column; gap: var(--space-lg); margin: var(--space-xl) 0; }
.location-access__item{ display: flex; gap: 14px; align-items: flex-start; }
.location-access__icon{ flex: none; width: 24px; height: 24px; margin-top: 2px; color: var(--vp-gilt); }
.location-access__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.location-access__item h4{ font-family: var(--font-sans); font-weight: 600; font-size: 15px; margin-bottom: 4px; }
.location-access__item p{ font-size: var(--text-small); color: var(--vp-ink-soft); }

.location-supporting{ margin-top: var(--space-xl); padding-top: var(--space-lg); border-top: 1px solid var(--vp-line-soft); }
.location-supporting dt{ font-family: var(--font-sans); font-weight: 600; font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vp-ink-soft); margin-top: var(--space-md); }
.location-supporting dt:first-child{ margin-top: 0; }
.location-supporting dd{ margin: 2px 0 0; font-size: var(--text-small); }
.location-supporting dd a{ color: var(--vp-ink); text-decoration: underline; text-decoration-color: var(--vp-gilt); text-underline-offset: 3px; }

/* ---------------------------------------------------------------------
   14. HEADER
   --------------------------------------------------------------------- */
.site-header{
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: 0 clamp(20px, 4vw, 56px);
  min-height: 60px;
  background: var(--vp-white);
  border-bottom: 1px solid var(--vp-line-soft);
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* wordmark sized and positioned to share a visual baseline with the nav labels,
   rather than floating above them at a larger, disconnected scale */
.site-header .wordmark{
  font-size: clamp(1.05rem, 0.5vw + 0.95rem, 1.2rem);
  text-decoration: none;
  position: relative;
  top: 1px; /* optical correction: Josefin Sans caps sit slightly high next to Inter */
}
.site-header .monogram{ font-size: 1.05rem; text-decoration: none; }

/* mobile nav uses the VP monogram, not the full wordmark, per brand rules —
   swap happens automatically at the header's own breakpoint */
.site-header .wordmark--full{ display: inline-flex; }
.site-header .monogram--compact{ display: none; }
@media (max-width: 720px){
  .site-header{ min-height: 56px; }
  .site-header .wordmark--full{ display: none; }
  .site-header .monogram--compact{ display: inline-flex; }
}
/* Landing-page override: this funnel header keeps the full Village Point
   wordmark on mobile too, rather than swapping to the VP monogram —
   scoped to .site-header--centered so the component-library header (with
   real nav) keeps its monogram swap intact. Same specificity as the rule
   above, so source order (this comes after) makes it win. */
@media (max-width: 720px){
  .site-header--centered .wordmark--full{ display: inline-flex; }
  .site-header--centered .monogram--compact{ display: none; }
}
.primary-nav ul{ display: flex; align-items: center; gap: var(--space-xl); }
.primary-nav a{
  display: inline-flex;
  align-items: center;
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  color: var(--vp-ink);
  text-decoration: none;
}
.primary-nav a:hover{ color: var(--vp-gilt); }
.header-actions{ display: flex; align-items: center; gap: var(--space-lg); }
.nav-toggle{ display: none; }

/* landing-page header — funnel pages drop the nav entirely and centre the
   wordmark, so there's nothing to click except the hero CTA */
.site-header--centered{ justify-content: center; }

/* dark-mode switcher — sits top-right regardless of header layout
   (centred wordmark or full nav), same thin-line icon language as the
   amenity/trust icons elsewhere in the system. */
.theme-toggle{
  position: absolute;
  right: var(--container-pad);
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--vp-line);
  color: var(--vp-ink-soft);
  flex: none;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.theme-toggle:hover{ border-color: var(--vp-gilt); color: var(--vp-gilt); transform: translateY(-50%) translateY(-1px); }
.theme-toggle:active{ transform: translateY(-50%); }
.theme-toggle__icon{ display: flex; align-items: center; justify-content: center; width: 19px; height: 19px; }
.theme-toggle__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
/* default (light theme): show moon = "tap for dark". Dark theme: show
   sun = "tap for light" — the icon always signals the destination. */
.theme-toggle__icon--sun{ display: none; }
html[data-theme="dark"] .theme-toggle__icon--sun{ display: flex; }
html[data-theme="dark"] .theme-toggle__icon--moon{ display: none; }

@media (max-width: 720px){
  .theme-toggle{ width: 36px; height: 36px; }
}

/* mobile header preview (fixed-width documentation frame) */
.mobile-frame{
  width: 390px;
  max-width: 100%;
  margin: 0 auto;
  border: 1px solid var(--vp-line);
  overflow: hidden;
}
.mobile-header{
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 20px;
  background: var(--vp-white);
  border-bottom: 1px solid var(--vp-line-soft);
}
.mobile-header .monogram{ font-size: 1.1rem; }
.hamburger{ display: flex; flex-direction: column; gap: 5px; width: 22px; }
.hamburger span{ height: 2px; background: var(--vp-ink); display:block; }
.mobile-body{ padding: 40px 24px; background: var(--vp-paper); text-align: center; }

/* ---------------------------------------------------------------------
   14b. INTRO / BUILDING FRAME
   --------------------------------------------------------------------- */
.intro-frame{
  background: var(--vp-paper);
  border: 1px solid var(--vp-line);
  padding: var(--space-3xl) var(--space-2xl);
}
.intro-frame .grid-2{ align-items: center; gap: var(--space-2xl); }
.intro-frame .asset-well{ aspect-ratio: 4/3; }
.intro-frame .asset-well.ratio-portrait{ aspect-ratio: 3/4; } /* override for tall/vertical source photography */
.intro-frame .eyebrow{ display: block; margin-bottom: var(--space-md); }
.intro-frame h2{ margin-bottom: var(--space-md); }
.intro-frame p{ color: var(--vp-ink-soft); max-width: 46ch; margin-bottom: var(--space-lg); }

/* ---------------------------------------------------------------------
   15. HERO
   --------------------------------------------------------------------- */
/* NOTE: .hero sets vertical rhythm only (longhand top/bottom). Never use
   the `padding` shorthand here — a 3-value shorthand (top / left-right /
   bottom) resets padding-left/right too, which silently zeroes out
   .hero-grid's own side padding wherever the two rules share specificity.
   That bug shipped in an earlier version — keep top/bottom longhand only. */
.hero{ position: relative; background: #FBFAF6; padding-top: var(--section-pad-y-major); padding-bottom: var(--space-3xl); }

/* subtle organic paper texture — desktop only. CSS/SVG turbulence, no
   image asset, no architectural line-art. It only ever shows in the
   hero's empty white space since the photography sits opaquely on top —
   atmosphere, not decoration. */
@media (min-width: 721px){
  .hero::before{
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320'%3E%3Cfilter id='vpgrain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.012' numOctaves='4' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.72  0 0 0 0 0.65  0 0 0 0 0.42  0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='320' height='320' filter='url(%23vpgrain)'/%3E%3C/svg%3E");
    background-size: 340px 340px;
  }
}

/* .hero-grid carries its own max-width + side padding (it IS the
   container for this section) so .hero itself can go full-bleed on
   mobile without fighting a second nested .container. Sits above the
   texture layer. */
.hero-grid{
  position: relative;
  z-index: 1;
  max-width: var(--container-max);
  margin: 0 auto;
  padding-left: var(--container-pad);
  padding-right: var(--container-pad);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3xl);
  align-items: center;
}
.hero-copy .eyebrow{ display: block; margin-bottom: var(--space-md); }
.hero-copy h1{ margin-bottom: var(--space-lg); }
.hero-copy .body-l{ margin-bottom: var(--space-xl); max-width: 46ch; }

/* TRUST STRIP, now living inside the hero itself rather than as a
   separate section below it. Left-aligned on desktop to sit flush
   with the rest of the left column; reuses the shared .press-logos
   family as-is (same per-logo sizing/muted treatment), just
   re-justified — see .hero-trust .press-logos below. Sits below the
   CTA, completing the column's rhythm: eyebrow -> heading -> body ->
   CTA -> trust line -> logos. Margin widened again from an earlier
   pass (was space-xl) — that read as too close to the CTA cluster;
   this reads as a distinct secondary proof layer, sitting further
   down the column, closer to the level of the image's lower half. */
.hero-trust{ margin-top: var(--space-3xl); }
.hero-trust__line{
  font-family: var(--font-sans);
  font-size: 13px;
  letter-spacing: 0.03em;
  color: var(--vp-ink-soft);
  margin-bottom: var(--space-sm);
}
.hero-trust .press-logos{ justify-content: flex-start; }

/* compact, FLAT social-proof widget — sits in the hero's lower-left
   space beneath the CTA. No fill at all (background: transparent) so
   it reads as the same surface as the hero itself, not a floating UI
   card — only a hairline border defines its edge. Two stat blocks
   (Google / Facebook) separated by a single vertical rule, each with
   its own real brand mark (not a generic letter-in-a-circle) so the
   source is instantly recognisable — same pairing as the trust
   strip's review widgets, just a quieter, flatter treatment suited
   to sitting inside the hero itself. */
.hero-social-proof{
  display: inline-flex;
  align-items: stretch;
  margin-top: var(--space-2xl);
  padding: 4px;
  background: transparent;
  border: 1px solid var(--vp-line);
  border-radius: 14px;
}
.social-proof-block{
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
}
.social-proof-block__icon{ flex: none; width: 22px; height: 22px; }
.social-proof-block__icon svg{ display: block; width: 100%; height: 100%; }
.social-proof-block__body{ display: flex; flex-direction: column; gap: 2px; line-height: 1.2; }
.social-proof-block__rating{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 700;
  color: var(--vp-ink);
}
.social-proof-block__stars{ color: var(--vp-gold-deep); font-size: 11px; letter-spacing: 1px; }
/* fractional star rating — a muted "track" row of 5 stars sits
   underneath a gold "fill" row of the same 5 stars, and the fill row
   is clipped to a percentage width (rating/5, e.g. 4.6/5 = 92%) via
   overflow:hidden on an absolutely-positioned wrapper. Reads as a
   genuinely partial star rather than always rounding up to a full
   5-star row. */
.star-rating{
  position: relative;
  display: inline-block;
  font-size: 11px;
  letter-spacing: 1px;
  line-height: 1;
}
.star-rating__track{ color: var(--vp-line); }
.star-rating__fill{
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  white-space: nowrap;
  color: var(--vp-gold-deep);
}
.social-proof-block__count{ font-family: var(--font-sans); font-size: 12px; color: var(--vp-ink-soft); }
.social-proof-divider{ flex: none; width: 1px; align-self: stretch; margin: 10px 0; background: var(--vp-line); }

/* two placements of the same widget, toggled by breakpoint like the
   hero-visual desktop/mobile images — never both visible at once */
.hero-social-proof--mobile{ display: none; }
.hero-visual{ position: relative; }
.hero-visual .asset-well{ aspect-ratio: 3/4; }

/* review widget, overlaid on the main hero photo (desktop only) —
   anchored to the lower-left area of the image, inset from the edges
   so it reads as intentionally placed on the photo rather than
   floating loose over it. .asset-well is already position:relative +
   overflow:hidden, so this just needs its own absolute offset within
   that box. */
.hero-visual__proof{
  position: absolute;
  left: 24px;
  right: 24px;
  bottom: 24px;
  z-index: 2;
  display: flex;
  justify-content: center;
}
/* the base .hero-social-proof is deliberately transparent/flat — right
   for sitting on the plain hero background (still used by the mobile
   placement below the photo). Overlaid directly on a photo, flat
   transparent would be illegible against busy image content, so this
   modifier swaps in a soft, mostly-opaque white surface, a gentle
   blur, and a very light shadow — enough to guarantee contrast without
   reading as a heavy card dropped on top of the photo. Two classes,
   not one, so this reliably wins over the base rule regardless of
   source order. */
.hero-social-proof.hero-social-proof--overlay{
  background: rgba(255,255,255,0.92);
  border-color: rgba(255,255,255,0.6);
  box-shadow: 0 12px 28px rgba(31,27,20,0.14);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* responsive art direction — separate crops for desktop vs mobile,
   not the same image squeezed to fit both */
.hero-visual--mobile{ display: none; }

/* ---- mobile: controlled-height image up top, text follows in normal
   flow below it — NOT the old full-bleed-image-with-text-overlaid-on-
   top approach. That version made the hero effectively one full
   viewport tall before any message appeared. Now the image is capped
   at ~50vh and the text starts immediately after it (pulled up
   slightly to overlap the image's own bottom fade, so the join still
   reads as one soft blend rather than a hard cut). Image visually
   leads via flex `order`, not DOM order — .hero-copy stays first in
   the markup/reading order, .hero-visual is just reordered to render
   above it, so this is a presentation change only. ---- */
@media (max-width: 720px){
  .hero{ padding-top: 0; padding-bottom: 0; }
  .hero-grid{
    display: flex;
    flex-direction: column;
    /* .hero-grid's desktop rule sets align-items:center for the two-
       column grid layout — carried unchanged into a column flex
       context, that centers each item on the cross axis (horizontal)
       and lets it shrink-wrap to its own content width instead of
       stretching full-width. That was the actual cause of the mobile
       hero image reading as inset/framed with side gutters: the image
       well was never full width to begin with. Reset to stretch so
       both .hero-visual and .hero-copy fill the full viewport width. */
    align-items: stretch;
    position: relative;
    max-width: none;
    padding-left: 0;
    padding-right: 0;
  }

  .hero-visual{ order: -1; position: relative; height: 50vh; overflow: hidden; width: 100%; }
  .hero-visual--desktop{ display: none; }
  .hero-visual--mobile{ display: block; height: 100%; width: 100%; }
  .hero-visual--mobile .asset-well{
    height: 100%;
    width: 100%;
    aspect-ratio: auto;
    border: none;
    border-radius: 0;
    margin: 0;
    /* show the floral/arch detail rather than the ceiling/top of the
       frame, now that the crop is much shorter than the old full-height
       version */
    position: relative;
  }
  /* the gold corner-tick decoration (.asset-well::before) is meant for
     bordered/inset image wells elsewhere on the page — on a full-bleed
     edge-to-edge mobile hero image it reads as a stray mark rather
     than a deliberate accent, so it's suppressed here specifically. */
  .hero-visual--mobile .asset-well::before{ display: none; }
  .hero-visual--mobile .asset-well img{ object-position: center 30%; }
  /* soft fade at the image's own bottom edge — this is what .hero-copy
     (below) overlaps into, so the transition from photo to text
     background still reads as one continuous blend rather than a
     hard line, even though the image is no longer full-bleed behind
     the text. Taller and multi-stop rather than a flat two-stop
     gradient — a short/steep fade reads as a hard horizontal edge cut
     across the photo; this builds gradually across ~1/3 of the image
     height, staying fully transparent well past the review widget so
     it never washes that out, and doesn't reach full white until
     right at the very bottom edge. */
  .hero-visual--mobile .asset-well::after{
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 46%;
    /* ends at #FBFAF6 (the hero's own background), not pure white —
       otherwise the fade's own end color mismatched the section
       background it's fading into, and the seam between the two
       showed up as a visible line rather than a smooth blend. */
    background: linear-gradient(to bottom,
      rgba(251,250,246,0) 0%,
      rgba(251,250,246,0) 40%,
      rgba(251,250,246,0.22) 60%,
      rgba(251,250,246,0.55) 78%,
      rgba(251,250,246,0.85) 90%,
      #FBFAF6 100%);
    pointer-events: none;
  }

  .hero-copy{
    position: relative;
    z-index: 2;
    /* pulled up to overlap the image's fade zone above — this is the
       "text begins naturally after / overlapping the lower fade, not
       miles below" behaviour; small negative margin, not a full
       re-overlay of the photo */
    margin-top: -56px;
    padding: 0 var(--container-pad) var(--space-2xl);
  }
  .hero-copy .body-l{ max-width: none; }
  .hero-social-proof--desktop{ display: none; }

  /* The in-hero CTA button is hidden on mobile again — the floating
     Check Availability bar (mobile-cta-bar, fixed to the viewport) is
     the single mobile conversion action, so a second identical black
     button stacked directly above it in the content flow was
     redundant. Desktop is unaffected — .link-row still renders there.
     The media trust strip stays visible on mobile, now sitting
     directly under the body copy since the CTA row above it is gone;
     reading order down the mobile hero is: wordmark (header, above),
     hero image with the review widget overlaid on it, then eyebrow,
     headline, body copy, and finally the trust strip. */
  .hero-copy .link-row{ display: none; }
  .hero-trust{ text-align: center; }
  .hero-trust .press-logos{ justify-content: center; }

  /* the review widget now lives INSIDE the mobile image well itself
     (.hero-visual__proof, shared with desktop), overlapping the
     image's lower/transition zone — not below the text cluster. The
     base rule hides it by default (display:none) so it only ever
     shows via this override; positioning comes from the shared
     .hero-visual__proof absolute+centered rule, so no separate
     width/margin placement is needed here any more, just the sizing. */
  .hero-social-proof--mobile{ display: flex; }
  /* same flat card, slightly denser so it stays proportional against
     the smaller mobile hero copy rather than the desktop-sized version
     just being squeezed in */
  .hero-social-proof--mobile .social-proof-block{ padding: 8px 14px; gap: 8px; }
  .hero-social-proof--mobile .social-proof-block__icon{ width: 18px; height: 18px; }
  .hero-social-proof--mobile .social-proof-block__rating{ font-size: 13px; }
  .hero-social-proof--mobile .social-proof-block__stars{ font-size: 10px; }
  .hero-social-proof--mobile .social-proof-block__count{ font-size: 11px; }
  .hero-social-proof--mobile .social-proof-divider{ margin: 8px 0; }
}

/* ---------------------------------------------------------------------
   16. TRUST STRIP (compact utility band)
   --------------------------------------------------------------------- */
.trust-strip{
  padding: var(--section-pad-y-tight) 0;
  border-top: 1px solid var(--vp-line-soft);
  border-bottom: 1px solid var(--vp-line-soft);
  background: var(--vp-paper);
}
.trust-strip .container{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  text-align: center;
}
.trust-strip__headline{
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 16px;
  color: var(--vp-ink);
}

/* review widgets — self-contained social-proof cards, not dull subtext */
.review-widgets{ display: flex; align-items: center; justify-content: center; gap: var(--space-md); flex-wrap: wrap; }
.review-widget{
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--vp-white);
  border: 1px solid var(--vp-line);
  border-radius: var(--radius-btn);
  padding: 10px 18px 10px 10px;
}
.review-widget__icon{
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 15px;
  color: #FFFFFF;
}
.review-widget__icon--google{ background: #4285F4; }
.review-widget__icon--facebook{ background: #1877F2; }
.review-widget__body{ text-align: left; }
.review-widget__stars{
  display: block;
  color: var(--vp-gold-deep);
  font-size: 13px;
  letter-spacing: 2px;
  line-height: 1;
}
.review-widget__meta{
  display: block;
  margin-top: 4px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--vp-ink-soft);
}

/* ---------------------------------------------------------------------
   17. HERITAGE CARD — Section 10. A single bounded, light warm-ivory/
   stone card sitting on the white page. The client-supplied façade
   illustration is used as-is (no re-cropping, no regenerating, same
   zoom level throughout). Composition pass: the illustration has a
   modest inset on the right only (var(--space-lg) desktop /
   var(--space-md) mobile) — top, left and bottom stay flush, so the
   roofline reaches the card's own top edge (an earlier pass also
   inset the top and read as a visible gap above the artwork) while it
   still runs full-width underneath the copy. Because the inset shows
   actual card parchment rather than a different-toned frame, it reads
   as breathing room on the same sheet, not a boxed-in image.
   object-position is pushed right/up
   (72% 30% desktop, 78% 28% mobile) so the composition is deliberately
   off-centre — building anchored right, not centred in the card —
   with more of its own detail (windows, cornice) surviving into the
   visible right-hand portion. Legibility on the text side is handled
   by a SEPARATE gradient "wash" layer (.heritage-card__wash) sitting
   on top of the image rather than by masking the image itself, tuned
   tighter than the previous pass so the fade starts closer to the
   text block and clears earlier, leaving more façade visible on the
   right. Layer order: facade (1) → wash (2) → paper grain (3) →
   copy (4).
   Mobile: same inset-from-top/right approach, same "off to the right"
   object-position logic. The wash switches to a 135deg diagonal
   (opaque top-left, clear bottom-right) instead of desktop's left-to-
   right or the previous pass's top-to-bottom — since mobile copy is
   left-aligned and starts top-left, this protects the text without
   requiring a uniformly strong wash across the whole card, so the
   doorway/window detail toward the lower-right stays legible.
   --------------------------------------------------------------------- */
.heritage-card{
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: clamp(400px, 34vw, 520px);
  display: flex;
  align-items: center;
  background: var(--vp-parchment);
  border: 1px solid var(--vp-line);
  border-radius: var(--radius-card);
}
.heritage-card__facade{
  position: absolute;
  top: 0;
  right: var(--space-lg);
  bottom: 0;
  left: 0;
  object-fit: cover;
  object-position: 72% 30%;
  z-index: 1;
  opacity: .68;
  mix-blend-mode: multiply;
  pointer-events: none;
}
.heritage-card__wash{
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to right,
    rgba(248,244,236,1)    0%,
    rgba(248,244,236,.95) 20%,
    rgba(248,244,236,.75) 32%,
    rgba(248,244,236,.40) 46%,
    rgba(248,244,236,.12) 60%,
    rgba(248,244,236,0)   74%);
}
.heritage-card__grain{
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  opacity: .1;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='220'%20height='220'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='3'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
}
.heritage-card__copy{ position: relative; z-index: 4; padding: var(--space-4xl) var(--space-2xl); max-width: 460px; }
.heritage-card__copy .eyebrow{ display: block; margin-bottom: var(--space-md); }
.heritage-card__copy h2{ color: var(--vp-ink); margin-bottom: var(--space-md); }
.heritage-card__copy p{ font-size: var(--text-body); line-height: 1.7; color: var(--vp-ink-soft); max-width: 38ch; }

@media (max-width: 720px){
  /* still a single, compact card — no separate visual block, no extra
     height reserved for the image. Facade stays full-bleed (inset:0)
     within this same card; only the wash direction and strength
     change versus desktop. */
  .heritage-card{ min-height: 0; align-items: flex-start; }
  .heritage-card__facade{
    top: 0;
    right: var(--space-md);
    object-position: 78% 28%;
    opacity: .5;
  }
  .heritage-card__wash{
    background: linear-gradient(135deg,
      rgba(248,244,236,.92) 0%,
      rgba(248,244,236,.78) 22%,
      rgba(248,244,236,.52) 38%,
      rgba(248,244,236,.28) 55%,
      rgba(248,244,236,.10) 72%,
      rgba(248,244,236,0)   88%);
  }
  .heritage-card__copy{ padding: var(--space-2xl) var(--space-lg); max-width: none; }
  .heritage-card__copy p{ max-width: none; }
}

/* ---------------------------------------------------------------------
   18. FOOTER — centred, considered close to the page (bounded dark moment)
   --------------------------------------------------------------------- */
.site-footer{ background: var(--vp-dark-bg); color: var(--vp-dark-text); padding: var(--space-4xl) 0 var(--space-xl); text-align: center; }
.footer-center{ display: flex; flex-direction: column; align-items: center; }
.footer-wordmark{ font-size: 1.5rem; text-decoration: none; }
.footer-tagline{ margin-top: 14px; max-width: 42ch; color: var(--vp-dark-text-soft); font-size: var(--text-small); }

.footer-divider{ width: 100%; max-width: 120px; display: flex; align-items: center; justify-content: center; margin: var(--space-2xl) 0; }
.footer-divider .mark{ width: 6px; height: 6px; background: var(--vp-gold); transform: rotate(45deg); }

.footer-nav{ display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px 28px; margin-bottom: var(--space-2xl); }
.footer-nav a{ color: var(--vp-dark-text); text-decoration: none; font-size: var(--text-small); }
.footer-nav a:hover{ color: var(--vp-gold); }

.footer-cta{ margin-bottom: var(--space-2xl); }
.footer-contact{ font-size: var(--text-small); color: var(--vp-dark-text-soft); margin-bottom: var(--space-2xl); }
.footer-contact a{ color: var(--vp-dark-text-soft); text-decoration: underline; text-decoration-color: rgba(205,191,99,0.4); text-underline-offset: 3px; }

.footer-legal{
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding-top: var(--space-xl);
  border-top: 1px solid rgba(245,241,232,0.12);
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--vp-dark-text-mute);
}
.footer-legal__links a{ color: var(--vp-dark-text-mute); text-decoration: none; margin: 0 2px; }
.footer-legal__links a:hover{ color: var(--vp-gold); }

/* ---------------------------------------------------------------------
   18b. LANDING FOOTER (weddings-landing.html)
   Deliberately minimal — this is a single-conversion paid landing
   page, not the full site, so it carries none of .site-footer's nav/
   contact/legal-link rows. Just a calm closing note: wordmark and the
   founding date (the legal placeholder line still exists in markup,
   currently hidden via .is-hidden-temp — see the FOOTER comment in
   weddings-landing.html).
   Refinement pass: background moved off pure --vp-dark-bg black onto
   its own warmer --vp-footer-bg (#26241F) — a soft charcoal-brown,
   still dark enough for the gold wordmark to pop, just not a harsh
   solid-black block against the rest of the page's light palette. A
   very low-opacity noise layer (::before, same feTurbulence recipe
   used elsewhere on the page, e.g. Heritage's paper grain) sits over
   it for a soft material feel rather than a flat colour fill — needs
   its own z-index + .landing-footer__inner{position:relative;z-index:1}
   so the noise stays behind the actual text rather than washing over
   it, since a position:absolute, z-index:auto pseudo-element would
   otherwise paint above normal in-flow content by default.
   --------------------------------------------------------------------- */
.landing-footer{
  position: relative;
  background: var(--vp-footer-bg);
  color: var(--vp-dark-text);
  padding: var(--space-2xl) 0;
  text-align: center;
}
.landing-footer::before{
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: .05;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='3'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}
.landing-footer__inner{
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.landing-footer__wordmark{ font-size: 1.15rem; }
/* "— 1881 —" as real rules either side of the text (::before/::after
   on the flex row), not literal em-dash characters — stays crisp at
   any size and the line length is independently controllable from the
   text. Warmer/deeper than the page's brighter --vp-gold (used for the
   wordmark's own shimmer treatment) so the two don't visually compete —
   this reads as a quieter, secondary mark underneath the wordmark. */
.landing-footer__est{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: var(--space-lg) 0 0;
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  color: var(--vp-gold-deep);
}
.landing-footer__est::before,
.landing-footer__est::after{
  content: "";
  width: 28px;
  height: 1px;
  background: currentColor;
  opacity: .55;
}
.landing-footer__legal{
  margin: var(--space-lg) 0 0;
  max-width: 56ch;
  font-family: var(--font-sans);
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.01em;
  color: var(--vp-dark-text-mute);
}
@media (max-width: 720px){
  .landing-footer{ padding: var(--space-xl) 0; }
  .landing-footer__legal{ max-width: 40ch; }
}

/* ---------------------------------------------------------------------
   19. GALLERY — masonry proof grid only (single approved pattern)
   --------------------------------------------------------------------- */
.gallery-masonry{ columns: 3; column-gap: var(--space-md); }
.gallery-masonry .asset-well{ margin-bottom: var(--space-md); break-inside: avoid; }
.gallery-masonry .asset-well:nth-child(3n+1){ aspect-ratio: 3/4; }
.gallery-masonry .asset-well:nth-child(3n+2){ aspect-ratio: 1/1; }
.gallery-masonry .asset-well:nth-child(3n+3){ aspect-ratio: 4/5; }

/* ---------------------------------------------------------------------
   20. UTILITIES
   --------------------------------------------------------------------- */
.u-center{ text-align: center; }
.u-mt-0{ margin-top: 0; }
.u-mb-0{ margin-bottom: 0; }
.u-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;
}

/* ---------------------------------------------------------------------
   20b. MOBILE CHECK AVAILABILITY — floating pill (default, visible on
   load — not scroll-triggered) that expands into a single-panel sheet.
   --------------------------------------------------------------------- */
.mobile-cta-bar{
  position: fixed;
  left: 20px;
  right: 20px;
  bottom: 20px;
  z-index: 60;
  display: none; /* only ever shown on small screens */
  flex-direction: column;
  align-items: center;
  gap: 8px;
  /* longer than the page's default --dur (0.2s) — this now drives a
     genuine scroll-triggered slide (see script.js's scroll-visibility
     block), not just a quick show/hide on sheet open/close, so it
     gets its own slightly slower, more deliberate timing to read as
     an intentional slide rather than a snap. */
  transition: opacity 0.32s var(--ease), transform 0.32s var(--ease);
}
.mobile-cta-bar.is-hidden{ opacity: 0; transform: translateY(28px); pointer-events: none; }
/* same button family as the desktop CTA — black fill, same radius token,
   same press/hover motion language as .btn-primary.

   This pill floats over whatever the visitor has scrolled to — including
   the Real Weddings photography, which has its own dark passages
   (foliage, evening shots, black tuxedos). A plain black-on-black
   border used to let the pill's edge disappear into those. Fixed with
   a thin gold gradient ring (same stop order as .gold-text, so it
   reads as the same "gold" everywhere on the page) plus a dark
   hairline just outside it — the gold gives it a distinct silhouette
   against dark photos, the hairline does the same against bright ones,
   and the fill stays solid black either way. */
.mobile-cta-bar__pill{
  display: flex;
  align-items: center;
  justify-content: center; /* icon+label+chevron read as one centred
    group, matching the desktop CTA, instead of label stretching to
    fill the row and reading as left-leaning */
  gap: 12px;
  width: 100%;
  background:
    linear-gradient(var(--vp-ink), var(--vp-ink)) padding-box,
    linear-gradient(120deg, var(--vp-gold-deep) 0%, var(--vp-gold) 35%, var(--vp-gold-light) 55%, var(--vp-gold) 75%, var(--vp-gold-deep) 100%) border-box;
  border: 1.5px solid transparent;
  border-radius: var(--radius-btn);
  padding: 16px 20px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.38), 0 0 0 1px rgba(0,0,0,0.5);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.mobile-cta-bar__pill:active{ opacity: .88; transform: translateY(1px); }
.mobile-cta-bar__icon{ flex: none; width: 20px; height: 20px; color: var(--vp-white); }
.mobile-cta-bar__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.mobile-cta-bar__label{ flex: none; font-family: var(--font-sans); font-weight: 600; font-size: 15px; color: var(--vp-white); }
.mobile-cta-bar__chevron{ flex: none; width: 18px; height: 18px; color: rgba(255,255,255,0.72); }
.mobile-cta-bar__chevron svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

.mobile-sheet-backdrop{
  position: fixed; inset: 0; z-index: 70;
  background: rgba(31,31,31,0.42);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}
.mobile-sheet-backdrop.is-open{ opacity: 1; pointer-events: auto; }

/* Fixed-height panel on BOTH breakpoints — Step 1, Step 2 and the
   thank-you state all share the same box height (mobile: min(600px,
   86vh); desktop: 640px, see the min-width:721px block below) so
   nothing visibly resizes when moving between them. display:flex here
   plus flex:1/min-height:0 on .mobile-sheet__step (below) is what lets
   each step's own content align within that fixed box rather than
   dictating it — was previously desktop-only; now shared so the
   mobile bottom sheet gets the same "same height every step"
   guarantee the desktop modal already had. */
.mobile-sheet{
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  background: var(--vp-white);
  border-radius: 20px 20px 0 0;
  padding: 12px 24px calc(28px + env(safe-area-inset-bottom, 0px));
  height: min(600px, 86vh);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  transform: translateY(100%);
  transition: transform .32s var(--ease);
}
.mobile-sheet.is-open{ transform: translateY(0); }
.mobile-sheet__handle{ flex: none; width: 36px; height: 4px; border-radius: 2px; background: var(--vp-line); margin: 4px auto 18px; cursor: pointer; }
.mobile-sheet__header{ display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; }
.mobile-sheet__step-label{
  display: block;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--vp-gilt);
  margin-bottom: 4px;
}
.mobile-sheet__actions{ display: flex; gap: 12px; margin-top: var(--space-sm); }
.mobile-sheet__actions .btn{ flex: 1; }
.mobile-sheet__close{
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  color: var(--vp-ink-soft);
  border-radius: 50%;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.mobile-sheet__close:hover{ color: var(--vp-ink); background: rgba(31,31,31,0.06); }
.mobile-sheet__close svg{ width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.mobile-sheet h3{ font-family: var(--font-serif); font-weight: 600; font-size: 1.3rem; }
.mobile-sheet__subtext{ color: var(--vp-ink-soft); font-size: var(--text-small); margin: 6px 0 16px; }

/* Step 1 selections, echoed back at the top of Step 2 once populated —
   quiet confirmation that nothing was lost between steps. Hidden by
   default; script.js fills it in and un-hides it on Continue. */
.mobile-sheet__summary{ margin: -8px 0 16px; font-size: 13px; color: var(--vp-ink-soft); }
.mobile-sheet__summary strong{ color: var(--vp-ink); font-weight: 600; }

.mobile-sheet__step[hidden]{ display: none; }
/* every step is a flex column filling the fixed-height panel (see
   .mobile-sheet above); flex:1 + min-height:0 (not height:100%) is
   what lets it size correctly alongside the drag handle sibling on
   mobile, where the handle takes up real space — on desktop the
   handle is display:none so this still just fills the full box. */
.mobile-sheet__step{ display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* pins the primary action to the same vertical position on every
   step regardless of how many fields precede it — the actual
   mechanism behind "same height every step," not just matching a
   number. Shared across both breakpoints now (previously desktop-
   only), so the mobile bottom sheet gets the same guarantee. */
.mobile-sheet__step > .btn.btn-block,
.mobile-sheet__step > .mobile-sheet__actions{ margin-top: auto; }
/* the quiet "Powered by VenueFlow" line every state ends on — sits
   directly after whatever margin-top:auto/content already pinned
   itself to the bottom, so it reads as the panel's fixed closing
   element rather than something that could push the layout around.
   Deliberately not a link, not a logo lockup — a single quiet line,
   same weight roughly as the mobile-sheet__done-note it replaces. */
.mobile-sheet__footnote{
  flex: none;
  margin-top: 14px;
  text-align: center;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.01em;
  color: var(--vp-ink-soft);
  opacity: .8;
}

.mobile-sheet__done{
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-xl) 0 var(--space-md);
}
.mobile-sheet__done-close{ position: absolute; top: -4px; right: -4px; }
/* wraps the icon/heading/body/Close-button as one group. margin:auto 0
   gives it auto top+bottom margins, which — in a column flex
   container — absorb ALL the leftover vertical space and split it
   evenly, centring this group within whatever room is left ABOVE the
   footnote below (which has no auto margin of its own, so it simply
   sits at its natural size right after). Same fixed-height parity
   trick as the CTA-pinning above, just centring instead of bottom-
   pinning. */
.mobile-sheet__done-content{ margin: auto 0; }
.mobile-sheet__done-icon{
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 48px; height: 48px;
  margin: 0 auto var(--space-md);
  border-radius: 50%;
  background: rgba(140,125,51,0.12); /* gilt tint — success without a generic green */
  color: var(--vp-gilt);
  animation: vp-success-pop .5s var(--ease) both;
}
.mobile-sheet__done-icon svg{ width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* two small gold sparkle dots either side of the checkmark — twinkle
   twice on arrival then settle. A quiet, close-up detail on the icon
   itself; the bigger celebratory moment is the canvas confetti burst
   fired from script.js (see fireConfetti), which plays across the
   whole page, not just this icon. */
.mobile-sheet__done-icon::before,
.mobile-sheet__done-icon::after{
  content: "";
  position: absolute;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--vp-gold);
  opacity: 0;
  animation: vp-sparkle 1.3s ease-in-out .5s 2;
}
.mobile-sheet__done-icon::before{ top: -7px; left: -9px; }
.mobile-sheet__done-icon::after{ bottom: -5px; right: -10px; animation-delay: .75s; }
@keyframes vp-success-pop{
  0%{ transform: scale(0.55); opacity: 0; }
  60%{ transform: scale(1.08); opacity: 1; }
  100%{ transform: scale(1); opacity: 1; }
}
@keyframes vp-sparkle{
  0%, 100%{ opacity: 0; transform: scale(0.4); }
  50%{ opacity: 1; transform: scale(1); }
}
.mobile-sheet__done h3{ margin-bottom: 8px; }
.mobile-sheet__done p{ color: var(--vp-ink-soft); font-size: var(--text-small); margin-bottom: var(--space-lg); }
.mobile-sheet__done .btn{ min-width: 160px; }

/* icon-prefixed field — used inside the mobile sheet */
.field-icon-input{ position: relative; }
.field-icon-input__icon{
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; color: var(--vp-ink-soft); pointer-events: none;
}
.field-icon-input__icon svg{ width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
/* Specificity note: the base .field input[type="..."] rules (0,0,2,1) are
   MORE specific than a plain .field-icon-input input (0,0,1,1), so the
   base rule's padding-left:14px silently won regardless of source order —
   that's what caused icons to collide with placeholder text. Matching the
   attribute-selector specificity here (and covering select, which the
   base rule also styles) fixes it for good. */
.field .field-icon-input input[type="text"],
.field .field-icon-input input[type="email"],
.field .field-icon-input input[type="tel"],
.field .field-icon-input input[type="date"],
.field .field-icon-input input[type="number"],
.field .field-icon-input select{
  padding-left: 44px;
}

@media (max-width: 720px){
  .mobile-cta-bar{ display: flex; }
}

/* ---- desktop: the SAME dialog markup renders as a centred, boxed
   modal instead of a bottom sheet. It's still opened by the same
   data-open-sheet CTA and closed by the same data-close-sheet
   elements — only the positioning/shape changes at this breakpoint. ---- */
@media (min-width: 721px){
  .mobile-sheet-backdrop{ background: rgba(31,31,31,0.5); }

  .mobile-sheet{
    left: 50%;
    right: auto;
    bottom: auto;
    top: 50%;
    width: min(460px, calc(100vw - 48px));
    /* fixed box height — the reason step 1 -> step 2 -> done used to
       visibly resize the modal is that height was following content.
       Locking it here and letting content align within it (see
       .mobile-sheet__step above) keeps every state the same size. */
    height: 640px;
    max-height: 88vh;
    border-radius: 20px;
    padding: 36px 40px;
    display: flex;
    flex-direction: column;
    box-shadow: 0 30px 80px rgba(31,31,31,0.28);
    transform: translate(-50%, -46%) scale(0.96);
    opacity: 0;
    pointer-events: none;
    transition: transform .32s var(--ease), opacity .32s var(--ease);
  }
  .mobile-sheet.is-open{
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
    pointer-events: auto;
  }
  /* the drag handle is a mobile bottom-sheet affordance only — the
     desktop modal closes via the × button, backdrop click, or Escape */
  .mobile-sheet__handle{ display: none; }

  /* desktop-only addition on top of the shared .mobile-sheet__step
     rule above — clears space for the absolutely-positioned × button
     (below), which mobile doesn't need since its close button sits
     inline in each step's own header row instead. NOTE: margin-top:
     auto (shared rule) alone already claims all the leftover space in
     the column for the CTA, so this padding must stay off the
     button/actions row itself — putting it there instead previously
     inflated the button's own box by another 24px on top of the auto
     margin, which pushed Step 1's Continue button past the modal's
     bottom padding and made it look clipped. */
  .mobile-sheet__step{ padding-top: 24px; }

  /* × close button moves to the modal's top-left corner on desktop
     (mobile keeps it at top-right of the sheet's own header row) —
     absolutely positioned against .mobile-sheet itself so all three
     steps place it identically. .mobile-sheet__step's new padding-top
     (above) clears vertical space so step content never sits under it. */
  .mobile-sheet__close{
    position: absolute;
    top: 20px;
    left: 20px;
    right: auto;
  }
  /* position:static so its close button falls through to position
     against .mobile-sheet itself (see .mobile-sheet__close above)
     instead of this panel — keeps all three close buttons in the
     exact same corner regardless of which step is showing. Everything
     else (flex layout, centring) is already handled by the shared
     base rules for .mobile-sheet__done / .mobile-sheet__done-content. */
  .mobile-sheet__done{ position: static; }
}

/* ---------------------------------------------------------------------
   21. RESPONSIVE RULES
   --------------------------------------------------------------------- */
@media (max-width: 900px){
  .grid-4{ grid-template-columns: repeat(2, 1fr); }
  .grid-3{ grid-template-columns: repeat(2, 1fr); }
  .contact-grid{ grid-template-columns: 1fr; }
  .intro-frame .grid-2{ grid-template-columns: 1fr; }
}

@media (max-width: 720px){
  .primary-nav{ display: none; }
  .site-header .header-actions .btn-primary{ padding: 12px 18px; font-size: 14px; }

  /* hero's own mobile treatment (full-bleed overlay) lives in section 15 */

  .grid-2{ grid-template-columns: 1fr; }
  .grid-3{ grid-template-columns: 1fr; }
  .grid-4{ grid-template-columns: 1fr 1fr; }
  .form-row, .form-row--3{ grid-template-columns: 1fr; }

  .event-card{ flex-basis: 68vw; }

  .gallery-masonry{ columns: 1; }

  .footer-nav{ gap: 10px 18px; }
  .footer-legal{ flex-direction: column; align-items: center; text-align: center; }

  section.section, .section--major{ padding-top: var(--section-pad-y-tight); padding-bottom: var(--section-pad-y-tight); }
}

@media (max-width: 560px){
  .grid-4{ grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------
   22. DARK MODE
   Toggled via html[data-theme="dark"] (theme-toggle button in the
   header; applied pre-paint by the inline snippet in <head>, then
   persisted to localStorage by script.js).

   Approach: remap the handful of core tokens the whole system already
   builds on — --vp-white, --vp-ink, --vp-ink-soft, --vp-line,
   --vp-line-soft — onto the SAME charcoal/cream palette already used
   for this design's "bounded dark moments" (footer, heritage panel:
   --vp-dark-bg / --vp-dark-text / --vp-dark-text-soft). Because those
   tokens are used consistently in pairs throughout the stylesheet
   (e.g. .btn-primary is background:ink + color:white), remapping both
   ink and white together flips every existing light-surface/dark-text
   pairing into a dark-surface/light-text one automatically, including
   ones that were already inverted on purpose (the mobile CTA pill is
   background:ink + text:white, i.e. already "dark surface" in light
   mode — the remap correctly turns it into a light pill with dark
   text once the page itself goes dark). Photography, gold accents and
   the Google/Facebook brand marks are intentionally left untouched.

   What DOESN'T flip automatically: a few places use literal rgba(black)
   or rgba(white) values rather than the tokens above (form borders,
   placeholder text, the mobile pill's chevron, the select arrow's
   baked-in SVG colour). Those get explicit overrides below. */
html[data-theme="dark"]{
  --vp-white: var(--vp-dark-bg);
  --vp-ink: var(--vp-dark-text);
  --vp-ink-soft: var(--vp-dark-text-soft);
  --vp-paper: #242320;
  --vp-line: rgba(245,241,232,0.16);
  --vp-line-soft: rgba(245,241,232,0.09);
}

/* form fields: border + placeholder are literal rgba(black), so the
   base rules don't inherit the token remap above — set explicitly */
html[data-theme="dark"] .field input[type="text"],
html[data-theme="dark"] .field input[type="email"],
html[data-theme="dark"] .field input[type="tel"],
html[data-theme="dark"] .field input[type="date"],
html[data-theme="dark"] .field input[type="number"],
html[data-theme="dark"] .field select{
  border-color: rgba(245,241,232,0.2);
}
html[data-theme="dark"] .field input::placeholder{ color: rgba(245,241,232,0.38); }
html[data-theme="dark"] .field select:invalid{ color: rgba(245,241,232,0.45); }
/* select's dropdown chevron is a baked-in SVG stroke colour, not a
   token — swap for a light-on-dark version so it stays visible */
html[data-theme="dark"] .field select{
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23C9C2B4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

/* mobile CTA pill: chevron colour is a literal rgba(white) tuned for
   the *light-mode* pill (which is black) — once the pill flips to a
   light/cream surface in dark mode via the token remap, that literal
   white chevron would vanish, so it needs its own dark-mode value */
html[data-theme="dark"] .mobile-cta-bar__chevron{ color: rgba(31,31,31,0.55); }

/* dark-mode variant of the hero's paper-grain texture — same organic
   CSS/SVG turbulence technique, re-tuned to warm cream flecks at a
   similarly low alpha so it stays atmosphere, not decoration, against
   the charcoal background */
@media (min-width: 721px){
  html[data-theme="dark"] .hero::before{
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320'%3E%3Cfilter id='vpgraindark'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.012' numOctaves='4' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.96  0 0 0 0 0.94  0 0 0 0 0.87  0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='320' height='320' filter='url(%23vpgraindark)'/%3E%3C/svg%3E");
  }
}

/* mobile hero's legibility scrim — this was the actual bug: the
   gradient is baked as literal rgba(255,255,255,x)/#FFFFFF stops, not
   the --vp-white token, so it silently stayed a WHITE scrim under
   text that had already flipped to light/cream. Cream text on a white
   scrim is how the headline went nearly invisible. Same gradient
   shape, dark stops instead. */
@media (max-width: 720px){
  html[data-theme="dark"] .hero-copy{
    background: linear-gradient(to bottom,
      rgba(31,31,31,0) 0%,
      rgba(31,31,31,0.86) 22%,
      var(--vp-dark-bg) 42%,
      var(--vp-dark-bg) 100%);
  }
}

/* hero CTA sheen sweep — tuned as a WHITE highlight for the button's
   light-mode black surface. In dark mode that same button flips to a
   light/cream surface (via the --vp-ink/--vp-white remap), so a white
   sweep over a near-white button would be invisible. Give it a dark
   sweep instead so the shimmer still reads. */
html[data-theme="dark"] .btn-sheen::after{
  background: linear-gradient(75deg,
    transparent 0%,
    rgba(31,31,31,0.06) 45%,
    rgba(31,31,31,0.12) 50%,
    rgba(31,31,31,0.06) 55%,
    transparent 100%);
}

/* ---------------------------------------------------------------------
   23. PRESS LOGOS (weddings-landing.html)
   Shared by the hero's integrated trust strip (.hero-trust). Was
   previously used inside a standalone .proof-transition section
   between the hero and Wedding Spaces; that section has been removed
   now that its content lives in the hero instead (see .hero-trust in
   "15. HERO" above) — .press-logos itself is unchanged and still does
   the same job, just from a new home.
   --------------------------------------------------------------------- */
/* three press logos — each sized individually rather than sharing one
   equal-size slot. A shared box + object-fit:contain was mathematically
   even but optically uneven: ITV's bold rounded wordmark fills its own
   bounding box far more densely than the others, so at an identical
   box height it read as the dominant mark, while Manchester Evening
   News' thin serif masthead read as the faintest even at the same
   size. Tuned by eye instead — ITV brought down, Manchester Evening
   News brought up, BBC nudged slightly — so the three read as equally
   important proof marks despite very different native shapes and
   weights. Muted (grayscale + reduced opacity) so the strip stays
   quiet rather than turning into a logo wall; Manchester Evening News
   keeps a touch more opacity since its thinner strokes need it just
   to hold the same visual weight as the other two. */
.press-logos{
  display: flex;
  align-items: center;
  justify-content: center;
  /* fluid gap — stays roomy on wide desktop but tightens smoothly as
     the viewport narrows, so all three logos keep to one line right
     up to the 720px mobile handoff instead of Manchester Evening News
     wrapping to a second line. */
  gap: clamp(var(--space-sm), 4vw, var(--space-3xl));
  flex-wrap: nowrap;
}
.press-logos__item{ display: flex; align-items: center; justify-content: center; }
/* visual order BBC / Manchester Evening News / ITV, matching the
   mobile sequence below — DOM order stays BBC, ITV, MEN so the source
   order is unaffected, only the on-screen order is reflected via
   `order`. Applied unconditionally so desktop and mobile always match. */
.press-logos__item:nth-child(1){ order: 1; } /* BBC — left */
.press-logos__item:nth-child(3){ order: 2; } /* Manchester Evening News — centre */
.press-logos__item:nth-child(2){ order: 3; } /* ITV — right */
.press-logos__img{
  display: block;
  width: auto;
  filter: grayscale(1);
  opacity: 0.6;
  transition: opacity var(--dur) var(--ease);
}
.press-logos__item:hover .press-logos__img{ opacity: 0.85; }
.press-logos__img--bbc{ height: 28px; }
.press-logos__img--itv{ height: 31px; } /* nudged up again from 27px — still reading slightly small next to BBC/MEN */
.press-logos__img--men{ height: 40px; }
.press-logos__item img.press-logos__img--men{ opacity: 0.72; }

@media (max-width: 720px){
  /* single clean row, not a 2-up grid with Manchester Evening News
     dropped to its own line underneath (an earlier pass) — the
     mockup calls for BBC / MEN / ITV side by side in one line, with
     Manchester Evening News reading as the visual centrepiece. Visual
     order (BBC / MEN / ITV) is now set once, unconditionally, on the
     base .press-logos__item rule above — DOM order stays BBC, ITV,
     MEN, so nothing needs re-sequencing here at this breakpoint. */
  .press-logos{
    display: flex;
    flex-wrap: nowrap;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-md);
  }
  .press-logos__img--bbc{ height: 22px; }
  .press-logos__img--itv{ height: 24px; }
  /* the visual centrepiece of the row — sized up so it reads as the
     dominant mark, per the mockup, while BBC/ITV flank it at a
     quieter size */
  .press-logos__img--men{ height: 32px; }
}

/* ---------------------------------------------------------------------
   24. WEDDING SPACES (weddings-landing.html)
   Editorial image/copy pairing, not a bordered card grid — the renders
   carry the emotional weight, copy stays minimal. Rows alternate
   image-left/image-right on desktop so the two rooms read as distinct
   without needing different chrome; both collapse to image-first
   stacks on mobile regardless of which side they started on.
   --------------------------------------------------------------------- */
/* .spaces normally inherits .section--major's top padding, but that's
   sized for a section that opens the page rhythm on its own — here it
   follows the slim proof line, which already provides a break. Full
   major-scale padding on top of that was what read as "too far from
   the hero" in review. Bottom padding stays at the major scale since
   Wedding Spaces is still a substantial section flowing into what's
   next. */
.spaces{ padding-top: var(--section-pad-y); }
/* tightened from space-4xl (96px) — three stacked gaps at that scale
   (head→first room, room→room, CTA) made Wedding Spaces read as
   several loosely-related blocks rather than one cohesive section;
   see the refinement-pass note further down for the matching
   section-to-section boundary reduction. */
.spaces .section-head{ margin-bottom: var(--space-3xl); }

.space-feature{
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  align-items: center;
  gap: var(--space-3xl);
}
.space-feature + .space-feature{ margin-top: var(--space-3xl); }
.space-feature--reverse{ grid-template-columns: 1fr 1.15fr; }
.space-feature--reverse .space-feature__media{ order: 2; }
.space-feature--reverse .space-feature__copy{ order: 1; }

/* the renders should sit directly on the white page, not inside the
   shared .asset-well's paper-card treatment (paper background, gilt
   border, gold corner tick) — that framing device is right for photos
   elsewhere in the system, but here it competed with the renders
   themselves. No border/shadow added back in its place, per brief.
   aspect-ratio is also freed from .ratio-landscape's fixed 4/3 box —
   that, combined with object-fit:cover, was quietly cropping the
   renders' outer edges (most visible on the Lodge render). Height is
   now natural/auto, so the full render always shows uncropped at its
   own proportions. No hover zoom for now — static and calm, motion
   to be reconsidered later. */
.space-feature__media{
  overflow: hidden;
  background: none;
  border: none;
  aspect-ratio: auto;
}
.space-feature__media::before{ display: none; }
.space-feature__media img{
  width: 100%;
  height: auto;
  object-fit: contain;
  display: block;
}

.space-feature__copy{ max-width: 38ch; }
.space-feature--reverse .space-feature__copy{ margin-left: auto; }
.space-feature__copy .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.space-feature__copy h3{ margin-bottom: var(--space-sm); }
/* card-tier body copy (pairs with the card's own heading-s title) —
   explicit now rather than silently inheriting the page's default
   body size, so this stays a deliberate rule instead of a fallback
   that could drift if the base body size ever changes elsewhere */
.space-feature__copy p{ color: var(--vp-ink-soft); font-size: var(--text-body); line-height: 1.7; }

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

@media (max-width: 900px){
  .space-feature,
  .space-feature--reverse{ grid-template-columns: 1fr; gap: var(--space-lg); }
  .space-feature--reverse .space-feature__media{ order: 0; } /* image always leads on mobile, regardless of desktop alternation */
  .space-feature--reverse .space-feature__copy{ order: 1; margin-left: 0; }
  .space-feature__copy{ max-width: none; }
  .space-feature + .space-feature{ margin-top: var(--space-xl); }
  .spaces .section-head{ margin-bottom: var(--space-xl); }
  /* the unconditional .spaces{padding-top} above is sized for desktop
     rhythm — restore the tight mobile scale here so it doesn't
     override the site-wide mobile section tightening */
  .spaces{ padding-top: var(--section-pad-y-tight); }
}

/* True mobile only (720px, matching where .mobile-cta-bar takes over
   as the single conversion action) — NOT the 900px tablet breakpoint
   above, since the floating CTA bar isn't shown yet in the 720-900px
   range and hiding this without it would leave no CTA visible there
   at all. Same reasoning as the hero's in-copy CTA (see "15. HERO"):
   an inline black button stacked in the body would just duplicate
   the floating bar's job. */
@media (max-width: 720px){
  .spaces__cta{ display: none; }
}

/* ---------------------------------------------------------------------
   26b. REVIEWS (weddings-landing.html)
   Three slow, alternating-direction marquee ribbons of review cards
   (Google + Facebook, mixed), softly faded at both edges. Pure CSS:
   each row's track holds the same card set twice back-to-back and
   animates transform:translateX from 0 to -50% (or the reverse) —
   since the second half is identical to the first, the loop point is
   invisible. The duplicate set is aria-hidden; the first set carries
   role="list"/"listitem" so a screen reader hears the real content
   once, not twice.
   --------------------------------------------------------------------- */
/* was var(--vp-paper) (#F7F4EC) — read as too heavy/beige at full-
   section scale. This is a much lighter custom blend, mostly white
   with just a whisper of the paper tone, so the section still reads
   as subtly separated from the pure-white page above/below it
   without tipping into cream. The edge-fade gradients (below) are
   matched to this exact colour so they blend seamlessly. */
.reviews{ background: #FBFAF6; }
.reviews .section-head{ margin-bottom: var(--space-3xl); }

.review-marquee{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding: var(--space-sm) 0;
  overflow: hidden;
}
.review-marquee__row{ overflow: hidden; width: 100%; }
.review-marquee__track{
  display: flex;
  align-items: stretch;
  gap: var(--space-lg);
  width: max-content;
  will-change: transform;
}
.review-marquee__set{
  display: flex;
  align-items: stretch;
  gap: var(--space-lg);
}
.review-marquee__track--left{ animation-name: review-marquee-left; animation-timing-function: linear; animation-iteration-count: infinite; }
.review-marquee__track--right{ animation-name: review-marquee-right; animation-timing-function: linear; animation-iteration-count: infinite; }
@keyframes review-marquee-left{
  from{ transform: translateX(0); }
  to{ transform: translateX(-50%); }
}
@keyframes review-marquee-right{
  from{ transform: translateX(-50%); }
  to{ transform: translateX(0); }
}
/* Deliberately no hover-pause — rows keep drifting continuously
   regardless of mouse position or touch, per brief: nothing should
   stop/start based on interaction, mobile included (touch was never
   wired to pause anyway, but this makes the "always moving" intent
   explicit for anyone editing this later). */

/* soft edge fades — sit above the tracks, span the full height of
   the marquee, and match the section's own (lightened) background
   colour so cards read as gently dissolving rather than being cut
   off */
.review-marquee__edge{
  position: absolute;
  top: 0;
  bottom: 0;
  width: 96px;
  z-index: 2;
  pointer-events: none;
}
.review-marquee__edge--left{ left: 0; background: linear-gradient(to right, #FBFAF6 0%, rgba(251,250,246,0) 100%); }
.review-marquee__edge--right{ right: 0; background: linear-gradient(to left, #FBFAF6 0%, rgba(251,250,246,0) 100%); }

/* ---- review card — a refined quote tile, not a UI-component-y box:
   thin hairline border (no heavy shadow by default), generous
   padding, fixed width + min-height so all cards in a row read as
   the same size regardless of exact quote length. ---- */
.review-card{
  flex: 0 0 340px;
  width: 340px;
  min-height: 184px;
  display: flex;
  flex-direction: column;
  background: var(--vp-white);
  border: 1px solid var(--vp-line-soft);
  border-radius: 12px;
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  box-shadow: 0 1px 2px rgba(31,27,20,0.03), 0 8px 20px rgba(31,27,20,0.03);
}
.review-card__stars{
  color: var(--vp-gold-deep);
  font-size: 13px;
  letter-spacing: 2px;
  margin-bottom: var(--space-sm);
}
.review-card__quote{
  flex: 1;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: 1.55;
  color: var(--vp-ink);
}
.review-card__foot{
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--vp-line-soft);
}
.review-card__source-icon{ flex: none; width: 18px; height: 18px; }
.review-card__source-icon svg{ display: block; width: 100%; height: 100%; }
.review-card__author{
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 600;
  color: var(--vp-ink);
}
.review-card__source-label{
  font-weight: 400;
  color: var(--vp-ink-soft);
  opacity: 0.8;
}

@media (prefers-reduced-motion: reduce){
  /* animation:none rather than just pausing — the duplicate set stays
     put off to the right, clipped by .review-marquee__row's own
     overflow:hidden, so this reads as a normal static row, not a
     frozen mid-scroll frame */
  .review-marquee__track{ animation: none; }
}

@media (max-width: 720px){
  .reviews{ padding-top: var(--section-pad-y-tight); padding-bottom: var(--section-pad-y-tight); }
  .reviews .section-head{ margin-bottom: var(--space-2xl); }
  .review-marquee{ gap: var(--space-md); }
  .review-marquee__track{ gap: var(--space-md); }
  .review-marquee__set{ gap: var(--space-md); }
  /* smaller, lighter cards and a tighter loop distance so the motion
     still feels smooth rather than slow-to-the-point-of-static on a
     narrow viewport */
  .review-card{
    flex-basis: 240px;
    width: 240px;
    min-height: 168px;
    padding: var(--space-md) var(--space-md) var(--space-sm);
  }
  .review-card__quote{ font-size: var(--text-small); }
  .review-marquee__edge{ width: 40px; }
}

/* ---------------------------------------------------------------------
   27. BALLROOM PLATE (weddings-landing.html)
   One large contained image (full content width, never viewport-edge)
   with the eyebrow/headline/body overlaid — genuinely on top of the
   image, not stacked above it in normal flow — on a SHORT, subtle
   white fade confined to the top ~35-40% of the photo. By the lower
   half there's no white overlay at all: tables, chairs and floor
   stay fully visible, contrasty and warm. No card, no border, no
   second image.
   --------------------------------------------------------------------- */
.ballroom-plate__figure{ margin: 0; }
.ballroom-plate__media{ position: relative; }
.ballroom-plate__img{
  display: block;
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  /* keeps the chandeliers and window tops in the upper portion, where
     the fade sits, with tables filling the fully-clear lower half */
  object-position: center 22%;
  /* light touch only — softened contrast, slightly reduced
     saturation, clean whites, no filter heavy enough to read as
     "edited" */
  filter: saturate(0.94) contrast(0.97) brightness(1.01);
}
/* top-only fade — strongest right behind the eyebrow/headline, mostly
   gone by a third of the way down, fully gone well before halfway.
   This sits BEHIND .ballroom-plate__copy (which is the thing actually
   positioned over the image); the fade on its own is just a soft
   backdrop for the text, not a treatment applied to the whole photo. */
.ballroom-plate__fade{
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg,
    rgba(255,255,255,0.88) 0%,
    rgba(255,255,255,0.72) 16%,
    rgba(255,255,255,0.35) 32%,
    rgba(255,255,255,0.08) 48%,
    rgba(255,255,255,0) 62%);
  pointer-events: none;
}
/* the actual overlay — was previously position:relative, which let it
   fall into normal flow BELOW the image instead of over it. That was
   the bug: text reading as its own block underneath the photo rather
   than sitting on top of it. */
.ballroom-plate__copy{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  max-width: 720px;
  margin: 0 auto;
  padding: var(--space-2xl) var(--container-pad) 0;
  text-align: center;
}
.ballroom-plate__copy .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.ballroom-plate__copy h2{ margin-bottom: var(--space-md); }
/* flagship-tier body copy — this section pairs a large display-l
   heading (32-56px) with its supporting line, so the body needs to be
   body-l (18px) rather than the page's plain 16px default, or the
   text reads as undersized next to a headline this large. Same rule
   now applied to Real Weddings below, since it's the same "large
   photo + overlaid headline" pattern. */
.ballroom-plate__copy p{ color: var(--vp-ink-soft); font-size: var(--text-body-l); line-height: 1.6; }

@media (max-width: 900px){
  .ballroom-plate__img{ aspect-ratio: 4 / 5; object-position: center 18%; }
  .ballroom-plate__copy{ padding-top: var(--space-xl); max-width: 88%; }
  /* mobile text wraps to more lines, so the fade needs a little more
     coverage to stay behind all of it — still a light touch, not a
     wash across the photo, and still fully clear by halfway */
  .ballroom-plate__fade{
    background: linear-gradient(180deg,
      rgba(255,255,255,0.90) 0%,
      rgba(255,255,255,0.76) 22%,
      rgba(255,255,255,0.40) 40%,
      rgba(255,255,255,0.10) 54%,
      rgba(255,255,255,0) 68%);
  }
}

/* ---------------------------------------------------------------------
   28. REAL WEDDINGS (weddings-landing.html)
   Same family as "27. BALLROOM PLATE" for the main plate — a large
   contained editorial photo (the wedding-party group shot) with
   eyebrow/headline overlaid (no body copy here — the section leads
   with the photo, not a paragraph). On expand, the overlay fade AND
   the eyebrow/headline fade OUT (see .is-gallery-open below, toggled
   on the section by script.js), so the collapsed state reads as an
   editorial intro and the expanded state reads as a clean,
   unobstructed photo with a gallery beneath it.

   The toggle button is ALWAYS an absolutely positioned overlay, never
   in normal document flow — collapsed, it sits inside .real-weddings__media
   (bottom-centre of the main plate, on its own subtle gradient);
   expanded, script.js physically moves the same node inside
   .real-weddings__secondary-wrap (bottom-centre of the revealed
   gallery, on .real-weddings__gallery-fade instead). Both parents are
   position:relative, so the button's own rule works unchanged in
   either spot — no separate "is-overlaid" positioning variant needed,
   script.js still toggles that class but it's cosmetic now, not
   load-bearing.
   --------------------------------------------------------------------- */
.real-weddings__media{ position: relative; }
.real-weddings__img{
  display: block;
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  object-position: center 30%;
  /* same light touch as the Ballroom plate — softened contrast,
     slightly reduced saturation, no filter heavy enough to read as
     "edited" */
  filter: saturate(0.94) contrast(0.97) brightness(1.01);
}
/* Lighter and shorter than an earlier pass — that version protected
   the heading well but veiled the wedding party's faces too much.
   Now: clears by ~56% of the image height (was ~70%) and peaks lower
   (0.86 vs 0.94), so the fade still fully covers the eyebrow/headline
   zone at the top but the people underneath read clearly well before
   the photo's midpoint. Warm ivory (matches --vp-paper), not stark
   white. Fades OUT entirely once the gallery opens (see
   .is-gallery-open below) so the expanded state shows the photo clean
   and unobstructed. */
.real-weddings__fade{
  position: absolute;
  inset: 0;
  z-index: 1;
  /* strengthened from a 0.86 peak — the eyebrow/heading wasn't
     popping enough against the photo. Boosted through the top ~42%
     (where the copy actually sits) while leaving the clear point at
     56% unchanged, so the wedding party underneath still reads fully
     clearly below that line — same reasoning as before, just more
     contrast right behind the text. */
  background: linear-gradient(180deg,
    rgba(247,244,236,0.94) 0%,
    rgba(247,244,236,0.82) 18%,
    rgba(247,244,236,0.46) 30%,
    rgba(247,244,236,0.16) 42%,
    rgba(247,244,236,0) 56%);
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.35s var(--ease);
}
.real-weddings__copy{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  /* narrower than the Ballroom copy block — shorter lines sit more
     centrally in the veil's strongest zone, away from the fade-out
     edge and away from where people's faces start */
  max-width: 600px;
  margin: 0 auto;
  padding: var(--space-2xl) var(--container-pad) 0;
  text-align: center;
  opacity: 1;
  transition: opacity 0.35s var(--ease);
}
.real-weddings__copy .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.real-weddings__copy h2{ margin: 0; }

/* expanded state — toggled on the section root by script.js in the
   same click handler that reveals the gallery. Fully transparent AND
   pointer-events:none so the clean photo underneath is genuinely
   unobstructed, not just visually faded while still intercepting
   clicks/hover. */
.real-weddings.is-gallery-open .real-weddings__fade,
.real-weddings.is-gallery-open .real-weddings__copy{
  opacity: 0;
  pointer-events: none;
}

/* the button itself — always an absolute overlay, centred on
   whichever position:relative parent it currently lives in (the main
   plate when collapsed, the gallery wrap when expanded). */
.real-weddings__more-btn{
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  width: fit-content;
  margin: 0;
  background: var(--vp-white);
}
/* the shared global .btn:hover rule (see "9. BUTTONS") sets its own
   transform: translateY(-1px) on hover — since a single transform
   declaration replaces rather than combines with another rule's
   transform, that wiped out this button's translateX(-50%) centring
   on hover, so the button visibly jumped to the right by half its own
   width. Re-declaring both transforms together here (with matching
   specificity/source order to win) keeps it centred AND lifted on
   hover. Covers both the "Show more photos" and "Show fewer photos"
   states since it's the same button/class in both. */
.real-weddings__more-btn:hover{ transform: translateX(-50%) translateY(-1px); }

.real-weddings__secondary-wrap{
  position: relative;
  /* equal to the gallery's own internal gap (see .real-weddings__grid--secondary
     below) so the whole expanded section reads as one coherent
     spacing system, not a bigger gap above than within */
  margin-top: 18px;
}
/* claims its layout space instantly (see script.js — target.hidden
   flips with no transition on that), so nothing visibly grows or
   pushes the page around; only the photos' opacity is animated —
   reads as "already there, just becoming visible," not a box
   expanding underneath them */
.real-weddings__secondary-wrap > .real-weddings__grid--secondary{
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}
.real-weddings__secondary-wrap.is-visible > .real-weddings__grid--secondary{
  opacity: 1;
}
/* soft ivory gradient the overlaid button sits on top of, so it reads
   clearly against whatever photo happens to be behind it at the
   bottom of the gallery. Shorter and softer than an earlier pass —
   that version read as a cloudy block; this one stays low and only
   builds to real opacity in its last third, just enough to carry the
   button without washing out the tiles above it. Fades in with the
   gallery itself. */
.real-weddings__gallery-fade{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 84px;
  background: linear-gradient(180deg, rgba(247,244,236,0) 0%, rgba(247,244,236,0.5) 55%, rgba(247,244,236,0.86) 100%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}
.real-weddings__secondary-wrap.is-visible .real-weddings__gallery-fade{ opacity: 1; }

/* editorial mosaic — five supporting photos across two columns, not a
   plain equal-size row. Left column stays narrow and holds the three
   smaller/quieter moments (ring detail, ring exchange, the bouquet
   portrait), each sized at aspect-ratio 3/2 — the photos' own native
   ratio, so object-fit:cover has nothing to crop. The right column is
   wider and carries the two larger moments: friends laughing spans
   rows 1–2 (its height is derived from the two stacked narrow tiles,
   not a fixed pixel value, so it's sized by the grid rather than
   force-cropped into an arbitrary box), and the cake-cutting moment
   sits in row 3, also at its native 3/2 ratio. Row heights are auto
   (derived from each tile's own aspect-ratio) rather than fixed
   pixels — that fixed-height approach was squeezing every tile into a
   box much shorter than its native photo, cropping faces out of the
   top/bottom of nearly every image. */
.real-weddings__grid--secondary{
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  grid-auto-rows: auto;
  gap: 18px;
}
.rw-ring-box{ grid-column: 1; grid-row: 1; aspect-ratio: 3 / 2; }
.rw-ring-exchange{ grid-column: 1; grid-row: 2; aspect-ratio: 3 / 2; }
.rw-bouquet{ grid-column: 1; grid-row: 3; aspect-ratio: 3 / 2; }
.rw-friends{ grid-column: 2; grid-row: 1 / 3; } /* height set by the grid, not its own aspect-ratio */
.rw-cake{ grid-column: 2; grid-row: 3; aspect-ratio: 3 / 2; }
.rw-friends img{ object-position: center 32%; }
.rw-cake img{ object-position: center 42%; }
.rw-bouquet img{ object-position: center 35%; }

.real-weddings__tile{
  position: relative;
  overflow: hidden;
  background: var(--vp-paper);
  /* the actual root cause of the "empty space beneath the image"
     bug: several tiles (e.g. .rw-bouquet) carry their own explicit
     aspect-ratio:3/2 for auto-row-sizing purposes, but that ratio is
     computed against THEIR OWN column's width — column 1 is narrower
     than column 2, so a 3/2 tile in column 1 wants a shorter height
     than a 3/2 tile in column 2 gets. Because both width and height
     were otherwise definite (grid column width + this aspect-ratio),
     the browser let aspect-ratio win over the grid's own stretch
     alignment, so the tile's box stopped short of the row's actual
     (taller, column-2-driven) track height — exposing this
     background colour in the gap below the photo instead of the grid
     item stretching to fill its cell like every other grid item does
     by default. Setting an explicit height here removes the
     ambiguity (both dimensions are now unavoidably definite via the
     grid area itself), so every tile always fills its full cell;
     aspect-ratio still does its job of influencing how tall each
     auto row becomes in the first place, it just no longer overrides
     the final rendered box size once that's decided. */
  height: 100%;
}
/* each tile's img sits inside a <picture> wrapper for the webp
   source. <picture> is inline by default, so the img's own
   width/height:100% (below) was resolving against an undefined
   auto-height container rather than the tile's actual box — combined
   with the fix above, this guarantees object-fit:cover on the img
   always has a full, correctly-sized box to cover, however tall the
   grid ultimately makes any given row. */
.real-weddings__tile picture{ display: block; width: 100%; height: 100%; }
.real-weddings__tile img{
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* same shared colour-harmonisation pass as the plate image above —
     these five come from a different wedding/photographer, this
     keeps the whole section reading as one consistent edit */
  filter: saturate(0.92) contrast(0.96) brightness(1.02);
}
.real-weddings__tile::after{
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(247,244,236,0.10);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

@media (max-width: 900px){
  .real-weddings__img{ aspect-ratio: 4 / 5; object-position: center 24%; }
  .real-weddings__copy{ padding-top: var(--space-xl); max-width: 88%; }
  .real-weddings__fade{
    background: linear-gradient(180deg,
      rgba(247,244,236,0.96) 0%,
      rgba(247,244,236,0.86) 20%,
      rgba(247,244,236,0.48) 34%,
      rgba(247,244,236,0.18) 46%,
      rgba(247,244,236,0) 60%);
  }
  .real-weddings__more-btn{ bottom: 16px; }
  .real-weddings__secondary-wrap{
    /* mobile gallery gap is 16px (see below), so the gap above it
       matches that instead of the desktop 18px */
    margin-top: 16px;
  }
  .real-weddings__gallery-fade{ height: 68px; }
  /* clean vertical stack — no forced crop, every tile at its photos'
     native 3/2 ratio so faces and key moments always show fully.
     Order follows the DOM: ring detail, ring exchange, bouquet
     portrait, friends laughing, cake cutting. */
  .real-weddings__grid--secondary{
    grid-template-columns: 1fr;
    grid-auto-rows: auto;
    gap: 16px;
  }
  .rw-ring-box, .rw-ring-exchange, .rw-bouquet, .rw-friends, .rw-cake{
    grid-column: 1;
    grid-row: auto;
    aspect-ratio: 3 / 2;
  }
  /* NOTE: this grid intentionally has NO padding-bottom. An earlier
     pass added 56px here to "give the overlaid button room," but
     .real-weddings__secondary-wrap sizes itself to this grid's full
     height (including any padding) and the button is positioned
     bottom:16px WITHIN that wrapper — so that padding just pushed the
     last tile up and left the button sitting in blank space below it
     instead of over the photo. The button is meant to overlay the
     final tile directly (on .real-weddings__gallery-fade, which sits
     flush at the wrap's true bottom edge), so the grid must end
     exactly where the last tile ends, with no extra space beneath it. */
}

/* respects the OS-level reduced-motion preference — swap the eased
   fades for an instant state change rather than removing the
   functionality itself */
@media (prefers-reduced-motion: reduce){
  .real-weddings__fade,
  .real-weddings__copy,
  .real-weddings__secondary-wrap > .real-weddings__grid--secondary,
  .real-weddings__gallery-fade{
    transition: none;
  }
}

/* ---------------------------------------------------------------------
   29. PAGE SKELETON PLACEHOLDERS (weddings-landing.html)
   One reusable, deliberately empty placeholder component used for
   every section in the revised conversion-led order that isn't built
   yet: a small muted section number, the section name, a short
   one-line purpose note, and a quiet "Placeholder" badge. No sample
   copy, no components, no imagery — structural skeleton pass only.
   Same box everywhere so the unfinished sections read as one calm,
   consistent system rather than a row of dev-only boxes.
   --------------------------------------------------------------------- */
.skeleton-section{ padding-top: var(--section-pad-y); padding-bottom: var(--section-pad-y); }
.skeleton-placeholder{
  max-width: var(--container-max);
  margin: 0 auto;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: var(--space-2xl) var(--container-pad);
  background: var(--vp-white);
  border: 1px solid var(--vp-line);
  border-radius: 14px;
}
.skeleton-placeholder__number{
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--vp-ink-soft);
}
.skeleton-placeholder__name{
  /* pulled from the shared heading-s token instead of a one-off rem
     value, so any placeholder that later becomes a real card-tier
     section already starts from the right size */
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-heading-s);
  line-height: 1.25;
  color: var(--vp-ink);
  margin: 0;
}
.skeleton-placeholder__note{
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--vp-ink-soft);
  max-width: 42ch;
  margin: 0;
}
.skeleton-placeholder__badge{
  display: inline-block;
  margin-top: 6px;
  padding: 4px 12px;
  border: 1px solid var(--vp-line);
  border-radius: 999px;
  background: var(--vp-paper);
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--vp-ink-soft);
}
@media (max-width: 720px){
  .skeleton-section{ padding-top: var(--section-pad-y-tight); padding-bottom: var(--section-pad-y-tight); }
  .skeleton-placeholder{ min-height: 160px; padding: var(--space-xl) var(--container-pad); border-radius: 12px; }
}

/* generic "temporarily off the live page" toggle — used right now to
   pull Section 04 (At-a-glance) out of render without deleting its
   markup. One class, reusable for any future section that needs the
   same "keep the code, hide the output" treatment. */
.is-hidden-temp{ display: none; }

/* ---------------------------------------------------------------------
   30. FINAL CTA (weddings-landing.html) — Section 12.
   Same contained-photo-panel family as "27. BALLROOM PLATE," but the
   copy sits centred over the photo (not top-anchored) and closes with
   the primary CTA. A shallow closing banner, not a hero — aspect-ratio
   16/5 on desktop, object-position pulled down to 62% so the crop
   favours the fairy-light curtain with only a sliver of table styling
   (wine, florals, chair backs) grazing the bottom edge, not the full
   tablescape. images/final-cta-venue.jpg/.webp is the client's own
   pre-cropped source file (1129×499 — already close to the desktop
   panel's own aspect ratio, so there's almost no horizontal crop room
   left at that box size; a small sliver of balloon-tassel ribbon at
   the very left edge is baked into their crop and can't be trimmed
   further without cutting into the curtain itself). Mobile uses
   object-position 68% 50% to shift its narrower crop window away from
   that same left-edge tassel, since mobile's aspect ratio does leave
   horizontal room to move. The wash is a radial ivory gradient —
   strongest directly behind the text, easing out toward the edges —
   so it still reads as a soft glow rather than a flat rectangle.
   Filter on the img is the same light-touch saturate/brightness/
   contrast recipe used on the Ballroom photo. Border matches the
   Heritage card's (1px var(--vp-line), same var(--radius-card)) so
   the two contained-panel sections read as visually related closing
   moments, without turning this into a heavy boxed card.
   --------------------------------------------------------------------- */
.final-cta__figure{
  position: relative;
  overflow: hidden;
  margin: 0;
  border: 1px solid var(--vp-line);
  border-radius: var(--radius-card);
}
.final-cta__img{
  display: block;
  width: 100%;
  aspect-ratio: 16 / 5;
  object-fit: cover;
  object-position: 50% 62%;
  filter: saturate(0.9) brightness(1.05) contrast(0.97);
}
.final-cta__wash{
  position: absolute;
  inset: 0;
  z-index: 1;
  background: radial-gradient(ellipse 55% 68% at 50% 44%,
    rgba(248,244,236,.92) 0%,
    rgba(248,244,236,.78) 30%,
    rgba(248,244,236,.40) 55%,
    rgba(248,244,236,.10) 76%,
    rgba(248,244,236,0) 100%);
  pointer-events: none;
}
.final-cta__copy{
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-xl) var(--container-pad);
}
.final-cta__copy .eyebrow{ display: block; margin-bottom: var(--space-sm); }
.final-cta__copy h2{ max-width: 18ch; margin-bottom: var(--space-xs); }
.final-cta__copy p{ color: var(--vp-ink-soft); font-size: var(--text-body-l); line-height: 1.6; max-width: 46ch; margin: 0 auto var(--space-lg); }

@media (max-width: 720px){
  .final-cta__img{
    aspect-ratio: 20 / 17;
    object-position: 68% 50%;
  }
  .final-cta__wash{
    background: radial-gradient(ellipse 62% 56% at 50% 44%,
      rgba(248,244,236,.95) 0%,
      rgba(248,244,236,.82) 28%,
      rgba(248,244,236,.48) 52%,
      rgba(248,244,236,.18) 72%,
      rgba(248,244,236,0) 90%);
  }
  .final-cta__copy{ padding: var(--space-lg); }
  .final-cta__copy h2{ max-width: none; }
  .final-cta__copy p{ max-width: none; }
  /* mobile's only conversion control is the floating CTA bar — same
     rule already applied to ".spaces__cta" above */
  .final-cta__action{ display: none; }
}

/* ---------------------------------------------------------------------
   31. SECTION-TO-SECTION SPACING REFINEMENT — Wedding Spaces / Ballroom
   Plate / Real Weddings only. These three sections previously carried
   full major-scale padding (clamp(72px,11vw,160px)) on both sides of
   each boundary, so the gap between the Wedding Spaces CTA and the
   Ballroom section (and between the Ballroom section and Real
   Weddings) could reach ~300px combined on wider viewports — enough
   to read as disconnected sections rather than a continuous flow.
   Pulled down to a fixed 48px (var(--space-2xl)) on each side of both
   boundaries. Deliberately NOT touching Real Weddings' own bottom
   padding (flows into Reviews, out of scope here) or Wedding Spaces'
   top padding (flows from the hero, already tuned separately above).
   Uses "section.ballroom-plate" / "section.spaces" / "section.real-
   weddings" (tag+class) rather than a plain class selector so this
   reliably beats the sitewide mobile rule ("section.section,
   .section--major") of equal specificity at the 720px breakpoint
   below — a plain class selector there would silently lose and do
   nothing.
   --------------------------------------------------------------------- */
.spaces{ padding-bottom: var(--space-2xl); }
section.ballroom-plate{ padding-top: var(--space-2xl); padding-bottom: var(--space-2xl); }
.real-weddings{ padding-top: var(--space-2xl); }

@media (max-width: 720px){
  section.spaces{ padding-bottom: var(--space-lg); }
  section.ballroom-plate{ padding-top: var(--space-lg); padding-bottom: var(--space-lg); }
  section.real-weddings{ padding-top: var(--space-lg); }
}

