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

  :root {
    --cream:   #F8F4EF;
    --warm:    #EDE5D8;
    --sand:    #D4C4AF;
    --muted:   #9A8C7E;
    --charcoal:#2C2825;
    --ink:     #1A1714;
    --rose:    #C4786A;
    --gold:    #B8965A;
    --dark-gray:#2A2622;
    --serif:   'Cormorant Garamond', Georgia, serif;
    --sans:    'Jost', system-ui, sans-serif;
    --nav-h:   72px;
    --max:     1560px;
    --ease:    cubic-bezier(0.22, 1, 0.36, 1);
  }

  html { scroll-behavior: smooth; }
  body { background: var(--cream); color: var(--charcoal); font-family: var(--sans); font-weight: 300; overflow-x: hidden; -webkit-font-smoothing: antialiased; }
  img { display: block; max-width: 100%; }

  /* ============ NAV ============ */
  nav {
    position: fixed; top: 0; left: 0; right: 0; z-index: 100;
    height: var(--nav-h);
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 48px;
    background: rgba(248,244,239,0.8);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid rgba(212,196,175,0.35);
    transition: background 0.35s var(--ease), border-color 0.35s var(--ease);
  }
  .nav-logo { position: relative; display: inline-block; height: 40px; line-height: 0; text-decoration: none; }
  .nav-logo img { height: 40px; width: auto; transition: opacity 0.35s var(--ease); }
  .nav-logo .logo-light { position: absolute; inset: 0; opacity: 0; }
  nav.hero-nav .nav-logo .logo-dark { opacity: 0; }
  nav.hero-nav .nav-logo .logo-light { opacity: 1; }

  .nav-links { display: flex; gap: 40px; list-style: none; }
  .nav-links a {
    position: relative;
    font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
    font-weight: 400; color: var(--muted); text-decoration: none;
    padding: 6px 0;
    transition: color 0.25s var(--ease);
  }
  .nav-links a::after {
    content: '';
    position: absolute; left: 0; bottom: 0;
    width: 100%; height: 1px;
    background: currentColor;
    transform: scaleX(0); transform-origin: right;
    transition: transform 0.45s var(--ease);
  }
  .nav-links a:hover { color: var(--ink); }
  .nav-links a:hover::after { transform: scaleX(1); transform-origin: left; }

  .nav-cta {
    position: relative; overflow: hidden;
    font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 400;
    padding: 12px 29px;
    background: #fff;
    border: 0;
    color: var(--ink);
    text-decoration: none;
    opacity: 0.72;
    transition: color 0.4s var(--ease), opacity 0.4s var(--ease);
    z-index: 0;
  }
  .nav-cta::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
    z-index: -1;
  }
  .nav-cta:hover { color: #fff; opacity: 1; }
  .nav-cta:hover::before { transform: translateY(0); }

  nav.hero-nav { background: transparent; border-bottom-color: transparent; }
  nav.hero-nav .nav-links a { color: rgba(255,255,255,0.8); }
  nav.hero-nav .nav-links a:hover { color: #fff; }

  /* hamburger */
  .hamburger {
    display: none; background: none; border: 0; padding: 8px;
    cursor: pointer; z-index: 301;
    position: relative;
  }
  .hamburger span {
    display: block; width: 26px; height: 1px;
    background: var(--ink); margin: 7px 0;
    transition: transform 0.4s var(--ease), opacity 0.3s, background 0.3s;
  }
  nav.hero-nav .hamburger span { background: #fff; }
  .hamburger.open span:nth-child(1) { transform: translateY(8px) rotate(45deg); background: var(--ink); }
  .hamburger.open span:nth-child(2) { opacity: 0; }
  .hamburger.open span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); background: var(--ink); }

  /* Drawer uses 100dvh so the bottom CTA stays inside the visible
     viewport on iOS Safari — 100vh measures the large viewport (URL
     bar hidden) and would push "Get in Touch" behind Safari's toolbar.
     Bottom padding also respects the safe-area inset for gesture bars. */
  .mobile-drawer {
    position: fixed; inset: 0;
    width: 100vw; height: 100dvh;
    background: var(--cream);
    padding: 100px 40px calc(32px + env(safe-area-inset-bottom, 0px));
    transform: translateX(100%);
    transition: transform 0.55s var(--ease);
    z-index: 300;
    display: flex; flex-direction: column; gap: 4px;
    visibility: hidden;
  }
  /* Ghost close button — pinned to the exact slot the hamburger
     occupies when the menu is shut, so clicking ≡ / X feels like a
     single toggle in place. The hamburger lives inside the nav at
     24px right (nav padding on mobile), vertically centered in the
     72px nav height; it's 42×48 (26px span + 8px padding all sides).
     We mirror those dimensions here and size the internal SVG to
     26px so the X glyph matches the hamburger span width exactly. */
  .drawer-close {
    position: absolute;
    top: calc((var(--nav-h) - 48px) / 2);
    right: 24px;
    width: 42px; height: 48px;
    padding: 8px;
    background: transparent; border: 0;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; color: var(--ink);
    transition: color 0.3s var(--ease), transform 0.3s var(--ease);
    z-index: 310;
  }
  .drawer-close svg { width: 26px; height: 26px; }
  .drawer-close:hover {
    background: transparent;
    color: var(--rose);
    transform: rotate(90deg);
  }
  .mobile-drawer.open { transform: translateX(0); visibility: visible; }
  .mobile-drawer a {
    display: block;
    font-family: var(--serif); font-size: 36px; font-weight: 300;
    color: var(--ink); text-decoration: none;
    padding: 22px 0;
    border-bottom: 1px solid var(--sand);
    opacity: 0;
    transform: translateY(20px);
    transition-property: opacity, transform, color, padding-left;
    transition-duration: 0.5s, 0.5s, 0.25s, 0.4s;
    transition-timing-function: var(--ease);
    transition-delay: 0s, 0s, 0s, 0s;
  }
  .mobile-drawer.open a {
    opacity: 1; transform: translateY(0);
  }
  .mobile-drawer.open a:nth-child(1) { transition-delay: 0.15s, 0.15s, 0s, 0s; }
  .mobile-drawer.open a:nth-child(2) { transition-delay: 0.22s, 0.22s, 0s, 0s; }
  .mobile-drawer.open a:nth-child(3) { transition-delay: 0.29s, 0.29s, 0s, 0s; }
  .mobile-drawer.open a:nth-child(4) { transition-delay: 0.36s, 0.36s, 0s, 0s; }
  .mobile-drawer a:hover { color: var(--rose); padding-left: 10px; }
  .mobile-drawer .drawer-cta {
    margin-top: auto;
    font-family: var(--sans); font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
    padding: 20px 28px; background: var(--ink); color: var(--cream);
    text-align: center; border: 0;
    font-weight: 400;
    transition-property: opacity, transform, background, color;
    transition-duration: 0.5s, 0.5s, 0.35s, 0.35s;
    transition-timing-function: var(--ease);
    transition-delay: 0s, 0s, 0s, 0s;
  }
  .mobile-drawer.open .drawer-cta { transition-delay: 0.36s, 0.36s, 0s, 0s; }
  .mobile-drawer .drawer-cta:hover { background: var(--rose); color: var(--cream); padding-left: 28px; }

  /* ============ HERO (unchanged) ============ */
  .hero { position: relative; width: 100%; height: 100vh; overflow: hidden; }
  .hero-slides { position: absolute; inset: 0; }
  .hero-slide { position: absolute; inset: 0; background-size: cover; background-position: var(--focal-x, 50%) var(--focal-y, 50%); opacity: 0; transform: scale(1.05); transition: opacity 1.4s ease, transform 6s ease;}
  .hero-slide.active { opacity: 1; transform: scale(1); }
  .hero-slide::after { content: ''; position: absolute; inset: 0; background: linear-gradient(rgba(20,16,12,0) 0%, rgba(20,16,12,0.2) 50%, rgba(20,16,12,0.4) 100%); }
  /* Soft atmospheric halo behind the hero copy — two wide, blurry stops
     that darken the photo immediately behind the letters without any
     visible hard edge. No tight blur; the effect reads as gentle
     ambient darkening rather than a drop-shadow. All hero text
     descendants inherit this via .hero-content. */
  .hero-content { position: absolute; inset: 0; z-index: 10; display: flex; flex-direction: column; justify-content: flex-end; padding: 0 80px 80px; text-shadow: 0 2px 24px rgba(0,0,0,0.42), 0 4px 56px rgba(0,0,0,0.32); }
  .hero-eyebrow { font-size: 13px; letter-spacing: 0.28em; text-transform: uppercase; color: #fff; margin-bottom: 48px; }
  /* Editorial two-line eyebrow layout:
       Line 1 — "San Francisco Wedding Photographer"   (white, full weight)
       — 28px hairline rule, left-aligned —
       Line 2 — "Est. 2009"                            (white, 0.72 opacity)
     The short hairline does all the hierarchy work — no color accent, no
     underline — so the two lines read as a quietly refined typographic
     group rather than a labeled badge. The rule is an ::after on
     .eyebrow-main (left-aligned, 1px tall, dimmed white) so it shares a
     baseline with the start of the text above it. Dim-white on Est. 2009
     keeps it subordinate to the main line without introducing weight or
     color contrast that would compete with the rule. */
  .hero-eyebrow .eyebrow-main {
    display: block;
    position: relative;
    padding-bottom: 14px;
    margin-bottom: 14px;
  }
  .hero-eyebrow .eyebrow-main::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 28px;
    height: 1px;
    /* Matches .hero-stats border-top so the two horizontal rules in the
       hero composition share one color/opacity — a single hairline
       treatment repeating at top and bottom of the copy group. */
    background: rgba(255, 255, 255, 0.28);
  }
  .hero-eyebrow .eyebrow-year {
    display: inline-block;
    color: #fff;
    font-weight: 500;
  }
  .hero-title { font-family: var(--serif); font-size: clamp(64px, 8vw, 120px); line-height: 0.95; font-weight: 300; color: #fff; margin-bottom: 36px; }
  .hero-title em { font-style: italic; color: var(--rose); }
  /* Subtitle color bumped from 0.7 → 0.92 for readability against the
     hero slideshow — the 0.7 was too washed out on both desktop and
     mobile, especially on lighter slides. Layered text-shadow inherited
     from .hero-content keeps contrast clean on bright frames. */
  .hero-subtitle { font-size: 16px; line-height: 1.8; color: rgba(255,255,255,0.92); max-width: 500px; margin-bottom: 48px; font-weight: 300; }
  .hero-actions { display: flex; align-items: center; gap: 32px; margin-bottom: 64px; }

  /* Hero primary — filled pill matching the site's primary CTA family
     (17/34 padding, 425 weight, 0.22em tracking). Palette stays cream
     fill / ink label since it sits on the ink hero. Hover uses the same
     left-to-right rose sweep as .btn-view-all for a unified gesture. */
  .btn-primary {
    position: relative; overflow: hidden;
    padding: 17px 34px; background: var(--cream); color: var(--ink);
    font-family: var(--sans); font-size: 12px; font-weight: 425;
    letter-spacing: 0.22em; text-transform: uppercase;
    text-decoration: none;
    display: inline-flex; align-items: center; gap: 14px;
    transition: color 0.4s var(--ease);
    border: 0; cursor: pointer;
  }
  .btn-primary::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
  }
  .btn-primary span, .btn-primary .arrow { position: relative; z-index: 1; }
  .btn-primary .arrow {
    display: inline-block;
    font-size: 12.5px; line-height: 1;
    transition: transform 0.4s var(--ease);
  }
  .btn-primary:hover { color: var(--cream); }
  .btn-primary:hover::before { transform: translateX(0); }
  .btn-primary:hover .arrow { transform: translateX(6px); }

  /* Hero ghost — no fill, no border, just label + arrow on the dark
     hero. Shares the family's 12px / 0.22em / 425 weight / 12.5px arrow
     metrics so the pair reads as tonal variants of one component. */
  .btn-ghost {
    position: relative;
    font-family: var(--sans); font-size: 12px; font-weight: 425;
    letter-spacing: 0.22em; text-transform: uppercase;
    color: rgba(255,255,255,0.7); text-decoration: none;
    display: inline-flex; align-items: center; gap: 14px;
    transition: color 0.25s var(--ease);
  }
  .btn-ghost .arrow {
    display: inline-block;
    font-size: 12.5px; line-height: 1;
    transition: transform 0.4s var(--ease);
  }
  .btn-ghost:hover { color: #fff; }
  .btn-ghost:hover .arrow { transform: translateX(6px); }

  .hero-stats { display: flex; gap: 48px; padding-top: 32px; border-top: 1px solid rgba(255,255,255,0.28); }
  /* No per-element text-shadow — inherits the layered shadow from
     .hero-content. Label opacity stays at 0.88 (bumped from 0.5) since
     that's a visibility fix, not a shadow one. */
  .stat-num { font-family: var(--serif); font-size: 38px; font-weight: 300; color: #fff; line-height: 1; }
  .stat-label { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255,255,255,0.88); margin-top: 8px; }
  .hero-dots { position: absolute; bottom: 32px; right: 80px; z-index: 20; display: flex; gap: 10px; align-items: center; }
  .hero-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.35); border: none; padding: 0; cursor: pointer; transition: background 0.3s, transform 0.3s; }
  .hero-dot.active { background: #fff; transform: scale(1.3); }

  /* ============ SECTIONS COMMON ============ */
  section { padding: 110px 48px; }
  .container { max-width: var(--max); margin: 0 auto; }
  .section-label {
    font-size: 12px; letter-spacing: 0.24em; text-transform: uppercase;
    color: var(--gold); margin-bottom: 22px;
    display: inline-flex; align-items: center; gap: 14px;
  }
  .section-label::before { content: ''; width: 28px; height: 1px; background: var(--gold); }
  .section-title {
    font-family: var(--serif); font-size: clamp(40px, 4.4vw, 64px);
    font-weight: 300; color: var(--ink); line-height: 1.06;
    letter-spacing: -0.008em;
  }
  .section-title em { font-style: italic; color: var(--rose); }

  /* ============ FILMSTRIP — cinematic-a style, deep warm charcoal ============ */
  .filmstrip {
    padding: 22px 0;
    overflow: hidden;
    /* Darker than --dark-gray (#2A2622) but not pure #000 — a deep
       warm charcoal that keeps the subtle red/amber hint of the site
       palette, so the filmstrip sits against the About section
       without the clinical chill of true black. */
    background: #15120F;
    border-top: 1px solid rgba(0,0,0,0.5);
    border-bottom: 1px solid rgba(0,0,0,0.5);
  }
  .fs-viewport {
    overflow: hidden;
    position: relative;
    /* Grab affordance — the filmstrip is draggable. */
    cursor: grab;
    /* Let the browser handle vertical page scroll, but route horizontal
       gestures to our pointer handlers so swipes can move the strip. */
    touch-action: pan-y;
  }
  .fs-viewport.is-dragging {
    cursor: grabbing;
    /* Keep text/images from selecting or ghost-dragging mid-swipe. */
    user-select: none;
    -webkit-user-select: none;
  }
  .fs-viewport.is-dragging .fs-inner {
    /* Disable the 0.8s zoom transition during drag so the track feels
       1:1 with the pointer instead of easing behind it. */
    transition: none;
  }
  .fs-viewport img,
  .fs-viewport .fs-inner {
    -webkit-user-drag: none;
    user-select: none;
  }
  .fs-track {
    display: flex;
    gap: 0;
    width: max-content;
    will-change: transform;
  }
  .fs-cell {
    position: relative;
    width: 460px; height: 315px;
    flex: 0 0 auto;
    /* Matches the section background so image gaps disappear cleanly
       against the deep-charcoal bed, keeping the filmstrip reading as
       a single ribbon of frames rather than discrete tiles. */
    background: #15120F;
    border-right: 1px solid rgba(0,0,0,0.55);
    overflow: hidden;
  }
  .fs-inner {
    position: absolute; inset: 0;
    background-size: cover;
    background-position: center;
    transition: transform 0.8s var(--ease), filter 0.6s var(--ease);
  }
  .fs-cell:hover .fs-inner {
    transform: scale(1.04);
  }

  /* ============ ABOUT ============ */
  .about { background: var(--warm); padding: 88px 88px; }
  .about-grid {
    max-width: var(--max); margin: 0 auto;
    display: grid;
    grid-template-columns: 460px 1fr;
    gap: 96px;
    align-items: start;
  }
  .about-img-wrap {
    position: relative;
    width: 100%;
    max-width: 460px;
  }
  /* Frame clips the photo so the hover scale happens inside a fixed box
     while the rose name tag — anchored to the wrap, not the frame — stays
     parked in place, letting the photo swell underneath it. */
  .about-img-frame {
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
  }
  .about-img {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
    transition: transform 1s var(--ease);
    will-change: transform;
  }
  .about-img-wrap:hover .about-img { transform: scale(1.05); }
  .about-tag {
    position: absolute; left: -20px; bottom: -20px;
    background: var(--rose);
    padding: 16px 22px;
    z-index: 2;
  }
  .about-tag-name {
    font-family: var(--serif); font-size: 20px;
    color: var(--cream); line-height: 1.15;
    font-weight: 400; letter-spacing: 0.005em;
  }
  .about-tag-role {
    font-family: var(--sans); font-size: 10px;
    letter-spacing: 0.24em; text-transform: uppercase;
    color: rgba(248,244,239,0.88); margin-top: 6px;
    font-weight: 400;
  }

  .about-body { padding-top: 8px; }
  .about-body .section-title { margin: 6px 0 32px; }
  .about-lead {
    font-family: var(--serif); font-size: 22px; line-height: 1.55;
    color: var(--ink); font-weight: 300;
    margin-bottom: 24px;
  }
  .about-lead em { font-style: italic; color: var(--rose); }
  .about-text {
    font-size: 17px; line-height: 1.85;
    color: var(--charcoal); margin-bottom: 22px;
    font-weight: 300;
    max-width: 640px;
  }
  /* "Get To Know Me →" — mirrors the .ws-card-view micro-interaction
     (underline sweeps in from the left, arrow glides 5px right) but is
     triggered by the link's own :hover since this is a standalone link,
     not an overlay on a parent card. */
  .about-know-link {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    margin-top: 6px;
    font-family: var(--sans);
    font-size: 11px;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    font-weight: 400;
    color: var(--ink);
    text-decoration: none;
  }
  .about-know-link-label {
    position: relative;
    padding-bottom: 4px;
  }
  .about-know-link-label::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.55s var(--ease);
  }
  .about-know-link .arrow {
    display: inline-block;
    transition: transform 0.4s var(--ease);
  }
  .about-know-link:hover .about-know-link-label::after {
    transform: scaleX(1);
  }
  .about-know-link:hover .arrow {
    transform: translateX(5px);
  }
  .about-pillars {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: 32px;
    margin: 48px 0 36px;
    padding-top: 36px;
    border-top: 1px solid var(--sand);
  }
  .pillar { display: flex; flex-direction: column; gap: 14px; }
  .pillar-num {
    font-family: var(--serif); font-style: italic;
    font-size: 30px; font-weight: 300;
    color: var(--rose); line-height: 1;
  }
  .pillar-text {
    font-size: 15.5px; line-height: 1.7;
    color: var(--charcoal);
    font-weight: 300;
  }
  .pillar-text strong {
    font-weight: 500; display: block;
    font-family: var(--sans); font-size: 11px;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink); margin-bottom: 10px;
  }

  .press-row {
    margin-top: 8px;
    padding-top: 28px; border-top: 1px solid var(--sand);
  }
  .press-label {
    display: block;
    font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 24px;
  }
  .press-image {
    display: block;
    width: 100%;
    max-width: 640px;
    height: auto;
  }

  /* ============ WEDDINGS / PORTRAITS — 2x2 landscape ============
     Padding matches the homepage .investment section (88px on all four
     sides) and the inner .container inherits the site's default
     max-width (var(--max) = 1560px, centered). That keeps the Wedding
     Stories / Portrait Sessions grids flush with "Your Story, Artfully
     Preserved." so the homepage rhythm stays coherent top-to-bottom. */
  .gallery-section { padding: 88px 88px; }
  .gallery-head {
    display: flex; justify-content: space-between; align-items: flex-end;
    margin-bottom: 56px;
    gap: 40px;
  }
  .gallery-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 4px;
  }
  .gallery-footer {
    display: flex; justify-content: center;
    margin-top: 64px;
  }
  .gallery-card {
    position: relative;
    display: block;
    overflow: hidden;
    cursor: pointer;
    text-decoration: none;
    color: inherit;
  }
  .gallery-card-img {
    width: 100%;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    position: relative;
    background: #000;
  }
  .gallery-card-img .img-inner {
    position: absolute; inset: -5% 0;
    background-size: cover;
    background-position: center;
    transform: translate3d(0, var(--py, 0px), 0) scale(var(--hscale, 1));
    transition: transform 1.1s var(--ease), filter 0.7s var(--ease);
    will-change: transform;
  }
  .gallery-card:hover .img-inner {
    --hscale: 1.06;
    filter: brightness(0.92);
  }
  .gallery-card::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(20,16,12,0) 45%, rgba(20,16,12,0.8) 100%);
    opacity: 0.85;
    transition: opacity 0.5s var(--ease);
    pointer-events: none;
  }
  .gallery-card:hover::after { opacity: 1; }
  .gallery-overlay {
    position: absolute; left: 0; right: 0; bottom: 0;
    z-index: 2;
    padding: 40px 36px 32px;
    transform: translateY(0);
    transition: transform 0.5s var(--ease);
  }
  .gallery-names {
    font-family: var(--serif); font-size: 28px; font-weight: 300;
    color: #fff; letter-spacing: 0.005em;
    line-height: 1.1;
  }
  .gallery-names em { font-style: italic; color: var(--rose); }
  .gallery-venue {
    font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
    color: rgba(255,255,255,0.75); margin-top: 10px;
  }
  .gallery-view {
    display: inline-flex; align-items: center; gap: 10px;
    margin-top: 18px;
    font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
    color: rgba(255,255,255,0);
    transform: translateY(10px);
    transition: color 0.4s var(--ease), transform 0.5s var(--ease);
  }
  .gallery-view .arrow { transition: transform 0.4s var(--ease); }
  .gallery-card:hover .gallery-view {
    color: #fff;
    transform: translateY(0);
  }
  .gallery-card:hover .gallery-view .arrow { transform: translateX(5px); }

  /* Primary CTA — the one arrow-pill component used everywhere:
     Send Inquiry, View All Galleries, View All Sessions, Plan Your
     Story, See My Film / Commercial Work, Inquire About Your Date,
     Check Your Date. Only the color palette changes per background;
     everything typographic (weight, arrow size, padding, letter-
     spacing, ::before sweep) is shared so the family reads as one.
     Font-weight 450 + 13px arrow match the 500/14 "heavier" impression
     of the original About buttons, but at half a step so cream-on-ink
     variants don't read as too bold. Requires the variable Jost axis
     (Jost:wght@300..500) loaded in each page <head>. */
  .btn-view-all {
    position: relative; overflow: hidden;
    display: inline-flex; align-items: center; gap: 14px;
    padding: 17px 34px;
    background: var(--ink); color: var(--cream);
    font-family: var(--sans); font-size: 12px; font-weight: 425;
    letter-spacing: 0.22em; text-transform: uppercase;
    border: 0; text-decoration: none;
    flex-shrink: 0; cursor: pointer;
    transition: color 0.4s var(--ease);
  }
  .btn-view-all::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
  }
  .btn-view-all span, .btn-view-all .arrow { position: relative; z-index: 1; }
  .btn-view-all .arrow {
    font-size: 12.5px; line-height: 1;
    transition: transform 0.4s var(--ease);
  }
  .btn-view-all:hover::before { transform: translateX(0); }
  .btn-view-all:hover .arrow { transform: translateX(6px); }

  .portraits-section { background: var(--warm); }

  /* ============ EXPERIENCE ============ */
  .experience { background: var(--ink); padding: 88px 88px; position: relative; }
  .experience .section-label { color: var(--gold); }
  .experience .section-label::before { background: var(--gold); }
  .experience .section-title { color: var(--cream); }
  .exp-head { text-align: center; margin-bottom: 72px; }
  .exp-head .section-label { justify-content: center; }

  .exp-rail { position: relative; }
  .exp-img-row,
  .exp-body-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 36px;
  }
  .exp-img {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    cursor: pointer;
    opacity: 0;
    transform: translateY(64px);
    overflow: hidden;
    will-change: transform;
  }
  /* Photo as a real <img> with object-fit: cover. Replaces the old
     background-image + ::before pattern, which on some mobile browsers
     resolved background-size: cover unreliably and rendered the photo
     near native pixel size — looking heavily zoomed in. */
  .exp-img-photo {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    filter: saturate(0.94) brightness(0.95);
    transform: scale(1);
    transition: transform 1s var(--ease);
    will-change: transform;
  }
  .exp-img:hover .exp-img-photo {
    transform: scale(1.12);
  }
  .exp-timeline {
    position: relative;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    column-gap: 36px;
    margin: 28px 0 32px;
    height: 18px;
    align-items: center;
  }
  .exp-timeline-line {
    position: absolute;
    left: 11.5%; right: 11.5%;
    top: 50%;
    height: 1px;
    background: linear-gradient(90deg, rgba(212,174,110,0.2) 0%, rgba(212,174,110,0.55) 50%, rgba(212,174,110,0.2) 100%);
    transform: translateY(-50%);
  }
  .exp-timeline-dot {
    width: 12px; height: 12px;
    border-radius: 50%;
    background: var(--gold);
    justify-self: center;
    position: relative;
    z-index: 2;
    box-shadow: 0 0 0 4px var(--ink);
    transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease), background 0.45s var(--ease);
  }
  .exp-timeline-dot.is-active {
    transform: scale(1.4);
    background: var(--rose);
    box-shadow: 0 0 0 4px var(--ink), 0 0 0 9px rgba(196,120,106,0.2);
  }

  .exp-body {
    display: flex; flex-direction: column; text-align: left;
    opacity: 0;
    transform: translateY(64px);
  }
  .exp-img {
    transition: opacity 1.4s var(--ease), transform 1.1s var(--ease);
  }
  .exp-body {
    transition: opacity 1.4s var(--ease), transform 1.4s var(--ease);
  }
  .exp-rail.visible .exp-img,
  .exp-rail.visible .exp-body { opacity: 1; transform: translateY(0); }
  .exp-rail.visible .exp-img:nth-child(1),
  .exp-rail.visible .exp-body:nth-child(1) { transition-delay: 0s; }
  .exp-rail.visible .exp-img:nth-child(2),
  .exp-rail.visible .exp-body:nth-child(2) { transition-delay: 0.16s; }
  .exp-rail.visible .exp-img:nth-child(3),
  .exp-rail.visible .exp-body:nth-child(3) { transition-delay: 0.32s; }
  .exp-rail.visible .exp-img:nth-child(4),
  .exp-rail.visible .exp-body:nth-child(4) { transition-delay: 0.48s; }

  .exp-num {
    font-family: var(--serif); font-style: italic;
    font-size: 24px; color: var(--gold);
    margin-bottom: 16px; line-height: 1;
  }
  .exp-title {
    font-size: 12px; letter-spacing: 0.22em; text-transform: uppercase;
    color: var(--cream); font-weight: 400;
    margin-bottom: 18px;
  }
  .exp-desc {
    font-size: 15.5px; line-height: 1.8;
    color: rgba(248,244,239,0.68); font-weight: 300;
  }

  .exp-pull {
    margin: 88px auto 0;
    max-width: 820px;
    text-align: center;
    border-top: 1px solid rgba(255,255,255,0.1);
    padding-top: 52px;
  }
  .exp-quote {
    font-family: var(--serif); font-size: clamp(22px, 2.4vw, 30px);
    line-height: 1.5; font-style: italic; color: var(--cream);
    margin-bottom: 24px; font-weight: 300;
  }
  /* Per-line clip masks. Each mask is a block the width of the quote;
     its inner span rises from translateY(110%) to 0 with a staggered
     delay set per line in main.js. padding-bottom + negative margin
     preserves italic descender space below the clip line. */
  .exp-quote .line-mask {
    display: block;
    overflow: hidden;
    padding-bottom: 0.2em;
    margin-bottom: -0.2em;
    line-height: 1.5;
  }
  .exp-quote .line-inner {
    display: inline-block;
    transform: translateY(115%);
    transition: transform 1.4s cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform;
  }
  .exp-pull.words-in .exp-quote .line-inner {
    transform: translateY(0);
  }
  .exp-pull .exp-quote-attr {
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 0.7s ease-out, transform 0.7s ease-out;
    transition-delay: var(--attr-delay, 0.8s);
  }
  .exp-pull.words-in .exp-quote-attr {
    opacity: 1;
    transform: translateY(0);
  }
  /* Hash-load: snap to final state with no transition churn. */
  .exp-pull.words-instant .exp-quote .line-inner,
  .exp-pull.words-instant .exp-quote-attr {
    transition: none !important;
    transition-delay: 0s !important;
  }
  .exp-quote-attr {
    font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
    color: var(--gold);
  }

  /* ============ TESTIMONIAL CAROUSEL ============ */
  .testimonials { background: var(--cream); padding: 88px 88px; }
  .testi-header { text-align: center; margin-bottom: 56px; }
  .testi-header .section-label { justify-content: center; }
  .testi-footer {
    display: flex; justify-content: center; align-items: center;
    gap: 28px;
    margin-top: 18px;
  }
  .testi-btn {
    position: relative;
    width: 48px; height: 48px;
    border: 0;
    background: transparent;
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
    color: var(--sand);
    transition: color 0.3s var(--ease), opacity 0.3s var(--ease);
  }
  .testi-btn svg {
    width: 32px; height: 32px;
    stroke-width: 1.4;
    transition: transform 0.35s var(--ease);
  }
  .testi-btn:hover { color: var(--rose); }
  .testi-btn:hover.testi-btn-prev svg { transform: translateX(-3px); }
  .testi-btn:hover.testi-btn-next svg { transform: translateX(3px); }
  .testi-btn:disabled { opacity: 0.35; cursor: not-allowed; color: var(--sand); }
  .testi-btn:disabled:hover svg { transform: none; }

  .testi-viewport {
    width: 100vw;
    position: relative;
    left: 50%;
    margin-left: -50vw;
    padding: 20px 0 28px 0;
    margin-top: 12px;
    overflow: hidden;
    cursor: grab;
  }
  .testi-viewport.is-dragging { cursor: grabbing; }
  .testi-track {
    display: flex;
    gap: 28px;
    padding: 0 28px;
    transition: transform 0.75s var(--ease);
    will-change: transform;
  }
  .testi-card {
    flex: 0 0 400px;
    background: var(--warm);
    padding: 0;
    position: relative;
    display: flex; flex-direction: column;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    border-radius: 8px;
    overflow: hidden;
    transition: background 0.45s var(--ease), transform 0.45s var(--ease), box-shadow 0.45s var(--ease);
  }
  .testi-card:hover {
    background: #F0E7D6;
    transform: translateY(-4px);
    box-shadow: 0 12px 24px -14px rgba(26,23,20,0.32);
  }
  .testi-card-body {
    padding: 0 32px 34px;
    display: flex; flex-direction: column;
    flex-grow: 1;
    position: relative;
  }
  .testi-img {
    width: 100%;
    aspect-ratio: 3 / 2;
    background-size: cover; background-position: center;
    margin-bottom: 0;
    position: relative;
    overflow: hidden;
  }
  .testi-img::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image: inherit;
    background-size: cover;
    background-position: center;
    transform: scale(1);
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform;
  }
  .testi-card:hover .testi-img::before { transform: scale(1.06); }
  /* Placeholder state — shown until final testimonial photos are dropped in.
     Warm two-tone gradient keyed to the cream/taupe palette, with a quiet
     italic serif label so the slot reads intentionally unfinished rather
     than broken. The ::before zoom layer is suppressed because there's no
     image to zoom. Swap the .is-placeholder class out and add
     style="background-image:url(...)" to activate the normal image state. */
  .testi-img.is-placeholder {
    background: linear-gradient(135deg, #EFE5D6 0%, #CDB89B 100%);
  }
  .testi-img.is-placeholder::before { background-image: none; }
  .testi-card:hover .testi-img.is-placeholder::before { transform: none; }
  .testi-img.is-placeholder::after {
    content: "Photograph to follow";
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    font-family: 'Cormorant Garamond', Georgia, serif;
    font-style: italic;
    font-size: 15px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(42, 38, 34, 0.36);
    pointer-events: none;
    white-space: nowrap;
  }
  .testi-mark {
    font-family: 'Playfair Display', Georgia, serif;
    font-style: italic; font-weight: 400;
    font-size: 84px; line-height: 0.4;
    color: var(--rose);
    opacity: 0.85;
    letter-spacing: -0.02em;
    margin: -20px 0 48px 0;
    padding-left: 6px;
    display: block;
  }
  .testi-text {
    font-size: 16.5px; line-height: 1.75; color: var(--charcoal);
    margin-top: 0;
    font-style: italic; font-family: var(--serif); font-weight: 400;
    flex-grow: 1;
  }
  .testi-couple {
    margin-top: 28px; padding-top: 22px;
    border-top: 1px solid var(--sand);
    display: flex; justify-content: space-between; align-items: flex-end;
    gap: 18px;
  }
  .testi-couple-meta { flex: 1 1 auto; min-width: 0; }
  .testi-names {
    font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--rose); font-weight: 500;
  }
  .testi-venue {
    font-size: 14px; color: var(--muted); margin-top: 6px;
    font-style: italic; font-family: var(--serif);
  }
  .testi-view {
    position: absolute;
    right: 18px; bottom: 16px;
    display: inline-flex; align-items: center; gap: 8px;
    font-family: var(--sans); font-size: 10px;
    letter-spacing: 0.2em; text-transform: uppercase;
    color: #FBF6EC; font-weight: 500;
    text-shadow: 0 1px 14px rgba(0,0,0,0.45);
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
    z-index: 2;
    pointer-events: none;
  }
  .testi-view .arrow { transition: transform 0.35s var(--ease); }
  /* Underline sweeps left-to-right under "View Gallery" when the card
     is hovered. A background gradient sized 0 → 100% gives us a smoothly
     animatable underline that hugs only the text (not the arrow). */
  .testi-view-text {
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 1px;
    padding-bottom: 3px;
    transition: background-size 0.55s var(--ease) 0.12s;
  }
  .testi-card:hover .testi-view { opacity: 1; transform: translateY(0); }
  .testi-card:hover .testi-view .arrow { transform: translateX(5px); }
  .testi-card:hover .testi-view-text { background-size: 100% 1px; }

  .testi-dots {
    display: flex; justify-content: center; align-items: center; gap: 8px;
  }
  .testi-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--sand);
    border: 0; padding: 0; cursor: pointer;
    transition: background 0.3s, transform 0.3s;
  }
  .testi-dot.active { background: var(--rose); transform: scale(1.4); }

  /* ============ INVESTMENT ============ */
  .investment { background: var(--warm); padding: 88px 88px; }
  .investment-inner {
    max-width: var(--max); margin: 0 auto;
    display: grid; grid-template-columns: 7fr 5fr;
    gap: 80px; align-items: start;
  }
  .investment-img-wrap {
    overflow: hidden;
    position: relative;
  }
  .investment-copy .section-title { margin: 6px 0 28px; }
  .investment-lead {
    font-size: 17.5px; line-height: 1.8; color: var(--charcoal);
    margin-bottom: 36px; font-weight: 300;
    max-width: 600px;
  }
  .investment-img {
    width: 100%;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    position: relative;
  }
  .investment-img .img-inner {
    position: absolute; inset: -5% 0;
    background-size: cover;
    background-position: center;
    transform: translate3d(0, var(--py, 0px), 0) scale(var(--hscale, 1));
    transition: transform 1.1s var(--ease);
    will-change: transform;
  }
  .investment-copy:hover .investment-img .img-inner { --hscale: 1.02; }
  .investment-note {
    font-family: var(--serif); font-style: italic; font-size: 17px;
    color: var(--muted); margin-top: 28px;
    padding-top: 24px; border-top: 1px solid var(--sand);
    line-height: 1.65;
  }
  /* Investment Check Your Date — outline variant of .btn-view-all.
     Starts transparent with an ink hairline + ink label. The ::before
     sweep is repainted ink (instead of rose), so hovering slides a dark
     fill across the button and flips the label to cream. Same 17/34
     bounding box as the filled siblings — the 1px border is absorbed
     into the padding so the overall button size matches. */
  .investment-cta {
    margin-top: 28px;
    background: transparent;
    color: var(--ink);
    border: 1px solid var(--ink);
    padding: 16px 33px;
  }
  .investment-cta::before { background: var(--ink); }
  .investment-cta:hover { color: var(--cream); }

  .packages {
    display: flex; flex-direction: column;
    border-top: 1px solid var(--sand);
  }
  .pkg {
    display: grid;
    grid-template-columns: 48px 1fr auto;
    align-items: center;
    padding: 28px 0;
    border-bottom: 1px solid var(--sand);
    gap: 24px;
    transition: padding-left 0.4s var(--ease);
  }
  .pkg:hover { padding-left: 12px; }
  .pkg-idx {
    font-family: var(--serif); font-style: italic; font-size: 22px;
    color: var(--rose); line-height: 1;
  }
  .pkg-name {
    font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--ink); font-weight: 400;
  }
  .pkg-desc {
    font-size: 16px; color: var(--muted); margin-top: 8px;
    font-family: var(--serif); font-style: italic;
    line-height: 1.5;
  }
  .pkg-price {
    font-family: var(--serif); font-size: 30px; font-weight: 300;
    color: var(--ink); text-align: right;
  }

  .includes-box {
    background: var(--cream);
    padding: 36px;
    margin-top: 32px;
    border-radius: 8px;
  }
  .includes-title {
    font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase;
    color: var(--gold); margin-bottom: 22px;
  }
  .include-item {
    display: flex; gap: 18px;
    padding: 16px 0;
    border-bottom: 1px solid var(--warm);
    font-size: 16px; line-height: 1.6; color: var(--charcoal);
  }
  .include-item:last-child { border-bottom: none; }
  .include-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--rose); flex-shrink: 0; margin-top: 8px;
  }
  .invest-cta { margin-top: 36px; }

  .btn-invest {
    position: relative; overflow: hidden;
    display: inline-block;
    padding: 18px 40px;
    background: var(--ink); color: var(--cream);
    font-size: 12px; letter-spacing: 0.2em; text-transform: uppercase;
    text-decoration: none;
    transition: color 0.5s var(--ease);
  }
  .btn-invest::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
  }
  .btn-invest span { position: relative; z-index: 1; }
  .btn-invest:hover { color: var(--cream); }
  .btn-invest:hover::before { transform: translateX(0); }

  /* ============ LET'S CONNECT — dark inquire form (cinematic A adapted) ============ */
  .connect {
    background: var(--ink);
    padding: 104px 88px;
    position: relative;
    overflow: hidden;
    color: var(--cream);
    /* Anchor offset — when #contact is jumped to via any
       href="#contact" link (nav CTA, mobile drawer CTA, Check Your
       Date button), reserve --nav-h at the top so the section's
       upper edge lines up exactly with the bottom of the fixed nav
       bar instead of disappearing behind it. scroll-margin-top only
       affects anchor navigation, not layout, so the section still
       paints flush against the preceding block. */
    scroll-margin-top: var(--nav-h);
  }
  .connect::before {
    content: '';
    position: absolute; top: 0; right: -10%;
    width: 55%; height: 100%;
    background-size: cover; background-position: center;
    background-image: url('../images/photos/home/contact/contact-bg.jpg');
    filter: contrast(1.04);
    opacity: 0.85;
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 45%);
    mask-image: linear-gradient(90deg, transparent 0%, #000 45%);
    pointer-events: none;
    z-index: 0;
  }
  .connect-inner {
    max-width: var(--max); margin: 0 auto;
    position: relative; z-index: 2;
  }
  .connect-head { margin-bottom: 56px; max-width: 720px; }
  .connect .section-label { color: var(--rose); }
  .connect .section-label::before { background: var(--rose); }
  .connect-title {
    font-family: var(--serif); font-size: clamp(44px, 5.2vw, 72px);
    font-weight: 300; color: var(--cream);
    line-height: 1.08; letter-spacing: -0.01em;
    margin: 4px 0 24px;
  }
  .connect-title em { font-style: italic; color: var(--rose); }
  .connect-body {
    font-family: var(--serif); font-size: 21px;
    line-height: 1.6; color: rgba(248,244,239,0.7);
    font-weight: 300;
    max-width: 640px;
  }

  .connect-form {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: 1px;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.1);
    max-width: 920px;
  }
  /* Global [hidden] display:none is normally applied by the UA
     stylesheet, but .connect-form's display:grid class rule wins on
     specificity and keeps the form visible after submission. Force
     hidden here so JS .hidden = true actually removes it from view. */
  .connect-form[hidden] { display: none; }
  .field {
    background: var(--dark-gray);
    padding: 24px 28px;
    display: flex; flex-direction: column; gap: 10px;
  }
  .field.full { grid-column: 1 / -1; }
  .field label {
    font-family: var(--sans); font-size: 10px;
    letter-spacing: 0.24em; text-transform: uppercase;
    color: rgba(248,244,239,0.5); font-weight: 400;
    display: inline-flex; align-items: center; gap: 9px;
  }
  /* A tiny rose dot next to the label marks fields that must be filled in.
     Uses :has() so required-ness is driven entirely by the input's attribute
     — no separate HTML markup needed on the label. */
  .field:has(input[required]) label::after,
  .field:has(textarea[required]) label::after,
  .field:has(select[required]) label::after {
    content: '';
    width: 4px; height: 4px;
    border-radius: 50%;
    background: var(--rose);
    flex-shrink: 0;
  }
  .field input,
  .field textarea,
  .field select {
    background: transparent; border: 0; outline: 0;
    color: var(--cream); font-family: var(--serif);
    font-size: 20px; font-weight: 300;
    padding: 4px 0;
    border-bottom: 1px solid rgba(255,255,255,0.14);
    transition: border-color 0.3s var(--ease);
    width: 100%;
  }
  .field input::placeholder,
  .field textarea::placeholder { color: rgba(248,244,239,0.35); font-style: italic; }
  .field select { color: var(--cream); }
  .field select option { background: var(--ink); color: var(--cream); }
  .field input:focus,
  .field textarea:focus,
  .field select:focus { border-bottom-color: var(--rose); }
  .field textarea {
    resize: vertical; min-height: 100px;
    font-size: 18px; line-height: 1.55;
  }

  /* Date input: subtle red underline on invalid value (format check) */
  .field input:invalid:not(:placeholder-shown):not(:focus) {
    border-bottom-color: rgba(229, 115, 115, 0.6);
  }
  .submit-row {
    grid-column: 1 / -1;
    background: var(--dark-gray);
    padding: 24px 28px;
    display: flex; justify-content: space-between; align-items: center;
    gap: 20px; flex-wrap: wrap;
  }
  .submit-row small {
    font-family: var(--sans); font-size: 12px;
    letter-spacing: 0.16em; text-transform: uppercase;
    color: rgba(248,244,239,0.55);
  }
  /* Send Inquiry — same primary arrow-pill as .btn-view-all, colocated
     with the contact form block. Kept as its own class (not merged into
     .btn-view-all) so the contact section's form styling stays
     self-contained, but the visual contract is identical: 17/34
     padding, 450 weight, 13px arrow, rose ::before sweep. */
  .btn-submit {
    position: relative; overflow: hidden;
    display: inline-flex; align-items: center; gap: 14px;
    padding: 17px 34px;
    background: var(--ink); color: var(--cream);
    font-family: var(--sans); font-size: 12px; font-weight: 425;
    letter-spacing: 0.22em; text-transform: uppercase;
    border: 0; cursor: pointer;
    transition: color 0.4s var(--ease);
  }
  .btn-submit::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
  }
  .btn-submit span, .btn-submit .arrow { position: relative; z-index: 1; }
  .btn-submit .arrow {
    font-size: 12.5px; line-height: 1;
    transition: transform 0.4s var(--ease);
  }
  .btn-submit:hover::before { transform: translateX(0); }
  .btn-submit:hover .arrow { transform: translateX(6px); }
  /* Disabled/loading state — dimmed, no cursor pointer, sweep blocked. */
  .btn-submit:disabled {
    cursor: default;
    opacity: 0.55;
  }
  .btn-submit:disabled:hover::before { transform: translateX(-101%); }
  .btn-submit:disabled:hover .arrow { transform: none; }

  /* Honeypot — visually and interactively absent for real users, but
     still present in the DOM so bots (which fill every input) will
     check it. Web3Forms discards any submission where botcheck is
     non-empty. Using off-screen positioning instead of display:none
     because some bots skip display:none fields. */
  .connect-botcheck {
    position: absolute !important;
    left: -10000px !important;
    width: 1px !important;
    height: 1px !important;
    opacity: 0 !important;
    pointer-events: none !important;
  }

  /* Error message below the submit button — rose hairline underneath
     so it reads as part of the form chrome, not a system alert. */
  .connect-error {
    margin: 0;
    font-family: var(--sans);
    font-size: 13px;
    letter-spacing: 0.04em;
    color: var(--rose);
    line-height: 1.55;
    max-width: 520px;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
    pointer-events: none;
  }
  .connect-error.is-visible {
    opacity: 1;
    transform: translateY(0);
  }

  /* Success panel — same max-width as the form so the section doesn't
     visually reflow, same dark-gray tile feel, but sits alone. Fades
     in when the form is hidden. */
  .connect-success {
    max-width: 920px;
    background: var(--dark-gray);
    border: 1px solid rgba(255,255,255,0.1);
    padding: clamp(48px, 7vw, 72px) clamp(28px, 5vw, 64px);
    text-align: left;
    animation: connectSuccessIn 0.8s var(--ease) both;
  }
  .connect-success .section-label {
    color: var(--gold);
    margin-bottom: 18px;
  }
  .connect-success-title {
    font-family: var(--serif);
    font-size: clamp(36px, 4.2vw, 56px);
    font-weight: 300;
    font-style: italic;
    color: var(--cream);
    margin: 0 0 20px;
    line-height: 1.1;
    letter-spacing: -0.01em;
  }
  .connect-success-body {
    font-family: var(--serif);
    font-size: 20px;
    font-weight: 300;
    line-height: 1.55;
    color: rgba(248,244,239,0.82);
    margin: 0;
    max-width: 560px;
  }
  @keyframes connectSuccessIn {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  /* ============ FOOTER — bg image with locations ============ */
  footer {
    position: relative;
    overflow: hidden;
    min-height: 560px;
    padding: 0;
    color: var(--cream);
    background: var(--ink);
  }
  .footer-bg {
    position: absolute; inset: -18% 0;
    background-image: url('../images/photos/home/footer/footer-bg.jpg');
    background-size: cover;
    /* Focal point tuned to the couple's faces (bride ~55%/35%, groom
       ~65%/35%). x=60% biases horizontal crop toward them on narrow
       viewports where the wide landscape image would otherwise lose
       the groom's side to a center crop. y=40% biases vertical crop
       toward the faces on wide desktop viewports without slicing off
       the bridge towers above them. */
    background-position: 65% 40%;
    z-index: 0;
    transform: translate3d(0, var(--py, 0px), 0);
    will-change: transform;
  }
  .footer-bg::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(20,16,12,0) 0%, rgba(20,16,12,0.18) 55%, rgba(20,16,12,0.62) 100%);
  }
  .footer-inner {
    position: relative; z-index: 1;
    display: flex; flex-direction: column;
    justify-content: flex-end;
    min-height: 560px;
    padding: 100px 80px 40px;
  }
  .footer-hero {
    width: 100%;
    display: flex; justify-content: space-between; align-items: flex-end;
    gap: 48px; flex-wrap: wrap;
  }
  .footer-hero-left { flex: 1 1 auto; min-width: 0; }
  .footer-eyebrow {
    font-size: 12px; letter-spacing: 0.28em; text-transform: uppercase;
    color: rgba(255,255,255,0.75);
    margin-bottom: 26px;
    display: inline-flex; align-items: center; gap: 14px;
  }
  .footer-eyebrow::before {
    content: ''; width: 28px; height: 1px; background: var(--rose);
  }
  .footer-locations {
    font-family: var(--serif); font-weight: 300;
    font-size: clamp(44px, 6vw, 88px);
    line-height: 1.02; color: #fff;
    letter-spacing: -0.01em;
    margin-bottom: 16px;
  }
  .footer-locations em { font-style: italic; }
  .footer-tagline {
    text-align: right;
    font-family: var(--serif); font-weight: 300; font-style: italic;
    font-size: clamp(32px, 3.2vw, 45px);
    line-height: 1.3; color: rgba(255,255,255,0.82);
    padding-bottom: 12px;
    flex-shrink: 0;
  }
  .footer-tagline em { font-style: italic; }
  .footer-meta {
    width: 100%;
    padding-top: 32px; margin-top: 40px;
    border-top: 1px solid rgba(255,255,255,0.16);
    display: flex; justify-content: space-between; align-items: center;
    gap: 32px; flex-wrap: wrap;
  }
  .footer-nav {
    display: flex; gap: 32px; flex-wrap: wrap;
  }
  .footer-nav a {
    font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
    color: rgba(255,255,255,0.7); text-decoration: none;
    transition: color 0.25s var(--ease);
  }
  .footer-nav a:hover { color: #fff; }
  .footer-social {
    display: flex; gap: 22px; flex-wrap: wrap;
  }
  .footer-social a {
    font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
    color: #fff; text-decoration: none;
    transition: color 0.25s var(--ease);
  }
  .footer-social a:hover { color: var(--rose); }
  .footer-copy {
    font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
    color: rgba(255,255,255,0.55);
  }

  /* ============ FADE UP ============ */
  .fade-up {
    opacity: 0;
    transform: translateY(96px);
    transition:
      opacity 1.3s cubic-bezier(0.33, 0, 0.2, 1),
      transform 2.1s cubic-bezier(0.2, 0.65, 0.2, 1);
  }
  .fade-up.visible { opacity: 1; transform: translateY(0); }
  /* Gallery page prefers a softer, more subtle fade-up — photo tiles are
     dense and the big homepage-style rise felt overbearing in that grid.
     Timing stays the same so the motion feels unhurried, just shorter. */
  body.page-gallery .fade-up { transform: translateY(64px); }
  body.page-gallery .fade-up.visible { transform: translateY(0); }

  /* ============ GALLERY PAGE (story-*) ============
     Namespaced with .story- to avoid colliding with the homepage's
     .gallery-* classes (which drive the Weddings/Portraits cards).
     Nav/footer/fade-up patterns are reused as-is from the homepage. */

  /* Gallery page gets the solid-nav treatment from the top (no hero). */
  body.page-gallery,
  body.page-weddings-index,
  body.page-portraits-index,
  body.page-investment { padding-top: var(--nav-h); }

  /* --- Header: centered text block on muted backdrop.
     Shallow, symmetric padding — the grid section below supplies its own
     top padding, so the header only needs enough breathing room to feel
     like a defined opening, not a standalone hero. Top and bottom match
     so the nav-to-eyebrow distance equals the subtitle-to-section-bottom
     distance regardless of how many lines the subtitle wraps to. */
  .story-header {
    position: relative;
    min-height: 24vh;
    padding: clamp(32px, 3.5vw, 56px) 40px;
    text-align: center;
    overflow: hidden;
    isolation: isolate;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .story-header-backdrop {
    position: absolute; inset: 0;
    background:
      radial-gradient(ellipse at 50% 0%, rgba(196,120,106,0.06) 0%, transparent 55%),
      radial-gradient(ellipse at 20% 100%, rgba(184,150,90,0.05) 0%, transparent 50%),
      linear-gradient(180deg, var(--warm) 0%, var(--cream) 100%);
    z-index: -1;
  }
  .story-header-backdrop::after {
    /* subtle paper grain */
    content: '';
    position: absolute; inset: 0;
    background-image:
      radial-gradient(rgba(26,23,20,0.04) 1px, transparent 1px);
    background-size: 3px 3px;
    opacity: 0.5;
    mix-blend-mode: multiply;
    pointer-events: none;
  }
  .story-header-inner {
    max-width: 820px;
    margin: 0 auto;
    padding: 0 16px;
  }
  .story-eyebrow {
    font-family: var(--sans);
    font-size: 12px;
    letter-spacing: 0.42em;
    text-transform: uppercase;
    color: var(--rose);
    font-weight: 400;
    margin-bottom: 16px;
  }
  .story-title {
    font-family: var(--serif);
    font-weight: 300;
    font-style: normal;
    color: var(--ink);
    font-size: clamp(42px, 5.4vw, 76px);
    line-height: 0.98;
    letter-spacing: -0.01em;
    margin-bottom: 18px;
  }
  .story-title em {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    color: var(--rose);
    padding: 0 0.06em;
  }
  .story-hairline {
    width: 44px;
    height: 1px;
    background: var(--rose);
    margin: 0 auto 16px;
    opacity: 0.6;
  }
  .story-venue {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(17px, 1.6vw, 22px);
    color: var(--charcoal);
  }
  /* --- Grid: 2 columns desktop, 1 column mobile.
     Left/right padding and inter-image gap match the homepage's
     .gallery-section / .gallery-grid so the two pages feel like one system. */
  .story-grid-section {
    padding: 88px 80px;
    background: var(--cream);
  }
  .story-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
  }
  .story-photo {
    margin: 0;
    cursor: zoom-in;
  }
  .story-photo-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--warm);
  }
  .story-photo-frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 1.4s var(--ease), filter 0.8s var(--ease);
    will-change: transform;
  }
  /* Hover: gentle zoom only — no drop shadow, so the grid stays flat and
     editorial and neighboring tiles don't get darkened at the gutters. */
  .story-photo:hover .story-photo-frame img { transform: scale(1.045); }

  /* Desktop 2-col layout: row partners fade together (no per-tile stagger),
     so tiles that share a row don't arrive at different speeds. Row-level
     staggering happens naturally because each row crosses the viewport at
     its own scroll time. Mobile re-enables the odd/even stagger inside the
     @media block below, where each tile IS its own row. */

  /* --- Story footer: quiet signoff beneath the grid --- */
  .story-footer {
    margin: clamp(48px, 6vw, 80px) 0 0;
    padding-top: clamp(32px, 4vw, 56px);
    border-top: 1px solid rgba(26,23,20,0.08);
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 32px;
    flex-wrap: wrap;
  }
  /* Signature composition — small caps eyebrow, italic signature, hairline,
     brand mark. Echoes the story-header stack so the page opens and closes
     on the same visual note. */
  .story-signature {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }
  .story-signoff {
    font-family: var(--sans);
    font-size: 11px;
    letter-spacing: 0.42em;
    text-transform: uppercase;
    color: var(--rose);
    font-weight: 400;
    margin: 0 0 14px;
  }
  .story-author {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(30px, 3.4vw, 44px);
    line-height: 1;
    letter-spacing: -0.005em;
    color: var(--ink);
    /* Playfair italic caps (A, J, K, M, N, W…) have wide left-side swashes
       that overhang the character box. Safari sometimes clips that overhang
       to the element bounds during the .fade-up transform animation, so
       reserve room on both sides — same trick .story-title em uses. */
    padding: 0 0.12em;
    margin: 0 0 14px -0.12em;
  }
  .story-signature-rule {
    display: block;
    width: 36px;
    height: 1px;
    background: var(--rose);
    opacity: 0.55;
    margin: 0 0 14px;
  }
  .story-brand {
    font-family: var(--sans);
    font-size: 10.5px;
    letter-spacing: 0.38em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 400;
    margin: 0;
  }
  /* ============ LIGHTBOX ============ */
  .lightbox {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(14, 12, 10, 0.92);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.45s var(--ease);
  }
  .lightbox.open {
    opacity: 1;
    pointer-events: auto;
  }
  .lightbox-stage {
    position: relative;
    width: 92vw;
    height: 88vh;
    max-width: 1600px;
    display: grid;
    place-items: center;
    margin: 0;
    transform: scale(0.985);
    transition: transform 0.55s var(--ease);
  }
  .lightbox.open .lightbox-stage { transform: scale(1); }
  /* Each frame shrink-wraps its image at the image's natural contain-fit
     size. overflow:hidden clips the start-of-cycle overscale so the frame
     reads as a fixed crop. Both frames share the same grid cell so they
     can crossfade in place. */
  .lightbox-frame {
    grid-area: 1 / 1;
    display: block;
    overflow: hidden;
    line-height: 0;
    box-shadow: 0 40px 80px rgba(0,0,0,0.4);
    opacity: 0;
    transition: opacity 1.4s ease;
  }
  .lightbox-frame.active { opacity: 1; }
  /* The zoom-out happens on the image itself, inside its fixed frame —
     matches .hero-slide's opacity 1.4s / transform 6s pairing. */
  .lightbox-img {
    display: block;
    max-width: min(92vw, 1600px);
    max-height: 88vh;
    user-select: none;
    -webkit-user-drag: none;
    transform: scale(1.05);
    transition: transform 6s ease;
  }
  .lightbox-frame.active .lightbox-img { transform: scale(1); }
  .lightbox.is-dragging { cursor: grabbing; }
  .lightbox.is-dragging .lightbox-img,
  .lightbox.is-dragging .lightbox-frame { transition: none; }

  /* Ghost close button — mirrors `.drawer-close` (the X that closes the
     mobile hamburger menu): transparent background, no border, a bare
     stroked X that rotates 90° and shifts to the rose accent on hover.
     The only adaptation is color: the lightbox sits on a dark backdrop
     so we use #fff for the glyph instead of var(--ink). Sizing stays
     at 48×48 (42×42 on mobile via the <=900px override) with a 26px
     SVG matching the drawer's X width. */
  .lightbox-close {
    position: absolute;
    top: 24px; right: 24px;
    width: 48px; height: 48px;
    padding: 8px;
    background: transparent;
    border: 0;
    color: #fff;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.3s var(--ease), transform 0.3s var(--ease);
    z-index: 2;
  }
  .lightbox-close svg { width: 26px; height: 26px; }
  .lightbox-close:hover {
    background: transparent;
    color: var(--rose);
    transform: rotate(90deg);
  }

  /* Ghost prev/next buttons — same treatment as `.lightbox-close` and
     `.drawer-close`: transparent background, no border, a bare stroked
     chevron that shifts to the rose accent on hover. The hover motion
     is directional instead of rotational — the chevron SVG slides in
     the direction it points (prev: −6px X, next: +6px X) and scales
     up a hair, so the arrows feel active without moving the hit-target
     itself. Disabled state dims the whole button and cancels hover so
     you can't "nudge" a button that has nowhere to go. */
  .lightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 48px; height: 48px;
    padding: 8px;
    background: transparent;
    border: 0;
    color: #fff;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.3s var(--ease), opacity 0.3s var(--ease);
    z-index: 2;
  }
  .lightbox-nav svg {
    /* The chevron path only spans ~7 units of its 24-unit viewBox
       horizontally, where the X path spans ~14. Scaling the SVG up
       and bumping the stroke gives the arrow the same optical weight
       as the close button's X instead of looking like a thinner sibling. */
    width: 34px; height: 34px;
    /* Longer easing than the color fade so the grow feels deliberate,
       not a flick. Pure scale (no translate) keeps the chevron locked
       to the button's geometric center while it swells toward the
       cursor. */
    transition: transform 0.35s var(--ease);
  }
  .lightbox-nav svg path { stroke-width: 1.5; }
  .lightbox-nav:hover { color: var(--rose); }
  .lightbox-nav:hover:not(:disabled) svg { transform: scale(1.5); }
  .lightbox-nav:disabled {
    opacity: 0.25;
    cursor: default;
  }
  .lightbox-prev { left: 32px; }
  .lightbox-next { right: 32px; }

  /* Lock scroll on body when lightbox is open */
  body.lightbox-open { overflow: hidden; }

  /* ============ WEDDING STORIES INDEX (ws-*) ============
     Listing page: /weddings/index.html. Header reuses the .story-header
     block (same warm-cream backdrop, eyebrow → serif title → hairline →
     italic subtitle stack) so the opening beat matches the individual
     gallery pages a couple will click through to. Below the header, a
     responsive grid of <a> cards — each one a landscape photograph with
     couple names, venue, and a "View Gallery →" cue in an always-visible
     text block below the image. The entire anchor is the click target.
     Grid auto-flows any number of entries; collapses to one column on
     mobile (see responsive blocks further down).
     ================================================================= */
  .ws-grid-section {
    /* Padding mirrors the homepage .investment section (88px on all four
       sides) — the Wedding Stories archive sits inside the same content
       frame as "Your Story, Artfully Preserved." so the listing page and
       the homepage feel like one system. */
    padding: 88px 88px;
    background: var(--cream);
  }
  .ws-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* Row gap is tight — the card's own text block (names/venue/view
       gallery) already supplies vertical breathing room between the
       bottom of one row and the top of the next, so the grid's explicit
       row-gap can stay modest. Column gap stays tight so the two
       photographs dominate the row. */
    gap: clamp(36px, 3.2vw, 52px) clamp(20px, 2.4vw, 40px);
    max-width: var(--max);
    margin: 0 auto;
  }
  /* Card anchor — the whole card is clickable. text-decoration: none +
     color: inherit let the inner <h2>/<p>/<span> pick up their own colors
     from the rules below without the default link styling bleeding in. */
  .ws-card {
    display: block;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
  }
  .ws-card-img {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 2;  /* shared landscape crop — DSLR-native 3:2
                             keeps each photograph at the same footprint
                             so rows align cleanly. */
    overflow: hidden;
    background: var(--warm);
  }
  /* Inner background-image div drives both hover zoom and scroll
     parallax. The 5% vertical overhang (inset: -5% 0) gives the JS
     parallax room to translate the image without exposing the cream
     background. Matches .gallery-card-img .img-inner on the homepage
     so the parallax() loop in main.js can include both via one query. */
  .ws-card-img .img-inner {
    position: absolute; inset: -5% 0;
    background-size: cover;
    background-position: center;
    transform: translate3d(0, var(--py, 0px), 0) scale(var(--hscale, 1));
    transition: transform 1.1s var(--ease), filter 0.7s var(--ease);
    will-change: transform;
  }
  /* Hover: quiet image zoom. No overlay or gradient — the text below
     is already doing the work, so the photograph stays clean. */
  .ws-card:hover .ws-card-img .img-inner {
    --hscale: 1.045;
  }
  /* Text block: always visible beneath the photo. Centered so names,
     venue, and the view-gallery cue stack as a single composed unit. */
  .ws-card-body {
    text-align: center;
    padding: clamp(20px, 2vw, 28px) 8px 0;
  }
  .ws-card-names {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(26px, 2.4vw, 36px);
    line-height: 1.1;
    letter-spacing: -0.005em;
    color: var(--ink);
    margin: 0 0 14px;
  }
  /* Second name is italicized + rose — same treatment the homepage
     featured-wedding cards use for the overlay names, so the visual
     relationship between the two pages reads instantly. */
  .ws-card-names em {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    color: var(--rose);
    padding: 0 0.04em;
  }
  .ws-card-venue {
    font-family: var(--sans);
    font-size: 11px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 20px;
  }
  /* "View Gallery →" — a single inline row. On card hover, an underline
     wipes in beneath the label from the left while the arrow nudges
     right; both share the site's ease curve so it feels like one
     gesture. Only the label gets the underline (not the arrow), so the
     arrow can slide freely past the line's right edge. */
  .ws-card-view {
    display: inline-flex;
    /* Baseline alignment keeps the arrow glyph sitting on the same
       typographic baseline as "VIEW GALLERY". align-items:center would
       vertically-center the arrow against the label's box (which is
       taller by 4px of padding-bottom for the hover underline) and drift
       the arrow upward. */
    align-items: baseline;
    gap: 6px;
    font-family: var(--sans);
    font-size: 11px;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    font-weight: 400;
    color: var(--ink);
  }
  .ws-card-view-label {
    position: relative;
    padding-bottom: 4px;
  }
  .ws-card-view-label::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.55s var(--ease);
  }
  .ws-card:hover .ws-card-view-label::after {
    transform: scaleX(1);
  }
  .ws-card-view .arrow {
    display: inline-block;
    transition: transform 0.4s var(--ease);
  }
  .ws-card:hover .ws-card-view .arrow {
    transform: translateX(5px);
  }

  /* ============ ABOUT PAGE (ab-*) ============
     Namespaced under .ab- + scoped to body.page-about where useful.
     Design intent: editorial + cinematic — compact text header (no full
     hero), tilted intro polaroid, philosophy pillars that mirror the
     homepage's .about-pillars exactly, asymmetric film-director grid,
     full-bleed "behind the lens" strip, parallax California, masonry
     travel gallery with native aspect ratios, letter-close CTA. Uses
     the established cream/warm/rose/gold palette. */

  /* Photo column bleeds behind the fixed nav, so no top padding here —
     the hero's own padding handles clearance. Nav starts transparent
     (.hero-nav) and fades to solid once .ab-hero scrolls out of view,
     wired up by the heroEl scroll listener in main.js. */

  /* Page-specific fade-up: tighten travel distance so the intro polaroid
     + copy arrive together without the big homepage hero-style lift. */
  body.page-about .fade-up { transform: translateY(56px); }
  body.page-about .fade-up.visible { transform: translateY(0); }

  /* --- STICKY STACK — "page-turn" cover scroll ---
     Each .ab-stack-item is position:sticky at top:0 with height:100vh and
     overflow:hidden. As the user scrolls, the next section (later in DOM)
     slides up from below and paints over the previous one (which remains
     stuck at top:0). The parent .ab-stack is a plain block that inherits
     each child's scroll range. Works on every breakpoint — sections are
     tuned to fit a single mobile viewport on the small end. */
  body.page-about .ab-stack {
    position: relative;
  }
  body.page-about .ab-stack-item {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
  }
  /* On mobile, override any section's own `display` so its inner content
     block centers vertically inside the 100vh sticky frame. Without this,
     sections that anchor content to the top (hero, travel) leave a big
     empty bottom while sections with more content crop below the fold. */
  @media (max-width: 899px) {
    body.page-about .ab-stack-item {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: stretch;
    }
  }

  /* --- HERO — full-bleed B&W backdrop under the title + intro ---
     One 100vh sticky-stack page. A grayscale photograph fills the
     entire section (absolute bg + dark wash), the page title sits at
     the top beneath the fixed nav, and the polaroid + reading copy
     (light-on-dark) live in the remaining space below. */
  .ab-hero {
    position: relative;
    width: 100%;
    min-height: 100vh;
    /* Override the global `section { padding: 110px 48px }` so the
       photo truly bleeds edge-to-edge and all the way behind the nav. */
    padding: 0;
    display: flex;
    flex-direction: column;
    background: var(--ink);
    color: #fff;
    overflow: hidden;
    isolation: isolate;
  }
  .ab-hero-bg {
    position: absolute;
    /* Negative Y inset gives the bg ~140px of vertical slack so the JS
       parallax (--parallax-y up to ±110px) can shift the photo without
       ever revealing an edge outside the cover crop. */
    inset: -140px 0;
    background-size: cover;
    /* Parallax Y offset is driven by JS (--parallax-y on scroll). Using
       background-position keeps parallax on the paint layer and avoids
       conflicting with the Ken Burns transform below. */
    background-position: 50% calc(50% + var(--parallax-y, 0px));
    /* Full grayscale so the photo reads as a moody editorial backdrop. */
    filter: grayscale(1) brightness(0.72) contrast(1.08);
    transform: scale(1.04);
    animation: abHeaderKen 22s var(--ease) forwards;
    z-index: 0;
  }
  @keyframes abHeaderKen {
    from { transform: scale(1.10); }
    to   { transform: scale(1.00); }
  }
  .ab-hero-wash {
    position: absolute; inset: 0;
    /* Gradient tilted to the bottom — the copy column on the right
       sits in the darker zone so the white body text stays legible. */
    background:
      linear-gradient(
        180deg,
        rgba(20,16,12,0.42) 0%,
        rgba(20,16,12,0.48) 40%,
        rgba(20,16,12,0.62) 78%,
        rgba(20,16,12,0.78) 100%
      );
    z-index: 1;
    pointer-events: none;
  }

  /* ---- Mobile-only second sticky page of the hero ------------------
     On desktop the hero is a single .ab-stack-item — the #ab-hero-copy
     section is hidden entirely. On mobile (see the <=900px block
     further down), we flip this to display: flex so it becomes its
     own 100vh sticky page carrying the section-title + body copy that
     can't fit alongside the polaroid. */
  .ab-hero-copy { display: none; }
  /* Mobile-only red-ring mark that crowns the section-title on the
     second sticky page. Hidden on desktop — the desktop hero already
     carries enough visual weight. See the <=900px block for the mobile
     reveal (size + rose stroke color). */
  .ab-hero-copy-mark { display: none; }

  /* --- TOP: page title block, sits under the fixed nav --- */
  /* Header floats above the intro so the polaroid + copy block can center
     against the full viewport, not just the space remaining below the
     title. The intro below carries enough top padding to keep this from
     colliding with the copy on shorter screens. */
  .ab-hero-header {
    position: absolute;
    top: 0; left: 0; right: 0;
    z-index: 3;
    pointer-events: none;
  }
  .ab-hero-header-inner {
    padding: clamp(120px, 15vh, 176px) clamp(40px, 5vw, 96px) clamp(20px, 3vh, 40px);
    animation: abHeaderRise 1.4s var(--ease) both 0.15s;
    pointer-events: auto;
  }
  @keyframes abHeaderRise {
    from { opacity: 0; transform: translateY(28px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .ab-header-eyebrow {
    font-family: var(--sans);
    font-size: 12px;
    letter-spacing: 0.28em;
    text-transform: uppercase;
    color: rgba(255,255,255,0.82);
    margin: 0 0 22px;
    font-weight: 400;
    display: inline-flex; align-items: center; gap: 14px;
  }
  .ab-header-eyebrow::before {
    content: ''; width: 28px; height: 1px; background: var(--rose);
    flex-shrink: 0;
  }
  .ab-header-title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(56px, 6.8vw, 104px);
    line-height: 0.95;
    letter-spacing: -0.012em;
    color: #fff;
    margin: 0 0 28px;
  }
  .ab-header-title em {
    font-style: italic;
    color: var(--rose);
    padding: 0 0.04em;
    margin: 0 -0.02em;
  }
  .ab-header-sub {
    font-family: var(--sans);
    font-size: 15px;
    line-height: 1.8;
    color: rgba(255,255,255,0.72);
    font-weight: 300;
    max-width: 440px;
    margin: 0;
  }

  /* --- Centered polaroid + copy block.
     Fills the full section so the pair visually centers against the
     viewport rather than the space below the title. Top padding
     reserves room for the overlaid .ab-hero-header so the two never
     collide on short windows. */
  .ab-hero-intro {
    position: relative;
    z-index: 2;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Asymmetric vertical padding — larger bottom than top (beyond what's
       reserved for the header) biases the centered block upward in the
       viewport. Tuned for a balance that sits slightly above true center
       without hugging the title. */
    padding:
      clamp(240px, 30vh, 330px)
      clamp(40px, 5vw, 96px)
      clamp(88px, 11vh, 150px);
  }
  .ab-hero-intro-inner {
    display: grid;
    /* Content-sized columns (capped at polaroid + copy maxima) so the
       grid tracks collapse to the block's natural width. Combined with
       justify-content: center, this makes the left-of-polaroid and
       right-of-copy offsets perfectly symmetric — no slack on the
       copy side like a 1fr column would create. */
    grid-template-columns: minmax(0, 360px) minmax(0, 640px);
    gap: clamp(56px, 5.5vw, 96px);
    /* Top-align so the polaroid's top edge tracks the headline, not the
       centerline of the (taller) copy column. */
    align-items: start;
    justify-content: center;
    max-width: var(--max);
    width: 100%;
  }
  /* Reading copy — all text rendered light so it reads against the
     darkened B&W backdrop. Headline + signoff lift to pure white,
     body copy uses a softer warm-white for comfortable reading. */
  .ab-hero-intro-copy {
    color: rgba(255,255,255,0.88);
  }
  .ab-hero-intro-copy .section-label {
    color: var(--gold);
  }
  .ab-hero-intro-copy .section-label::before {
    background: var(--gold);
  }
  .ab-hero-intro-copy .section-title {
    color: #fff;
    margin: 0 0 36px;
  }
  .ab-hero-intro-copy .section-title em {
    color: var(--rose);
  }
  .ab-hero-intro-copy .ab-body {
    color: rgba(255,255,255,0.82);
  }
  .ab-hero-intro-copy .ab-signoff-rule {
    opacity: 0.75;
  }
  .ab-hero-intro-copy .ab-signoff-name {
    color: #fff;
  }
  .ab-hero-intro-copy .ab-signoff-role {
    color: rgba(255,255,255,0.82);
    opacity: 1;
  }
  /* Larger hero polaroid — a prominent "print on the wall" against the
     dark backdrop. Sits flush to the left edge of its column so that
     left-of-polaroid mirrors right-of-copy on the far side of the
     section. Slight upward lift adds a sense of pinned asymmetry. */
  .ab-polaroid--hero {
    transform: translateY(-8px);
    max-width: 360px;
    width: 100%;
    justify-self: stretch;
  }
  /* Specificity note: body.page-about .fade-up.visible (0,3,1) wins over
     .ab-polaroid--hero:hover (0,2,0), so without this the hover grow is
     silently overridden by the fade-up reveal transform. The rules below
     re-specify the resting and hover transforms with enough weight
     (0,4,1 / 0,5,1) to win once the polaroid is .visible. */
  body.page-about .ab-polaroid--hero.fade-up.visible {
    transform: translateY(-8px);
  }
  /* On hover: grow the whole polaroid (frame + tape + caption) as a
     single unit. Shadow deepens to sell the lift. */
  body.page-about .ab-polaroid--hero.fade-up.visible:hover {
    transform: translateY(-8px) scale(1.04);
    box-shadow:
      0 1px 2px rgba(0,0,0,0.18),
      0 32px 64px rgba(0,0,0,0.48),
      0 60px 100px rgba(0,0,0,0.32);
  }

  /* --- Scroll hint — bottom-center of the hero ---
     Two stacked open chevrons under a single horizontal "highlight"
     band that sweeps top-to-bottom across the whole stack on one
     2.4s clock. The chevrons themselves always render at a soft dim
     alpha (the track layer); a second, brighter chevron layer sits
     on top of them and is revealed ONLY through a gradient band in
     an SVG mask. As the band translates downward, the portions of
     both carets that fall inside the band briefly brighten — so the
     eye reads a ribbon of light descending across the cue, not a
     pulse tracing each V. The entire cluster also drifts ~20px
     downward each cycle (fading in at the top, out at the bottom)
     so the loop never shows a hard reset. */
  .ab-hero-scroll {
    position: absolute;
    bottom: clamp(28px, 4.2vh, 46px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 4;
    display: flex;
    color: #fff;
    text-decoration: none;
    opacity: 1;
    transition: opacity 0.4s var(--ease);
  }
  .ab-hero-scroll:hover { opacity: 1; }
  .ab-hero-scroll:focus-visible {
    outline: 1px solid rgba(255,255,255,0.55);
    outline-offset: 6px;
  }
  /* Single SVG carries both carets (track + pulse) on one 28×24
     viewBox — top caret V at y=3–11, bottom caret V at y=13–21, so
     their apexes sit 10 units apart in a tight chevron-stack
     cadence. 36×31 display keeps aspect ratio (36 / 28 ≈ 31 / 24).
     Overflow-visible lets the sweep rect live slightly outside the
     viewBox without getting clipped. */
  .ab-hero-scroll-svg {
    width: 36px;
    height: 31px;
    overflow: visible;
    animation: abScrollDrift 2.4s cubic-bezier(0.33, 0, 0.2, 1) infinite;
  }
  .ab-hero-scroll-track { opacity: 0.62; }
  /* The sweep rect is the only thing driving the flash. It's a
     32-unit-tall rectangle filled with a vertical gradient — fully
     transparent at the top and bottom, opaque in a narrow band
     around the middle. The rect is authored at y=0 and animated via
     CSS transform: translateY so it travels from -32 (band parked
     above the chevrons) to +32 (parked below), passing through the
     full chevron stack mid-cycle. Transform is used instead of
     animating the `y` attribute because CSS transform has
     universally reliable support across browsers; linear timing
     gives a constant descent velocity. */
  .ab-hero-scroll-sweep {
    transform: translateY(-32px);
    animation: abScrollSweep 2.4s linear infinite;
  }
  /* Shape drift — entire chevron stack travels ~20px downward across
     the cycle, smooth and continuous, then fades out at the bottom
     and restarts invisible at the top. Long, gradual opacity ramps
     on both ends: the shape brightens as it drifts down through the
     top third of the cycle, holds at full visibility briefly through
     the middle (where the highlight band sweeps across), then softens
     away across the back half. Intermediate opacity stops approximate
     an S-curve for a smoother rise and fall than a two-point fade. */
  @keyframes abScrollDrift {
    0%   { transform: translateY(0);      opacity: 0;    }
    10%  { transform: translateY(2px);    opacity: 0.15; }  /* just emerging    */
    22%  { transform: translateY(4.3px);  opacity: 0.5;  }  /* still rising     */
    36%  { transform: translateY(7.2px);  opacity: 0.9;  }
    46%  { transform: translateY(9.2px);  opacity: 1;    }  /* fully visible    */
    56%  { transform: translateY(11.2px); opacity: 1;    }  /* brief plateau    */
    68%  { transform: translateY(13.7px); opacity: 0.85; }  /* softening begins */
    80%  { transform: translateY(16.2px); opacity: 0.5;  }
    92%  { transform: translateY(18.7px); opacity: 0.15; }  /* almost gone      */
    100% { transform: translateY(20px);   opacity: 0;    }  /* faded out        */
  }
  /* Highlight band sweep — rect translates linearly from -32 (band
     fully above the chevrons) through 0 (band centered between them)
     to +32 (band fully below). Cycle runs continuously; drift
     opacity (below) handles the fade-in / fade-out at the cycle
     edges, so no holds are needed here. */
  @keyframes abScrollSweep {
    0%   { transform: translateY(-32px); }
    100% { transform: translateY( 32px); }
  }
  /* Respect reduced-motion: freeze the sweep and fall back to a very
     gentle opacity breath on the whole hint. Parking the band at y=0
     leaves it centered between the two carets (neither glowing), so
     the static state reads as just "two dim chevrons". */
  @media (prefers-reduced-motion: reduce) {
    .ab-hero-scroll {
      animation: abScrollStaticPulse 3s ease-in-out infinite;
    }
    .ab-hero-scroll-svg {
      animation: none;
      transform: none;
    }
    .ab-hero-scroll-sweep {
      animation: none;
      transform: translateY(0);
    }
    @keyframes abScrollStaticPulse {
      0%, 100% { opacity: 0.55; }
      50%      { opacity: 0.9;  }
    }
  }

  /* (Intro copy + polaroid layout now live inside .ab-hero above.) */

  /* Polaroid — shared between Intro and Travel sections.
     <figure> wrapper + <img> child so the photo loads with a proper
     request; CSS-drawn tape at top, handwritten caption at the bottom. */
  .ab-polaroid {
    position: relative;
    background: #fcf9f2;
    padding: 14px 14px 56px;
    margin: 0;
    box-shadow:
      0 1px 2px rgba(0,0,0,0.08),
      0 22px 44px rgba(26,23,20,0.18),
      0 48px 80px rgba(26,23,20,0.12);
    transition: transform 0.8s var(--ease), box-shadow 0.8s var(--ease);
    max-width: 420px;
    justify-self: center;
    will-change: transform;
  }
  /* (.ab-polaroid--intro removed — hero polaroid now uses .ab-polaroid--hero
     above, and the travel variant has its own rules further down.) */
  .ab-polaroid-tape {
    position: absolute;
    top: -18px;
    left: 50%;
    transform: translateX(-50%) rotate(-5deg);
    width: 118px; height: 30px;
    background: rgba(224, 200, 140, 0.52);
    box-shadow: 0 2px 6px rgba(0,0,0,0.08);
    pointer-events: none;
    z-index: 2;
  }
  /* <img> variant — displays inline, fills the polaroid card, crops to
     a 4:5 portrait frame via object-fit. */
  img.ab-polaroid-img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    object-position: center;
    filter: sepia(0.04) contrast(1.02);
  }
  /* Legacy background-image variant (still used by the Travel polaroid). */
  div.ab-polaroid-img {
    width: 100%;
    aspect-ratio: 4 / 5;
    background-size: cover;
    background-position: center;
    filter: sepia(0.04) contrast(1.02);
  }
  .ab-polaroid-caption {
    /* Occupy the full 56px white strip at the bottom of the polaroid
       (the parent's `padding-bottom: 56px`) and flex-center the text
       inside it. This way single-line captions sit dead-center and
       two-line captions (which happen on narrow viewports after the
       polaroid shrinks) stay balanced within the white space instead
       of anchoring awkwardly to the bottom edge. */
    position: absolute;
    left: 14px; right: 14px; bottom: 0;
    height: 56px;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    line-height: 1.2;
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: 16px;
    color: var(--ink);
    letter-spacing: 0.005em;
  }
  .ab-polaroid-caption--script {
    font-size: 15px;
    color: rgba(26, 23, 20, 0.6);
  }

  /* Shared body typography (reused by .ab-hero-intro-copy). */
  .ab-body {
    font-size: 17px;
    line-height: 1.85;
    color: var(--charcoal);
    font-weight: 300;
    margin: 0 0 22px;
    max-width: 56ch;
  }

  /* Signoff — italic name stacked above a hairline rule, with the role
     byline tucked under. Vertical column so the three pieces read as
     one editorial block: byline, separator, credit. */
  .ab-signoff {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
    margin: 36px 0 0;
  }
  .ab-signoff-rule {
    display: block;
    width: 44px; height: 1px;
    background: var(--rose);
    opacity: 0.7;
    margin: 2px 0;
    flex-shrink: 0;
  }
  .ab-signoff-name {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: 26px;
    line-height: 1;
    color: var(--ink);
    padding: 0 0.08em;
    margin-left: -0.08em;
  }
  .ab-signoff-role {
    font-family: var(--sans);
    font-size: 11px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--charcoal);
    opacity: 0.78;
    font-weight: 400;
    padding-left: 0.08em; /* visual alignment with name's optical inset */
  }

  /* --- PHILOSOPHY — its own stacked page, warm ground, single big title ---
     No eyebrow on this one (the big italic "My Philosophy." carries the
     whole page). Warm ground gives a distinct colour-shift from the cream
     Intro above, so the sticky-stack cover reveal reads as a real chapter
     break. The 01/02/03 pillars use the homepage .about-pillars markup. */
  .ab-philo {
    background: var(--warm);
    /* Horizontal padding matches homepage .investment so Philosophy and
       the "Your Story, Artfully Preserved" section feel like one system. */
    padding: clamp(64px, 7.5vh, 104px) 88px;
    position: relative;
    display: grid;
    place-items: center;
    overflow: hidden;
  }
  .ab-philo-inner {
    width: 100%;
    max-width: 1440px;
    margin: 0 auto;
    text-align: center;
  }
  .ab-philo-title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(52px, 6.4vw, 92px);
    line-height: 1.02;
    letter-spacing: -0.012em;
    color: var(--ink);
    margin: 0 0 clamp(32px, 4.2vh, 56px);
  }
  .ab-philo-title em {
    font-style: italic;
    color: var(--rose);
    padding: 0 0.04em;
    margin: 0 -0.02em;
  }

  /* Bracketing hairlines — one long rule across the top + bottom of the
     pillars row, echoing the homepage's about-pillars treatment. */
  body.page-about .ab-philo-pillars {
    margin: 0 auto;
    padding: clamp(28px, 3.8vh, 44px) 0;
    width: 100%;
    max-width: 1360px;
    border-top: 1px solid var(--sand);
    border-bottom: 1px solid var(--sand);
    gap: clamp(24px, 2.6vw, 40px);
  }

  /* Editorial portrait at the top of each pillar — natural aspect ratio
     preserved so the three images retain their original proportions
     above the existing 01/02/03 + caption. Subtle warm tone + soft
     drop shadow so they sit on the cream ground without stealing
     focus. Hover lifts and warms the print. */
  .ab-philo-pillars .pillar {
    gap: 20px;
  }
  .ab-philo-pillars .pillar-img {
    display: block;
    width: 100%;
    height: auto;
    filter: sepia(0.08) contrast(1.02) brightness(0.98);
    box-shadow:
      0 1px 2px rgba(26,23,20,0.08),
      0 20px 44px rgba(26,23,20,0.14),
      0 44px 72px rgba(26,23,20,0.08);
    transition:
      transform 0.9s var(--ease),
      filter 0.9s var(--ease),
      box-shadow 0.9s var(--ease);
    will-change: transform;
  }
  .ab-philo-pillars .pillar:hover .pillar-img {
    filter: sepia(0) contrast(1.04) brightness(1);
    transform: translateY(-4px);
    box-shadow:
      0 2px 4px rgba(26,23,20,0.10),
      0 28px 56px rgba(26,23,20,0.20),
      0 60px 100px rgba(26,23,20,0.10);
  }

  /* --- FILM DIRECTOR — looping video background behind the original
     two-column photo+copy layout. The video fills the full section via
     absolute positioning; a light wash (just enough to lift text off the
     reel) sits on top; the photo frame on the left and copy on the right
     float above it, mirroring the pre-video layout. */
  .ab-film {
    position: relative;
    overflow: hidden;
    background: var(--ink);
    isolation: isolate;
    /* Horizontal padding matches homepage .investment ("Your Story,
       Artfully Preserved") so the About page's editorial band feels
       flush with the homepage. */
    padding: clamp(64px, 7.5vh, 104px) 88px;
    display: grid;
    place-items: center;
  }
  .ab-film-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    z-index: 0;
    pointer-events: none;
    transform: scale(1.02);
    filter: saturate(1.02) contrast(1.02);
  }
  /* Gentle wash — keeps the reel visible but gives the left-column photo
     and right-column white type enough contrast to read. Much lighter
     than the full-bleed overlay version. */
  .ab-film-wash {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
      linear-gradient(
        110deg,
        rgba(12,10,8,0.32) 0%,
        rgba(12,10,8,0.18) 50%,
        rgba(12,10,8,0.28) 100%
      );
  }
  .ab-film-inner {
    position: relative;
    z-index: 2;
    max-width: 1360px;
    width: 100%;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1.05fr 1fr;
    gap: clamp(56px, 7vw, 96px);
    /* Top-align so the print and the "Beyond Weddings" eyebrow start on
       the same horizontal. Center-alignment read as visually mismatched
       because the caption makes the photo column taller. */
    align-items: start;
  }

  /* Left column: the print. Same 4/3 framed photo as before; a touch
     more shadow so it reads against the live footage behind it. */
  .ab-film-media { position: relative; }
  .ab-film-img-frame { max-height: 72vh; }
  .ab-film-img-frame {
    position: relative;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--warm);
    box-shadow:
      0 2px 4px rgba(0,0,0,0.20),
      0 30px 70px rgba(0,0,0,0.45);
  }
  .ab-film-img {
    position: absolute; inset: 0;
    background-size: cover;
    background-position: center;
    filter: contrast(1.04);
    transform: scale(1.04);
    transition: transform 2.4s var(--ease);
    will-change: transform;
  }
  .ab-film-media:hover .ab-film-img { transform: scale(1.12); }
  .ab-film-caption {
    margin-top: 18px;
    font-family: var(--sans);
    font-size: 10.5px;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    /* Lighter than the old warm-ground version so it reads over video. */
    color: rgba(255,255,255,0.78);
    display: flex; align-items: center; gap: 12px;
  }
  .ab-film-caption::before {
    content: ''; width: 20px; height: 1px; background: var(--rose);
  }

  /* Right column: copy. Inverted for light-on-dark readability now that
     the ground is moving footage rather than cream. */
  .ab-film-copy { max-width: 520px; color: #fff; }
  .ab-film-copy .section-label {
    margin-bottom: 24px;
    color: rgba(255,255,255,0.8);
  }
  .ab-film-copy .section-label::before {
    background: rgba(255,255,255,0.55);
  }
  .ab-film-copy .section-title {
    color: #fff;
    margin-bottom: 18px;
  }
  .ab-film-copy .section-title em { color: var(--rose); }
  .ab-film-sub {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(22px, 2vw, 28px);
    color: var(--rose);
    margin-bottom: 28px;
    line-height: 1.3;
    padding: 0 0.04em;
    margin-left: -0.04em;
  }
  .ab-film-body {
    font-size: 17px;
    line-height: 1.85;
    color: rgba(255,255,255,0.90);
    font-weight: 300;
    margin-bottom: 36px;
    max-width: 48ch;
  }
  /* CTA inverted for dark ground: white stroke + white text; hover
     fills white and reveals dark ink. */
  /* Film section sits on var(--ink), so the default ink-on-ink button
     would disappear. Flip the palette: cream fill + ink text, and let
     the base's rose ::before still sweep in on hover — rose reads well
     against both cream and ink so the same animation works on either
     background. */
  .ab-film-cta {
    margin-top: 4px;
    background: var(--cream);
    color: var(--ink);
  }

  /* --- TRAVEL — polaroid on the left, header + editorial mosaic on the
     right. Mirrors the Hero's polaroid-left layout so the About page
     has a recognizable visual rhythm. Same horizontal padding as the
     philosophy + film sections so the content column aligns. */
  .ab-travel {
    position: relative;
    background: var(--ink);
    color: var(--cream);
    /* Horizontal padding matches homepage .investment so Travel sits flush
       with the 'Your Story, Artfully Preserved' band. */
    padding: clamp(80px, 10vh, 136px) 88px;
    overflow: hidden;
    isolation: isolate;
  }
  .ab-travel-inner {
    position: relative;
    z-index: 1;
    max-width: 1360px;
    margin: 0 auto;
  }
  .ab-travel-layout {
    display: grid;
    grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
    gap: clamp(56px, 5vw, 96px);
    align-items: start;
  }
  .ab-travel-right {
    min-width: 0; /* allows the grid column to contract past its content */
  }
  .ab-travel-head {
    text-align: left;
    margin: 0 0 clamp(28px, 4vh, 48px);
  }
  .ab-travel-head .section-title {
    color: #fff;
    margin: 0 0 20px;
  }
  .ab-travel-deck {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(19px, 1.7vw, 23px);
    line-height: 1.5;
    color: rgba(248,244,239,0.68);
    max-width: 58ch;
    margin: 0;
  }
  /* Mobile-only forced break inside .ab-travel-deck — hidden on desktop so
     the paragraph reads as one continuous italic line, revealed below the
     900px breakpoint so it splits at "happiness" for a tighter rhythm on
     narrow viewports. */
  .deck-mobile-break { display: none; }
  .ab-polaroid--travel {
    margin: 0;
    background: #faf5eb;
    max-width: 360px;
    width: 100%;
  }
  /* Specificity override (see hero polaroid above) — the fade-up.visible
     rule on body.page-about would otherwise flatten this transform. */
  body.page-about .ab-polaroid--travel.fade-up.visible {
    transform: none;
  }
  /* Same whole-polaroid grow as the hero variant. */
  body.page-about .ab-polaroid--travel.fade-up.visible:hover {
    transform: scale(1.04);
  }
  .ab-polaroid--travel .ab-polaroid-img { filter: sepia(0.06) contrast(1.02); }

  /* Editorial mosaic — 6-column × 4-row grid with varied spans so the
     six prints read as a composed spread rather than a uniform strip.
     Photo 1 anchors the left as a 3×3 hero frame, photo 2 is the big
     top-right landscape, photo 3 a narrow landscape beneath it, and
     photos 4-6 form a bottom strip of three equal prints. object-fit
     lets each photo crop-to-fill without regard to its native ratio. */
  .ab-travel-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    grid-auto-rows: minmax(96px, 13.5vh);
    gap: 10px;
    max-width: none;
    margin: 0;
  }
  /* Each <figure.ab-travel-cell> is a grid child that clips its image so
     the hover zoom crops cleanly inside the cell instead of bleeding
     into adjacent prints. */
  .ab-travel-cell {
    margin: 0;
    overflow: hidden;
    position: relative;
    /* Cells open the shared lightbox on click — surface that affordance
       with a zoom-in cursor, matching the wedding gallery's story-photo. */
    cursor: zoom-in;
  }
  .ab-travel-cell:focus-visible {
    outline: 2px solid var(--accent, currentColor);
    outline-offset: 2px;
  }
  .ab-travel-photo {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    margin: 0;
    filter: brightness(0.92) saturate(0.96);
    transform: scale(1);
    transition:
      transform 0.9s cubic-bezier(0.22, 0.68, 0.24, 1),
      filter 0.6s var(--ease);
    will-change: transform;
  }
  /* Hover the cell (not the image directly) so the cursor stays over
     a stable target while the image scales up beneath the clip mask. */
  .ab-travel-cell:hover .ab-travel-photo {
    transform: scale(1.08);
    filter: brightness(1.02) saturate(1.02);
  }
  /* Desktop mosaic spans — scoped to (min-width: 901px) so they never
     apply on mobile. Previously these were top-level rules + a mobile
     reset, but during the sticky-stack scroll + lazy-image load there
     was a brief layout pass on iOS Safari where the top-level spans
     resolved before the mobile reset, producing a flash of the desktop
     mosaic on phones. Confining the spans to desktop eliminates that
     window entirely — mobile only ever sees the column-count masonry. */
  @media (min-width: 901px) {
    .ab-travel-cell:nth-child(1) { grid-column: 1 / 4; grid-row: 1 / 4; }
    .ab-travel-cell:nth-child(2) { grid-column: 4 / 7; grid-row: 1 / 3; }
    .ab-travel-cell:nth-child(3) { grid-column: 4 / 7; grid-row: 3 / 4; }
    .ab-travel-cell:nth-child(4) { grid-column: 1 / 3; grid-row: 4 / 5; }
    .ab-travel-cell:nth-child(5) { grid-column: 3 / 5; grid-row: 4 / 5; }
    .ab-travel-cell:nth-child(6) { grid-column: 5 / 7; grid-row: 4 / 5; }
  }
  /* Grid-level fade + gentle per-item stagger triggered when the
     container enters the viewport (JS toggles .is-visible on the grid).
     Fade lives on the cell (not the image) so the hover-zoom transform
     on .ab-travel-photo is not overridden by the reveal transform. */
  .ab-travel-grid .ab-travel-cell {
    opacity: 0;
    transform: translateY(32px);
  }
  .ab-travel-grid.is-visible .ab-travel-cell {
    opacity: 1;
    transform: translateY(0);
    transition:
      opacity 1s var(--ease),
      transform 1s var(--ease);
  }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(1) { transition-delay: 0s; }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(2) { transition-delay: 0.08s; }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(3) { transition-delay: 0.16s; }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(4) { transition-delay: 0.24s; }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(5) { transition-delay: 0.32s; }
  .ab-travel-grid.is-visible .ab-travel-cell:nth-child(6) { transition-delay: 0.40s; }

  /* --- CALIFORNIA — full-bleed parallax, quiet centered closing card.
     Redesigned for restraint: one hairline rule, one line of text above,
     one italic hero word, one line of text below. No ornaments, no
     coordinate tag, no cascading diagonal. The photograph carries the
     mood; the type just names the place. */
  .ab-cali {
    position: relative;
    min-height: 90vh;
    display: flex;
    align-items: center;
    overflow: hidden;
    color: var(--cream);
    padding: 0;
  }
  .ab-cali-bg {
    position: absolute;
    /* Extra Y slack for the parallax Y range (~±120px). -14% over a
       100vh section gives plenty of overhang on both ends so the photo
       can drift without ever baring an edge. */
    inset: -14% 0;
    background-size: cover;
    /* JS-driven parallax via --parallax-y (see about parallax in main.js).
       Replaces the previous background-attachment:fixed, which is unreliable
       on iOS and gets disabled on mobile anyway. */
    background-position: 50% calc(50% + var(--parallax-y, 0px));
    filter: contrast(1.03);
    z-index: 0;
    /* Matches the .ab-cta-bg hover-zoom pattern so both photo-backdrop
       sections breathe the same way when the user pauses on them. */
    transform: scale(1);
    transition: transform 3.2s var(--ease);
    will-change: transform;
  }
  .ab-cali:hover .ab-cali-bg { transform: scale(1.05); }
  /* Simpler wash — slightly darker + more even so the centered type has
     a consistent ground to sit on, regardless of scroll position. */
  .ab-cali-wash {
    position: absolute; inset: 0;
    background:
      linear-gradient(180deg, rgba(12,10,8,0.42) 0%, rgba(12,10,8,0.28) 50%, rgba(12,10,8,0.48) 100%);
    z-index: 1;
  }
  .ab-cali-inner {
    position: relative;
    z-index: 2;
    max-width: 820px;          /* narrow column — the copy doesn't need 1560px */
    margin: 0 auto;
    width: 100%;
    /* Horizontal padding matches homepage .investment so the section
       still snaps to the same rhythm as the rest of the About page. */
    padding: clamp(80px, 10vh, 140px) 88px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  /* California grizzly silhouette PNG — sits directly above the hairline
     rule as a quiet heraldic anchor. Low opacity lets the silhouette
     recede into the photograph as a subtle heraldic mark, no shadow. */
  .ab-cali-bear {
    display: block;
    width: 63px;
    height: auto;
    margin: 0 0 16px;
    opacity: 0.4;
  }
  /* Specificity override — `.fade-up.visible { opacity: 1 }` (0,2,0)
     would otherwise flatten the bear to opaque after reveal. This
     selector (0,4,1) pins the final state at 0.4 so the bear fades
     in from 0 to 0.4 (not 0 to 1), and starts the heartbeat pulse. */
  body.page-about .ab-cali-bear.fade-up.visible {
    opacity: 0.4;
    /* Delay 1.2s lets the reveal fade complete before the pulse takes
       over — otherwise the two opacity animations fight for the first
       frame of the heartbeat. */
    animation: bear-heartbeat 2.4s ease-in-out 1.2s infinite;
  }
  /* Heartbeat — two quick opacity pulses (lub-dub) followed by a long
     rest. 0.4 baseline (quiet resting state), peaks at 0.85 so the
     beats read clearly against the dim base. */
  @keyframes bear-heartbeat {
    0%   { opacity: 0.4; }
    14%  { opacity: 0.6; }
    28%  { opacity: 0.4; }
    42%  { opacity: 0.6; }
    56%  { opacity: 0.4; }
    100% { opacity: 0.4; }
  }
  /* Respect users who prefer reduced motion — pin the bear at its
     static 0.4 opacity instead of pulsing. */
  @media (prefers-reduced-motion: reduce) {
    body.page-about .ab-cali-bear.fade-up.visible {
      animation: none;
    }
  }
  /* Hairline rule — single thin anchor at the top of the composition,
     replacing the old rose ornament. Desaturated white, so it fits the
     non-rose palette. */
  .ab-cali-rule {
    display: block;
    width: 56px;
    height: 1px;
    background: rgba(255,255,255,0.55);
    margin: 0 0 clamp(28px, 3.4vh, 44px);
  }

  /* Above-line narrative — upright Cormorant, quiet. */
  .ab-cali-above {
    margin: 0;
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(20px, 2.1vw, 28px);
    line-height: 1.3;
    letter-spacing: 0.002em;
    color: rgba(255,255,255,0.88);
  }

  /* Hero word — Playfair italic in the site's rose accent color, the
     same accent used for "new places" / "cultures" in the Travel section
     above. Keeps the typographic emphasis scheme consistent across the
     whole page. */
  .ab-cali-hero {
    margin: clamp(10px, 1.4vh, 18px) 0 clamp(18px, 2.4vh, 28px);
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(64px, 10vw, 140px);
    line-height: 0.96;
    letter-spacing: -0.02em;
    color: var(--rose);
    text-shadow: 0 8px 40px rgba(0,0,0,0.34);
  }
  .ab-cali-hero em {
    font-style: italic;
    color: var(--rose);
  }

  /* Below-line — matches .ab-cali-above in weight and family so the
     section reads as one typographic voice, not three. */
  .ab-cali-below {
    margin: 0;
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(17px, 1.55vw, 21px);
    line-height: 1.5;
    letter-spacing: 0.004em;
    color: rgba(255,255,255,0.82);
    max-width: 40ch;
  }

  /* Gentle staggered reveal — bear, rule, above, hero, below. Single
     line each, so the cascade is quick. */
  .ab-cali .ab-cali-bear  { transition-delay: 0s; }
  .ab-cali .ab-cali-rule  { transition-delay: 0.08s; }
  .ab-cali .ab-cali-above { transition-delay: 0.2s; }
  .ab-cali .ab-cali-hero  { transition-delay: 0.36s; }
  .ab-cali .ab-cali-below { transition-delay: 0.6s; }

  /* --- CTA — "Let's Connect" closing section.
     Redesigned April 2026: the pale-pink (#f7cfc4) italic tint and the
     rose divider rule are gone. The italic accent on "Connect." now
     uses var(--rose), the same accent used for "new places" / "cultures"
     (Travel) and "California" (closing card), so one italic+rose system
     runs the whole page. A small uppercase meta label replaces the rose
     divider, and the primary action is a solid cream-filled button so
     clicking is the obvious next step. */
  .ab-cta {
    background: var(--cream);
    padding: clamp(110px, 13vw, 168px) 48px;
    text-align: center;
    position: relative;
    overflow: hidden;
    isolation: isolate;
  }
  .ab-cta-inner {
    max-width: 760px;
    margin: 0 auto;
    position: relative;
    z-index: 2;
  }

  /* Eyebrow meta — small uppercase sans label, same type family as the
     section eyebrows used elsewhere on the site (footer "RedSphere Studios
     · Est. 2009"). Sits where the old rose divider used to. */
  .ab-cta-eyebrow {
    margin: 0 0 clamp(22px, 2.8vh, 30px);
    font-family: var(--sans);
    font-weight: 400;
    font-size: 11.5px;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: var(--muted);
  }

  .ab-cta-title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(54px, 7.5vw, 104px);
    line-height: 1;
    letter-spacing: -0.018em;
    color: var(--ink);
    margin: 0 0 clamp(28px, 3vh, 38px);
  }
  /* "Connect." italic accent — Playfair italic in the site's rose color,
     matching the italic accents in the Travel title ("new places" /
     "cultures") and the California word, so one italic+rose system runs
     the whole page. */
  .ab-cta-title em {
    font-family: 'Playfair Display', var(--serif);
    font-style: italic;
    font-weight: 400;
    color: var(--rose);
    padding: 0 0.04em;
  }

  .ab-cta-body {
    font-family: var(--serif);
    font-weight: 300;
    font-style: normal;          /* was italic; upright reads calmer + less ornamental */
    font-size: clamp(18px, 1.7vw, 22px);
    line-height: 1.6;
    color: var(--charcoal);
    margin: 0 auto clamp(40px, 5vh, 52px);
    max-width: 54ch;
  }

  /* Solid primary button — matches the homepage contact form's
     .btn-submit animation exactly (rose sweep from the left, arrow
     nudge, text-color cross-fade). The only difference is the starting
     color: this button begins with a cream fill + ink label so it
     reads as a lighter hero CTA against the beach photograph, where
     the homepage form button starts on ink. No border, no lift, no
     shadow — just solid color in both states. */
  /* Inquire About Your Date — same primary arrow-pill as .btn-view-all,
     standalone class because the CTA section sets its own color palette
     (cream fill, ink label). Type metrics must match the rest of the
     family: 17/34 padding, 450 weight, 13px arrow. */
  .ab-cta-btn-solid {
    position: relative; overflow: hidden;
    display: inline-flex; align-items: center; gap: 14px;
    padding: 17px 34px;
    background: var(--cream); color: var(--ink);
    font-family: var(--sans); font-size: 12px; font-weight: 425;
    letter-spacing: 0.22em; text-transform: uppercase;
    text-decoration: none;
    border: 0;
    transition: color 0.4s var(--ease);
  }
  .ab-cta-btn-solid::before {
    content: ''; position: absolute; inset: 0;
    background: var(--rose);
    transform: translateX(-101%);
    transition: transform 0.5s var(--ease);
  }
  .ab-cta-btn-solid > span { position: relative; z-index: 1; }
  .ab-cta-btn-solid .ab-cta-btn-arrow {
    display: inline-block;
    font-size: 12.5px;
    line-height: 1;
    transition: transform 0.4s var(--ease);
  }
  .ab-cta-btn-solid:hover { color: var(--cream); }
  .ab-cta-btn-solid:hover::before { transform: translateX(0); }
  .ab-cta-btn-solid:hover .ab-cta-btn-arrow { transform: translateX(6px); }

  /* --- CTA beach variant — full-bleed sunlit photograph behind the
     invitation. Flips type to light-on-dark; the solid cream button
     is inherited from the base and pops cleanly against the dark wash. */
  .ab-cta--beach {
    background: var(--ink);
    min-height: 82vh;
    display: grid;
    place-items: center;
  }
  .ab-cta-bg {
    position: absolute;
    /* Negative Y inset gives the parallax Y range (~±110px) room to
       shift without the photo's edge ever coming into view. */
    inset: -140px 0;
    background-size: cover;
    /* JS-driven parallax via --parallax-y. 38% vertical anchor is kept
       so the beach horizon still sits where the composition wants it;
       the parallax offset modulates that anchor on scroll. */
    background-position: 50% calc(38% + var(--parallax-y, 0px));
    filter: contrast(1.04) saturate(1.02);
    z-index: 0;
    transform: scale(1.04);
    transition: transform 3.2s var(--ease);
    will-change: transform;
  }
  .ab-cta--beach:hover .ab-cta-bg { transform: scale(1.08); }
  .ab-cta-wash {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    /* Neutral wash — no warm-rose stop. Center bumped darker (was 34%)
       so the italic rose "Connect." has a stable ground to sit on
       regardless of which part of the photograph scrolls behind it. */
    background:
      linear-gradient(
        120deg,
        rgba(14, 10, 8, 0.62) 0%,
        rgba(14, 10, 8, 0.46) 55%,
        rgba(14, 10, 8, 0.48) 100%
      ),
      linear-gradient(to bottom, rgba(14,10,8,0) 55%, rgba(14,10,8,0.38) 100%);
  }
  .ab-cta--beach .ab-cta-eyebrow {
    color: rgba(255, 255, 255, 0.82);
  }
  .ab-cta--beach .ab-cta-title {
    color: #fff;
    text-shadow: 0 2px 30px rgba(0,0,0,0.28);
  }
  /* "Connect." on the beach photograph — the base var(--rose) (#C4786A)
     is a warm terracotta that gets muddied by the sand + sunlight in the
     background. Lift the accent to a brighter, more luminous warm-rose
     (#ECA495) for this variant only, so it pops against the wash while
     staying within the same hue family as the rest of the page's italic
     accents. Add a stronger shadow for extra separation from sand tones. */
  .ab-cta--beach .ab-cta-title em {
    color: #ECA495;
    text-shadow:
      0 2px 20px rgba(0, 0, 0, 0.45),
      0 0 28px rgba(10, 8, 6, 0.3);
  }
  .ab-cta--beach .ab-cta-body {
    color: rgba(255, 255, 255, 0.92);
  }
  /* Beach variant: the button inherits the base animation + solid-color
     treatment as-is. No variant overrides needed. */

  @media (prefers-reduced-motion: reduce) {
    .story-photo-frame img,
    .lightbox, .lightbox-stage, .lightbox-frame, .lightbox-img,
    .lightbox-close, .lightbox-nav,
    .ab-hero-bg, .ab-polaroid, .ab-travel-photo, .ab-cta-bg,
    .ab-film-video {
      transition: none !important;
      animation: none !important;
    }
  }

  /* ============ RESPONSIVE ============ */
  @media (max-width: 1100px) {
    .about-grid { gap: 56px; grid-template-columns: 360px 1fr; }
    .about-img-wrap { max-width: 360px; }
    .investment-inner { grid-template-columns: 1fr; gap: 48px; }
    /* About page: tighten hero/philo/film padding + gap; travel masonry
       drops to 2 columns. Film collapses to single column. */
    .ab-hero-header-inner { padding: clamp(104px, 13vh, 144px) 40px clamp(20px, 3vh, 36px); }
    .ab-hero-intro {
      padding:
        clamp(215px, 27vh, 300px)
        40px
        clamp(72px, 9vh, 120px);
    }
    .ab-hero-intro-inner { grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); gap: 48px; }
    .ab-polaroid--hero { max-width: 320px; }
    /* Keep 88px horizontal padding at tablet — homepage .investment
       doesn't override horizontal padding at this breakpoint either. */
    .ab-philo { padding: clamp(64px, 8vh, 96px) 88px; }
    /* Film: collapse the 2-col photo+copy to a stack on tablet. Video still
       fills the section behind both. */
    .ab-film-inner { grid-template-columns: 1fr; gap: 48px; max-width: 720px; }
    .ab-film-media { max-width: 460px; margin: 0 auto; }
    .ab-film-img-frame { max-height: 52vh; }
    /* Travel: collapse the polaroid-left / content-right split to a stack so
       the mosaic has room to breathe on tablet. The 6-col grid stays, but
       we shrink its row height so it doesn't dominate the fold. */
    .ab-travel-layout {
      grid-template-columns: 1fr;
      gap: 48px;
    }
    .ab-polaroid--travel {
      max-width: 320px;
      margin: 0 auto;
    }
    .ab-travel-head { text-align: center; max-width: 640px; margin: 0 auto clamp(24px, 3vh, 40px); }
    .ab-travel-grid { grid-auto-rows: minmax(80px, 11vh); }
    body.page-about .ab-philo-pillars { grid-template-columns: 1fr 1fr 1fr; gap: 28px; }
    /* Experience: drop the timeline and interleave images/bodies in a 2-col grid
       (2 imgs → 2 bodies → 2 imgs → 2 bodies). The 4-dot rail only shows on
       wide screens where all four columns can breathe. */
    .exp-timeline { display: none; }
    .exp-rail {
      display: grid;
      grid-template-columns: 1fr 1fr;
      column-gap: 32px;
      row-gap: 36px;
    }
    .exp-img-row,
    .exp-body-row { display: contents; }
    .exp-img-row > .exp-img:nth-child(1) { grid-column: 1; grid-row: 1; }
    .exp-img-row > .exp-img:nth-child(2) { grid-column: 2; grid-row: 1; }
    .exp-body-row > .exp-body:nth-child(1) { grid-column: 1; grid-row: 2; }
    .exp-body-row > .exp-body:nth-child(2) { grid-column: 2; grid-row: 2; }
    .exp-img-row > .exp-img:nth-child(3) { grid-column: 1; grid-row: 3; }
    .exp-img-row > .exp-img:nth-child(4) { grid-column: 2; grid-row: 3; }
    .exp-body-row > .exp-body:nth-child(3) { grid-column: 1; grid-row: 4; }
    .exp-body-row > .exp-body:nth-child(4) { grid-column: 2; grid-row: 4; }
    .connect-form { grid-template-columns: 1fr; }
  }
  @media (max-width: 900px) {
    nav { padding: 0 24px; }
    .nav-links, .nav-cta { display: none; }
    .hamburger { display: block; }
    section, .gallery-section, .connect, .about, .investment, .experience, .testimonials { padding: 72px 24px; }
    /* Mobile hero layout — Safari's dynamic URL bar eats the bottom
       of 100vh, so we pin content to a safe band: top padding clears
       the fixed nav (--nav-h + breathing room), bottom padding clears
       both the scroll indicator AND the URL bar via safe-area insets.
       Fonts tightened from desktop (eyebrow/title/subtitle/stats) so
       the stack fits within the visible viewport on an iPhone without
       changing the layout itself — same column, same order. */
    /* Hero keeps its desktop 100vh on mobile so the slideshow fills
       the ENTIRE screen on first load — dvh shrinks the hero while
       Safari's URL bar is visible, which lets the next section
       (filmstrip) show through. 100vh extends the hero behind
       Safari's chrome so the photo takes over the viewport; the
       dots + scroll indicator then use explicit bottom offsets
       large enough to clear Safari's toolbar. */
    /* (intentionally not overriding .hero height here) */
    .hero-content {
      justify-content: flex-end;
      /* Bottom padding uses the (100vh - 100dvh) trick to auto-
         adjust for Safari's URL bar: when the bar is visible,
         (100vh - 100dvh) ≈ URL-bar height, so content sits a
         consistent amount above the VISIBLE bottom. When the bar
         hides on scroll, the calc collapses to just that amount.

         Bottom offset trimmed 110px → 82px (−28px) so the whole
         content block — eyebrow, title, subtitle, stats — drops
         28px closer to the viewport edge, matching the About hero's
         content shift. Both `.hero-dots` (bottom: ~24px + dvh) and
         `.ab-hero-scroll` (bottom: ~22px + dvh) are absolutely
         pinned to the viewport bottom, so they don't move — the
         visible effect is that the title sits lower with the dots
         and scroll chevron still tucked under it. */
      padding:
        calc(var(--nav-h) + 12px) 28px
        calc(100vh - 100dvh + 82px) 28px;
    }
    .hero-eyebrow { font-size: 11px; letter-spacing: 0.24em; margin-bottom: 18px; }
    .hero-eyebrow .eyebrow-main { padding-bottom: 8px; margin-bottom: 8px; }
    /* Title goes much bigger on mobile — first + second passes landed
       too small. Push the clamp hard so "Honest. Heartfelt. Cinematic."
       reads as the dominant element on the screen. line-height pulled
       in to 0.92 so three big lines still fit within the content band. */
    .hero-title { font-size: clamp(68px, 19vw, 104px); margin-bottom: 18px; line-height: 0.92; }
    .hero-subtitle { font-size: 14px; line-height: 1.55; margin-bottom: 20px; max-width: 100%; }
    .hero-stats { gap: 20px; flex-wrap: wrap; padding-top: 14px; }
    .stat-num { font-size: 26px; }
    .stat-label { font-size: 10px; margin-top: 4px; }
    /* Dots + scroll indicator pinned just above Safari's URL bar.
       The hero is 100vh (fills the full screen), but 100vh extends
       behind Safari's URL bar on iOS. Using (100vh - 100dvh) as the
       offset auto-matches the URL-bar height: when the bar is
       visible, content sits ~24-26px above the VISIBLE bottom;
       when the bar hides on scroll, calc collapses to just 24-26px
       and they sit the same distance above the viewport edge. */
    /* Hide the dot indicator on mobile — swipe is the primary navigation
       gesture on touch devices and the dots become 6px tap targets, too
       small to be useful. The `heroGestures` pointer handler in main.js
       already wires horizontal swipe → next/prev, so removing the dots
       leaves the slideshow fully navigable. */
    .hero-dots { display: none; }
    /* Mobile focal points for hero slideshow. Landscape photos cropped
       into a portrait viewport default to a center-center crop, which
       on many of the slides pushes faces off-frame or to an awkward
       edge. Each .hero-slide can set --focal-x-m / --focal-y-m inline
       to bias the crop toward the subject's faces on mobile only;
       desktop continues to use --focal-x / --focal-y (defaulting to
       center), so this override has zero effect above 900px. */
    .hero-slide { background-position: var(--focal-x-m, var(--focal-x, 50%)) var(--focal-y-m, var(--focal-y, 50%)); }
    /* Footer scrim — desktop uses 0 → 0.18 → 0.62. On mobile the
       footer text (locations, tagline, CTA) sits closer to busy
       parts of the bridge photo, so we strengthen the bottom half
       of the wash to keep the copy legible without darkening the
       top edge where the image breathes into the page above. */
    .footer-bg::after { background: linear-gradient(180deg, rgba(20,16,12,0.12) 0%, rgba(20,16,12,0.54) 60%, rgba(20,16,12,0.96) 100%); }
    /* About page full-bleed backdrops on mobile. Landscape photos
       crop horizontally on narrow viewports, so the default center
       crop loses the subject. The CTA photograph is a beach shot
       where the bride + groom run to the right (~70% x / ~55% y);
       biasing the crop there keeps them in frame. The California
       photograph has its compositional weight on the right edge
       (the Sausalito waterfront + structures), so anchoring the
       crop to the far right holds the composition on mobile while
       the open water on the left falls off screen. Both keep the
       parallax offset pattern so scroll-driven drift still works. */
    .ab-cta-bg { background-position: 72% calc(55% + var(--parallax-y, 0px)); }
    .ab-cali-bg { background-position: 100% calc(50% + var(--parallax-y, 0px)); }
    .ab-hero-scroll { bottom: calc(100vh - 100dvh + 22px); }
    /* Homepage investment packages — on mobile the middle column
       (name + italic description) wraps to multiple lines, so the
       desktop `align-items: center` leaves the roman numeral and
       the price floating in the middle of the row. Switch to
       baseline alignment so the "i."/"ii."/"iii.", the category
       name, and the price all share the same first-line baseline,
       reading as a clean horizontal sweep across every row.
       Price font size also drops from 30px → 20px so "From $5,900"
       doesn't dominate the card. */
    .pkg {
      align-items: baseline;
      grid-template-columns: 40px 1fr auto;
      gap: 16px;
      padding: 22px 0;
    }
    .pkg-idx { font-size: 18px; }
    .pkg-name { font-size: 12px; letter-spacing: 0.18em; }
    .pkg-desc { font-size: 14.5px; margin-top: 6px; line-height: 1.45; }
    .pkg-price {
      font-size: 20px;
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }
    /* Desktop uses only .hero-slide::after (0 → 0.2 @ 50% → 0.4 @ 100%).
       On mobile we keep that base intact and ADD a second gradient that
       is fully transparent on the top half and ramps down gradually to
       a darker bottom, purely to lift the subtitle + stats off bright
       slide content. Top of the hero reads identical to desktop; only
       the lower band builds additional darkening.
       z-index 2 sits above the slide + its ::after tint but below
       .ab-hero-scroll (z:4), .hero-content (z:10), and .hero-dots
       (z:20) so the chevron + dots stay crisp above the wash. */
    .hero::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: 2;
      pointer-events: none;
      /* Gradual wash — top 30% stays fully clean (photo reads
         exactly like desktop), then ramps up through the title
         and subtitle band into a firmer bottom. Stops at 0.62
         so the image never goes "totally dark" — still reads
         as a photograph, just weighted enough for white type
         to hold the eye against bright slide content. */
      background: linear-gradient(
        180deg,
        rgba(20,16,12,0)    0%,
        rgba(20,16,12,0)    30%,
        rgba(20,16,12,0.22) 55%,
        rgba(20,16,12,0.42) 75%,
        rgba(20,16,12,0.62) 100%
      );
    }
    .about-grid { grid-template-columns: 1fr; gap: 64px; }
    .about-img-wrap { max-width: 350px; margin: 0 auto; }
    /* Flip back to the bottom-left corner on mobile. At desktop the tag
       anchors left (see base rule), then got right-pinned for the 900px+
       tablet layout; this override returns it to the left edge on phones
       so it hangs off the side of the centered profile pic consistent
       with the desktop anchor direction. */
    .about-tag { left: -16px; right: auto; bottom: -20px; }
    .about-pillars { grid-template-columns: 1fr; gap: 28px; }
    .gallery-grid { grid-template-columns: 1fr; gap: 8px; }
    .gallery-head { flex-direction: column; align-items: flex-start; }
    /* Gallery page: collapse to single column on mobile, matching the
       homepage's .gallery-section padding (72px 24px) + .gallery-grid gap (8px). */
    .story-grid { grid-template-columns: 1fr; gap: 8px; }
    .story-photo-frame { aspect-ratio: 4 / 3; }
    .story-grid-section { padding: 72px 24px; }
    .story-footer { flex-direction: column; align-items: flex-start; gap: 24px; }
    /* Wedding Stories index: single column on mobile, tighter padding,
       and a smaller vertical gap between cards so the stack doesn't
       spread too thin. */
    .ws-grid { grid-template-columns: 1fr; gap: 40px; }
    .ws-grid-section { padding: 40px 24px 72px; }
    .ws-card-body { padding-top: 18px; }
    /* Single-column: bring back the odd/even stagger so the grid reveals
       in a soft cascade instead of all-at-once. Each tile is its own row
       here, so there are no row partners to desync. */
    .story-photo:nth-child(even) { transition-delay: 0.08s; }
    /* Mobile: hide the prev/next buttons entirely. The existing swipe
       gesture (horizontal pointer drag on the lightbox backdrop) handles
       navigation on touch devices, which is more idiomatic on a phone
       than tapping small corner targets. The close X stays so there's
       still an obvious way to exit. */
    .lightbox-nav { display: none; }
    /* Mobile: kill the 6-second scale zoom on the image itself. On a
       small screen the slow Ken-Burns drift reads as lag — especially
       when the viewer is swiping through a set quickly — so we lock
       the image to scale(1) with no transition. The crossfade between
       frames stays intact because that lives on `.lightbox-frame`'s
       opacity transition, not on `.lightbox-img`'s transform. */
    .lightbox-img,
    .lightbox-frame.active .lightbox-img {
      transform: none;
      transition: none;
    }
    /* Crossfade speed is split by intent on mobile:
         - Auto-advance (5s timer) keeps the 1.4s dissolve — same slow,
           cinematic feel as desktop. This is the default.
         - Manual swipe/navigation triggers `.fast-swap` on the lightbox,
           cutting the fade to 0.35s so the image keeps up with the
           user's finger.
       Class is added in `next()`/`prev()` and cleared before each
       auto-advance tick so a stale fast-swap doesn't leak into the
       idle slideshow. */
    .lightbox.fast-swap .lightbox-frame { transition: opacity 0.12s ease; }
    /* Match the close button at 42×42 on mobile so all three lightbox
       controls form a consistent ghost-button set. 42px container +
       8px padding = 26px content box, identical to the SVG size. */
    .lightbox-close { top: 16px; right: 16px; width: 42px; height: 42px; }
    .lightbox-stage { width: 96vw; height: 82vh; }
    .testi-card { flex: 0 0 calc(100vw - 56px); }
    .testi-card-body { padding: 28px 24px; }
    .testi-couple { flex-direction: column; align-items: flex-start; gap: 14px; }
    /* Experience: collapse from the 2-col interleaved layout to a single
       column on mobile. Each image reads as a full-width photo followed
       directly by its matching body copy — the 2-col version was already
       pretty tight at 900px and the body copy's two short paragraphs
       don't need a half-screen column. Explicit grid-column/row values
       reorder the DOM-order rows (imgs first, bodies second) into the
       desired img1 → body1 → img2 → body2 → img3 → body3 → img4 → body4
       stack. */
    .exp-rail {
      grid-template-columns: 1fr;
      column-gap: 0;
      row-gap: 28px;
    }
    /* On mobile, render experience photos at their native 3:2 aspect.
       The desktop 4:3 cell crops 8% off each side via background-size:
       cover — fine on a wide monitor, but on a narrow phone that crop
       can land on edge content (curtains, walls) and hide the actual
       subject. Matching the cell aspect to the image aspect means
       cover is effectively no-crop and the full frame is visible. */
    .exp-img { aspect-ratio: 3 / 2; }
    /* Tighten the gap between the "Your Experience" heading block and
       the first image (desktop 72px → 52px). The heading on mobile
       stacks into a tighter vertical rhythm that makes the 72px drop
       feel cavernous above the photo, but 36px felt too cramped — 52px
       preserves some breathing room. */
    .exp-head { margin-bottom: 52px; }
    /* Tighten the gap between step 04's copy and the divider +
       testimonial pull-quote below it (desktop 88px → 48px). Less
       negative space between the stepped flow and the closing quote
       keeps the eye moving down the page on a narrow screen. */
    .exp-pull { margin-top: 48px; padding-top: 32px; }
    .exp-img-row > .exp-img:nth-child(1)   { grid-column: 1; grid-row: 1; }
    .exp-body-row > .exp-body:nth-child(1) { grid-column: 1; grid-row: 2; }
    .exp-img-row > .exp-img:nth-child(2)   { grid-column: 1; grid-row: 3; }
    .exp-body-row > .exp-body:nth-child(2) { grid-column: 1; grid-row: 4; }
    .exp-img-row > .exp-img:nth-child(3)   { grid-column: 1; grid-row: 5; }
    .exp-body-row > .exp-body:nth-child(3) { grid-column: 1; grid-row: 6; }
    .exp-img-row > .exp-img:nth-child(4)   { grid-column: 1; grid-row: 7; }
    .exp-body-row > .exp-body:nth-child(4) { grid-column: 1; grid-row: 8; }
    .fs-cell { width: 340px; height: 233px; }
    .filmstrip { padding: 16px 0; }
    .connect::before { display: none; }
    .footer-inner { padding: 80px 24px 32px; }
    .footer-meta { flex-direction: column; align-items: flex-start; gap: 20px; }
    .footer-tagline { text-align: left; margin-top: 32px; }
    /* About page mobile: sticky-stack stays on (height:100vh + overflow
       hidden). Single column everywhere + tighter padding. The hero keeps
       its top-anchored layout because the fixed nav lives over its top
       edge and the header copy has baked-in clearance — overriding the
       stack-item's flex-center here prevents the title from sliding up
       behind the nav. Every other section uses the inherited flex
       vertical-center. */
    body.page-about .ab-hero {
      justify-content: flex-start;
    }
    /* On mobile, return the header to normal document flow — single-column
       stack reads top-to-bottom and the absolute overlay would leave a
       gaping void above the intro otherwise. */
    .ab-hero-header {
      position: relative;
    }
    /* Hero header on mobile — tighten padding and shrink the eyebrow
       margin-bottom so the title block doesn't eat 200px of vertical
       space on a phone. Header + polaroid + copy all have to share
       one 100vh sticky page.

       Top padding bumped 68px → 96px (+28px) to open breathing room
       between the fixed nav bar and the "Meet Your Photographer"
       eyebrow. Because `.ab-hero` is `justify-content: flex-start` on
       mobile (see above), every block below — eyebrow, title,
       polaroid, signoff — shifts down by the same 28px. The scroll
       indicator is absolutely positioned against the viewport bottom
       so it stays fixed; the net effect is a 28px shorter gap between
       the "FOUNDER & PHOTOGRAPHER, REDSPHERE STUDIOS" role line and
       the scroll chevron. */
    .ab-hero-header-inner {
      padding: 96px 24px 4px;
    }
    /* Eyebrow stays on mobile — page 1 is now just title + polaroid +
       signoff (body copy moved to page 2) so there's room for the
       "Meet Your Photographer" framing. Shrunk tracking/size so it
       doesn't overpower the composition. */
    .ab-header-eyebrow {
      font-size: 10.5px;
      letter-spacing: 0.24em;
      margin-bottom: 14px;
    }
    /* Two-line left-aligned title on mobile — the hero is now split
       across two sticky pages (page 1: title + polaroid + signoff;
       page 2: section-title + body copy) so the title block has
       enough vertical room to breathe at a comfortable size without
       the creative compression tricks. */
    .ab-header-title {
      font-size: clamp(44px, 13vw, 58px);
      line-height: 0.96;
      margin: 0 0 12px;
    }
    .ab-hero-intro {
      /* Override the desktop `align-items: center` — on mobile the
         intro block (polaroid + signoff) needs to sit at the top of
         its flex container so it hugs right under the "About Alex."
         title instead of floating mid-column. Any extra vertical
         slack collects at the bottom, above the scroll chevron. */
      align-items: flex-start;
      padding: 8px 24px 24px;
    }
    .ab-hero-intro-inner {
      grid-template-columns: 1fr;
      gap: 20px;
      text-align: left;
    }
    .ab-polaroid--hero {
      /* 195px — middle ground between the overly-dainty 150px and the
         overbearing 240px that pushed the signoff into the scroll
         chevron. At this size the polaroid still feels like the focal
         element of page 1 while leaving a clean vertical gap between
         the role line and the scroll indicator at the bottom. */
      max-width: 195px;
      margin: 4px auto 0;
    }
    /* Mobile split: page 1 of the hero now carries ONLY the polaroid
       + the Alex-Stark signoff. The section-title + 2 body paragraphs
       that live inside .ab-hero-intro-copy on desktop are hidden on
       mobile and re-rendered on the new #ab-hero-copy sticky page
       below. */
    .ab-hero-intro-copy .section-title,
    .ab-hero-intro-copy .ab-body { display: none; }
    .ab-hero-intro-copy .ab-signoff {
      margin-top: 14px;
      gap: 8px;
    }
    .ab-hero-intro-copy .ab-signoff-name { font-size: 20px; }
    .ab-hero-intro-copy .ab-signoff-role { font-size: 10.5px; letter-spacing: 0.22em; }

    /* Page 2 of the hero on mobile — reveal the #ab-hero-copy section
       and lay out its contents centered within the 100vh sticky frame.
       Swapped the duplicated hero photo for a solid ink background with
       a subtle top-to-bottom gradient for depth; hiding the .ab-hero-bg
       and .ab-hero-wash children neutralizes the markup we left in the
       HTML for structural parity with page 1. The dark tone keeps the
       page reading as a continuation of the About chapter without
       duplicating the photograph. */
    .ab-hero-copy {
      display: flex;
      flex-direction: column;
      justify-content: center;
      position: relative;
      overflow: hidden;
      background:
        radial-gradient(
          ellipse at 50% 30%,
          #2a221d 0%,
          #1a1714 70%,
          #120f0c 100%
        );
    }
    .ab-hero-copy .ab-hero-bg,
    .ab-hero-copy .ab-hero-wash { display: none; }
    .ab-hero-copy-inner {
      position: relative;
      z-index: 2;
      padding: 0 28px;
      max-width: 540px;
      width: 100%;
      margin: 0 auto;
      color: #fff;
    }
    /* Section-title on the second hero page — larger than the desktop
       intro-copy title since it no longer competes with a polaroid
       alongside it. Styled on-the-dark so the rose italic accent still
       reads as a highlight. */
    .ab-hero-copy-inner .section-title {
      color: #fff;
      font-size: clamp(30px, 8.4vw, 40px);
      line-height: 1.12;
      margin: 0 0 18px;
    }
    .ab-hero-copy-inner .ab-body {
      color: rgba(255,255,255,0.86);
      font-size: 14.5px;
      line-height: 1.7;
      margin: 0 0 14px;
    }
    .ab-hero-copy-inner .ab-body:last-child { margin-bottom: 0; }
    /* RedSphere logo mark — a small heraldic accent that sits above
       the section-title. Stays in the normal flex flow so the whole
       group (mark + gap + copy block) remains vertically centered in
       the 100vh sticky page. The generous 72px bottom margin opens up
       the breathing room between the mark and the title; because the
       parent is `justify-content: center`, increasing that gap pushes
       the mark further up the page while the title drifts only
       slightly lower — the net effect the user asked for. */
    .ab-hero-copy-inner { text-align: center; }
    .ab-hero-copy-mark {
      display: block;
      align-self: center;
      width: 22px;
      height: auto;
      margin: 0 0 72px;
    }
    .ab-philo, .ab-film, .ab-travel, .ab-cta { padding: 44px 24px; }
    /* Travel polaroid on mobile: trimmed to 140px to make room for a
       properly-weighted deck paragraph below. Still reads as a proper
       Polaroid at this size — just more restrained — and buys enough
       vertical room to bump the deck to real body-copy size. */
    .ab-polaroid--travel {
      display: block;
      max-width: 140px;
      margin: 0 auto;
    }
    /* Film: photograph should feel like a cinematic print, not a thumbnail.
       Drop the tablet max-width/max-height caps and shift to a 4/5
       portrait aspect so Alex's photo dominates the fold above the copy. */
    /* Mobile film: stack BEYOND WEDDINGS → image → title → sub → body → CTA,
       all centered. We flatten .ab-film-copy with display: contents so its
       children become direct flex children of .ab-film-inner, letting us
       use `order` to pull the section-label above the image. */
    .ab-film-inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      /* Tightened from 20px — the CTA was getting pushed out of the
         100vh frame. Shrinking the inter-element gap lets the full
         stack (label → image → caption → title → sub → body → CTA)
         sit inside one sticky page on a phone. */
      gap: 12px;
      max-width: none;
      text-align: center;
    }
    .ab-film-copy {
      display: contents;
    }
    /* Pull the eyebrow to the very top of the mobile slide. */
    .ab-film-copy .section-label {
      order: -1;
      justify-content: center;
      margin-bottom: 0;
      width: 100%;
    }
    .ab-film-media {
      order: 0;
      max-width: none;
      width: 100%;
    }
    .ab-film-img-frame {
      /* Inherit the desktop 4:3 aspect so Alex's photo reads at its
         intended proportion. */
      max-height: none;
    }
    .ab-film-caption { margin-top: 8px; justify-content: center; }
    .ab-film-copy .section-title {
      font-size: clamp(32px, 8vw, 44px);
      margin-top: 0;
      margin-bottom: 8px;
    }
    .ab-film-sub { margin-bottom: 8px; }
    .ab-film-body {
      font-size: 13.5px;
      line-height: 1.5;
      /* Cancel the desktop `max-width: 48ch` that was left-anchored —
         auto margins center the constrained body under the title.
         Tightened bottom margin from the desktop 36px to pull the CTA
         closer under the paragraph. */
      margin: 0 auto 12px;
    }
    /* Film CTA on mobile — the desktop padding (17px 34px) plus the
       full label "See My Film / Commercial Work" was too wide for a
       phone column, forcing the label to wrap onto two lines.
       Tighten padding, shrink the label a hair, and pin the inner
       span to nowrap so the whole button reads as one line. */
    .ab-film-cta {
      align-self: center;
      padding: 14px 22px;
      gap: 10px;
    }
    .ab-film-cta span:not(.arrow) {
      white-space: nowrap;
      font-size: 12.5px;
      letter-spacing: 0.16em;
    }
    .ab-philo-title { font-size: clamp(36px, 8vw, 56px); margin-bottom: 24px; }
    /* Mobile pillars: swap the desktop 3-column card layout for a compact
       vertical stack where each pillar becomes a horizontal row (small
       image on the left, number + title + body on the right). This fits
       three full pillars inside one 100vh section without cropping the
       images down to slivers. */
    body.page-about .ab-philo-pillars { grid-template-columns: 1fr; gap: 16px; padding: 20px 0; }
    /* The pillar has THREE children (img, num, text) and a 2-col grid,
       so implicit auto-flow was forcing .pillar-text into row 2 / col 1
       — wrapping the body copy inside the image column and making it
       unreadable. Explicit grid-template-areas pins the image to span
       both rows on the left, and stacks num + text in the right column.
       Columns split 50/50 so the image reads as a thumbnail and the
       body copy gets a readable measure — previously the image locked
       at 200px crowded the text into a narrow alley on small phones. */
    .ab-philo-pillars .pillar {
      display: grid;
      grid-template-columns: 1fr 1fr;
      /* Single-row layout — the numbering (01/02/03) is hidden on mobile
         (see `.pillar-num` rule below), so the text column no longer
         needs a separate row for the number. Collapsing to one row lets
         the body copy vertically center against its thumbnail instead
         of hanging off the top of a two-row cell. */
      grid-template-rows: 1fr;
      grid-template-areas: "img text";
      column-gap: 16px;
      row-gap: 4px;
      align-items: start;
      text-align: left;
    }
    /* Hide the italic numeral on mobile — the thumbnail + title already
       read as a clear card and the numeral was crowding the text
       column's top edge. Desktop keeps it (still rendered in the
       normal cascade). */
    .ab-philo-pillars .pillar-num { display: none; }
    .ab-philo-pillars .pillar-img {
      grid-area: img;
      /* Fill the 1fr column so the image scales with the viewport
         instead of locking at 200px. Natural aspect ratio preserved
         via auto height. */
      width: 100%;
      height: auto;
      align-self: center;
    }
    .ab-philo-pillars .pillar-num {
      grid-area: num;
      font-size: 13px;
      margin: 4px 0 2px;
    }
    .ab-philo-pillars .pillar-text {
      grid-area: text;
      font-size: 13.5px;
      line-height: 1.5;
    }
    .ab-philo-pillars .pillar-text strong {
      font-size: 17px;
      display: block;
      margin-bottom: 4px;
    }
    /* Travel on phone: polaroid at top (centered), head (title + deck) in
       the middle, and the mosaic as a 3-column CSS masonry at bottom.
       Each piece stacks naturally via the .ab-travel-layout grid (already
       collapsed to 1 column at the tablet breakpoint). The layout gap
       controls the breathing room between the polaroid and the title
       below it. */
    .ab-travel-layout { gap: 32px; }
    /* Breathing room between the deck paragraph and the photo masonry
       below. Tightened from 44px to 24px so the 3-column masonry has
       more headroom inside the 100vh travel slide. */
    .ab-travel-head { margin: 0 auto 24px; text-align: center; }
    /* Phone: uniform 2-row × 3-column grid. Each cell has a fixed
       aspect ratio so space is reserved before lazy images load —
       no reflow flash as intrinsic dimensions arrive. Photos crop
       to fill via object-fit: cover. */
    .ab-travel-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      grid-template-rows: repeat(2, auto);
      grid-auto-rows: auto;
      gap: 4px;
      column-count: auto;
      column-gap: 4px;
    }
    .ab-travel-cell:nth-child(1),
    .ab-travel-cell:nth-child(2),
    .ab-travel-cell:nth-child(3),
    .ab-travel-cell:nth-child(4),
    .ab-travel-cell:nth-child(5),
    .ab-travel-cell:nth-child(6) {
      grid-column: auto;
      grid-row: auto;
    }
    .ab-travel-cell {
      break-inside: auto;
      display: block;
      margin: 0;
      width: 100%;
      /* Matches the travel photos' native 3:2 landscape so object-fit:
         cover is effectively no-crop. Reserving the cell footprint
         before images load is what prevents the reflow flash. */
      aspect-ratio: 3 / 2;
      overflow: hidden;
    }
    .ab-travel-photo {
      /* Crop each image to fill the cell so the grid is uniform from
         first paint — matches the desktop behavior. */
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    /* Tighten the travel header so the masonry has enough vertical room
       to fit cleanly within 100vh. */
    .ab-travel-head .section-title { font-size: clamp(28px, 7vw, 40px); margin-bottom: 10px; }
    /* Deck paragraph on mobile: let it span the section's natural content
       width (bounded by the 24px section padding) instead of an extra
       32ch cap that created visible dead space on both sides.
       Bumped to 16.5px per Alex's readability note — the shrunk-down
       polaroid above (140px) buys the vertical room needed to host
       proper body-copy size without pushing the masonry out of 100vh. */
    .ab-travel-deck {
      font-size: 16.5px;
      line-height: 1.5;
      max-width: none;
      margin: 0;
      text-align: center;
    }
    /* Drop the forced break between "happiness" and "and inspires me..." —
     * at the smaller font size the sentence wraps naturally into two
     * balanced lines, and forcing a break here just added a third. */
    .deck-mobile-break { display: none; }
    /* California mobile — centered composition stays, just tighten the
       padding and shrink the hero word so it always fits the viewport.
       (No background-attachment override needed anymore — JS-driven
       parallax replaces the old fixed-attachment pseudo-parallax.) */
    .ab-cali-inner { padding: 88px 24px; }
    .ab-cali { min-height: 72vh; }
    .ab-cali-bear { width: 49px; margin-bottom: 12px; }
    .ab-cali-rule { width: 40px; margin-bottom: 24px; }
    /* Rebalanced above/below copy on phones — at <=390px widths the
       vw-derived sizes were hitting the floor (17 / 15px) and reading
       too quietly next to the 75–80px "California." hero. Bumped both
       floors and slopes so the supporting lines hold their own against
       the display word without crowding it. */
    .ab-cali-above { font-size: clamp(20px, 5.2vw, 26px); }
    .ab-cali-hero { font-size: clamp(56px, 15vw, 96px); }
    .ab-cali-below { font-size: clamp(17px, 4.6vw, 22px); }
    /* Beach CTA: just let the section shrink on mobile. (Fixed attachment
       is gone now — JS-driven parallax handles the depth effect.) */
    .ab-cta--beach { min-height: 68vh; padding: 96px 24px; }
  }
