/* ============================================================
   VERITY VISTA — shared stylesheet
   Classical x noir. Sharp gold corner brackets on photography,
   fully rounded pill buttons/inputs everywhere else — deliberate
   contrast, not an inconsistency.
   ============================================================ */

/* ============================================================
   COLOUR, IN TWO LAYERS

   Layer one is the palette: the brand's actual colours. These do not
   change with the theme, because gold is gold.

   Layer two is roles: which of those colours is the ground here, and
   which one sits on top of it. Dark mode redefines the roles and
   nothing else, so switching themes is six values rather than a
   rewrite. That is also what makes a colour change a one-line job
   later, which is the point.

   --on-ground is the key idea. Every border, shadow and quiet piece of
   text is the ground's opposite at some opacity, so instead of writing
   rgba(10, 16, 14, 0.14) - ink, at 14%, correct only on a light ground
   - they are written rgb(var(--on-ground) / 0.14). Each section sets
   --on-ground once, everything inside inherits it, and the same rule
   reads correctly on marble and on ink without being written twice.

   The channels below are space separated on purpose: that is what lets
   an alpha be applied to a variable at the point of use.

   LIGHT MODE IS UNCHANGED. Every role below resolves to exactly the
   value that was hardcoded before it, so nothing moves by a single
   hex digit.
   ============================================================ */
:root {
  --teal: #166373;
  --teal-hover: #1c7e91;
  --gold: #EF972E;
  --gold-hover: #ffb35c;
  --ink: #0A100E;
  --ivory: #ECE6D7;
  --marble: #F2ECDF;
  /* Same hex values as the "Paper"/"Deep Teal" entries in theme.json's
     color palette (which only exposes them as --wp--preset--color--*,
     for use in the editor's color pickers) -- declared again here,
     plain, so .section-deep-teal/.marquee-section can reference them
     the same simple way as the four colors above without depending on
     WP's global-styles stylesheet having already run. */
  --paper: #FAF7F0;
  --deep-teal: #0D3E48;

  /* The palette again, as channels, so opacities can be applied to it. */
  --ch-ink: 10 16 14;
  --ch-ivory: 236 230 215;
  --ch-gold: 239 151 46;
  --ch-teal: 22 99 115;
  --ch-deep-teal: 13 62 72;
  --ch-marble: 242 236 223;

  /* ---- roles: the four grounds a section can sit on ---- */
  --ground: var(--marble);            /* the light sections */
  --ground-raised: var(--paper);      /* cards lifted off them */
  --ground-mid: var(--teal);          /* the teal sections */
  --ground-gallery: var(--deep-teal); /* the scroll gallery */
  --ground-deep: var(--ink);          /* footer, hero, overlays */

  /* ---- roles: what sits on a ground ---- */
  --on-light: var(--ch-ink);          /* ink on marble, today */
  --on-dark: var(--ch-ivory);         /* ivory on ink, today */

  /* The current section's own. Sections override it; anything that is
     not in a section inherits the light one, which is what the page
     ground is. */
  --on-ground: var(--on-light);

  /* Shadows are not the ground's opposite, they are the absence of
     light, so they stay black and only their strength changes. */
  --ch-shadow: 0 0 0;

  /* Overlays that are dark whatever the theme is: the photo viewer,
     the intro, the popup scrim. A photo is shown against darkness in
     both modes, so these do not flip. */
  --scrim: rgb(var(--ch-ink) / 0.94);

  /* Four one-off colours that were never part of the palette. Each is
     kept at exactly its old value here so light mode does not move,
     and each becomes a single line to redefine for dark mode.

     The greys and whites are the honest reason this layer exists: a
     lone grey hairline and a pair of white field backgrounds are
     invisible against a dark ground, and there is no way to find that
     out from a palette they were never in. */
  --hairline: rgb(128 128 128 / 0.2);      /* one footnote rule */
  --field-ground: rgb(255 255 255 / 0.6);  /* contact form fields */
  --field-ground-focus: rgb(255 255 255 / 0.9);
  --ch-error: 179 38 30;                   /* an invalid field, not a theme colour */

  /* The colour behind a photo that does not fill its box. Rule one
     means photos are shown whole rather than cropped, so this shows
     more often here than it would on most sites, and it has to be the
     ground's colour in both themes rather than a fixed cream. */
  --photo-ground: var(--marble);

  /* The sticky filter bar on the portfolio. It was a bare #ECE6D7,
     which is ivory, and being a hex rather than an rgba it slipped
     past the migration entirely: in dark mode the bar stayed cream
     with ivory chips on it, so the categories vanished. */
  --bar-ground: var(--ivory);

  /* The light / dark switch.

     Its whole job is to say which mode you are in, so it is the one
     control that must NOT look the same in both. It was --ground-deep,
     which is dark either way, and against the dark nav in dark mode it
     more or less disappeared.

     Light: a dark pill with a pale ball, on cream.
     Dark:  a teal pill with a gold ball, on near-black.

     Different colour, different ball, so it reads at a glance without
     having to work out which end the ball is at. */
  --toggle-track: var(--ink);
  --toggle-ball: var(--paper);

  /* Small brand-coloured text on the page ground: eyebrows, mono
     labels, the badge, card links.

     Teal is the right colour for these on cream. On the dark blue
     ground it is teal on near-teal, which is what made "Our
     Philosophy" and every other eyebrow read as a dim blue smudge.
     Gold is already the site's other accent and it carries on a dark
     ground, so dark mode borrows it.

     Only the SMALL text moves. Teal stays teal wherever it is a
     background, a button or a border, so the brand is untouched. */
  --accent-text: var(--teal);

  /* The floating nav bar's own tint. It sits over whatever is
     scrolling under it, so it is a surface, not a section. */
  --nav-ground: rgb(var(--ch-marble) / 0.94);
}

/* ============================================================
   DARK MODE

   Everything below is roles. Not one palette colour is touched, so
   gold is still gold and teal is still teal: the site is recognisably
   itself, in the dark.

   The grounds are blue rather than neutral grey because the brand
   already leans that way, and a blue-black next to gold reads as a
   choice where a grey-black reads as a default.

   The four grounds keep their ORDER, which is the part that matters.
   This site alternates light and dark sections on purpose, and if dark
   mode flattened them into one colour the page would lose the rhythm
   that is most of its character. So marble becomes the lightest of the
   dark blues and ink becomes the darkest, and the alternation survives.

   TO RETUNE ANY OF IT, change a line here. Nothing else reads these
   values directly.
   ============================================================ */
[data-theme="dark"] {
  --ground: #101D28;            /* was marble: the lightest ground */
  --ground-raised: #172836;     /* cards lifted off it */
  --ground-mid: #123B49;        /* was teal */
  --ground-gallery: #0A1822;    /* was deep teal */
  --ground-deep: #070D13;       /* was ink: footer, hero, the darkest */

  /* The flip. Light sections now carry light text, and every border,
     shadow and quiet line that reads --on-ground follows from here
     without being listed. */
  --on-light: var(--ch-ivory);

  --photo-ground: #17242F;      /* behind a photo shown whole */
  --bar-ground: #16242F;        /* the portfolio's sticky filter bar */
  --toggle-track: var(--ivory);  /* light pill on a dark nav: the switch inverts with the theme */
  --toggle-ball: var(--ink);
  --accent-text: var(--gold);   /* teal text disappears on a teal-ish ground */
  --nav-ground: rgb(16 29 40 / 0.94);

  /* The three strays that were never in the palette. On a dark ground
     a 20% grey and a white field are invisible, so they are the ones
     that could not have been worked out automatically. */
  --hairline: rgb(var(--on-ground) / 0.18);
  --field-ground: rgb(var(--ch-ivory) / 0.06);
  --field-ground-focus: rgb(var(--ch-ivory) / 0.1);
}

/* Until the toggle has run, follow whatever the visitor's own system
   is set to. The toggle writes data-theme, which wins over this. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) {
    --ground: #101D28;
    --ground-raised: #172836;
    --ground-mid: #123B49;
    --ground-gallery: #0A1822;
    --ground-deep: #070D13;
    --on-light: var(--ch-ivory);
    --photo-ground: #17242F;
    --bar-ground: #16242F;
    --toggle-track: var(--ivory);
    --toggle-ball: var(--ink);
    --accent-text: var(--gold);
    --nav-ground: rgb(16 29 40 / 0.94);
    --hairline: rgb(var(--on-ground) / 0.18);
    --field-ground: rgb(var(--ch-ivory) / 0.06);
    --field-ground-focus: rgb(var(--ch-ivory) / 0.1);
  }
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  /* The page ground. Was --marble literally; it is now the role, so
     dark mode moves it without this rule knowing. It still matches
     theme.json's body background in light mode.

     It was --ink once, which only ever showed through in the gaps
     between full-bleed sections, and on a light page that read as a
     stray dark seam. */
  background: var(--ground);
  /* theme.json sets a body colour too. This one has to be here as
     well, or dark mode gets ink text on a dark ground for anything
     that is not inside a section. */
  color: rgb(var(--on-ground));
  font-family: 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--gold); text-decoration: none; }
a:hover { color: var(--gold-hover); text-decoration: underline; }
::selection { background: var(--gold); color: var(--ink); }
img { display: block; max-width: 100%; }

h1, h2, h3, h4 { text-wrap: pretty; margin: 0; }
p { text-wrap: pretty; }

/* Keyboard focus — visible everywhere, never suppressed */
:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 2px; }
.btn:focus-visible { outline-offset: 4px; }

/* ---------------------------------------------------------------
   Nav — a floating pill bar, not a full-width fixed strip. Sits
   on marble regardless of what's scrolling underneath it, so it
   reads consistently over both the dark hero and the light
   sections further down the page.
--------------------------------------------------------------- */
:root { --nav-offset: 106px; }

/* The page gutter, in one place.
   Sections inset their content by this, and anything that has to break
   back out to the full width pulls itself out by exactly the same
   amount. Keeping it a variable is not tidiness: the portfolio filter
   bar was hardcoded to -8vw while a blanket mobile rule further down
   forces every section to 6vw, so on a phone the bar pulled 2vw too far
   on each side and gave the whole page a horizontal scroll. */
:root { --vv-gutter: 8vw; }
@media (max-width: 640px) { :root { --vv-gutter: 6vw; } }
.site-nav {
  position: fixed; top: 18px; left: 18px; right: 18px; z-index: 50;
  max-width: 1240px; margin: 0 auto;
  height: 72px; border-radius: 999px;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 10px 0 24px;
  background: var(--nav-ground);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgb(var(--on-ground) / 0.06);
  box-shadow: 0 14px 34px rgb(var(--ch-ink) / 0.14);
  transition: box-shadow 300ms ease;
}
.site-nav.is-scrolled { box-shadow: 0 18px 44px rgb(var(--ch-ink) / 0.22); }

/* The light / dark switch, beside Book Now.

   Built to the pattern the client sent: a pill track with a moon at
   one end, a sun at the other, and a ball that slides between them.
   Colours are this site's rather than the reference's, so it reads as
   part of the nav instead of a widget dropped into it.

   A <button> rather than the reference's checkbox-and-label. The state
   already lives on <html data-theme>, set before first paint, so a
   checkbox would be a second copy of it to keep in step. A button with
   aria-pressed says the same thing to a screen reader and has nothing
   to fall out of sync with.

   Both icons stay visible. The ball covers whichever one is current,
   so what you SEE is the mode you would switch to. */
.theme-toggle {
  flex: none; margin-right: 12px;
  position: relative;
  width: 52px; height: 27px; padding: 0 6px;
  display: flex; align-items: center; justify-content: space-between;
  border: 1px solid rgb(var(--on-ground) / 0.16);
  border-radius: 999px;
  background: var(--toggle-track);
  cursor: pointer;
  font-size: 13px; line-height: 1;
}
.theme-toggle-moon { color: var(--gold); }
.theme-toggle-sun { color: var(--gold-hover); }

.theme-toggle-ball {
  position: absolute; left: 2px; top: 2px;
  width: 21px; height: 21px; border-radius: 50%;
  background: var(--toggle-ball);
  box-shadow: 0 1px 3px rgb(var(--ch-ink) / 0.4);
  transition: transform 200ms linear;
}
/* Ball left is light, ball right is dark, the way the reference has it. */
[data-theme="dark"] .theme-toggle-ball { transform: translateX(23px); }

.theme-toggle:hover { border-color: var(--gold); }
.theme-toggle:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* The bar is tight on a phone: logo, Book Now and the burger already
   share it. */
@media (max-width: 640px) {
  .theme-toggle { width: 33px; height: 18px; margin-right: 6px; font-size: 10px; padding: 0 3px; }
  .theme-toggle-ball { width: 13px; height: 13px; top: 2px; left: 2px; }
  [data-theme="dark"] .theme-toggle-ball { transform: translateX(14px); }
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle-ball { transition: none; }
}

.site-nav .brand { display: flex; align-items: center; gap: 12px; flex: none; }
.site-nav .brand-mark {
  width: 54px; height: 54px; border-radius: 50%;
  border: 1px solid rgb(var(--ch-teal) / 0.35);
  object-fit: cover; flex: none;
}
.site-footer .brand-mark {
  width: 64px; height: 64px; border-radius: 50%;
  border: 1px solid rgb(var(--ch-gold) / 0.5);
  object-fit: cover; flex: none; vertical-align: middle; margin-right: 14px;
}
.site-nav .brand-name {
  font-family: 'Fraunces', serif; font-style: italic; font-weight: 600;
  font-size: 23px; letter-spacing: 0.01em; color: rgb(var(--on-ground));
}
.site-nav .brand-name .dot { color: var(--gold); }

/* flex: 1 is what centres these. The bar is space-between, and once
   the toggle claimed the slack with margin-left: auto the links were
   left sitting against the logo. Letting the link row take the space
   between brand and actions, and centring inside it, puts them back in
   the middle whatever else is in the bar. */
.site-nav .links { display: flex; align-items: center; justify-content: center; gap: 2px; flex: 1; }
.nav-link {
  font-family: 'Archivo', sans-serif; font-size: 13.5px; font-weight: 500;
  color: rgb(var(--on-ground) / 0.55);
  padding: 10px 18px; border-radius: 999px;
  transition: background 200ms ease, color 200ms ease;
}
.nav-link:hover { color: rgb(var(--on-ground)); text-decoration: none; background: rgb(var(--on-ground) / 0.04); }
.nav-link.is-current { color: rgb(var(--on-ground)); background: rgb(var(--ch-teal) / 0.09); }

.site-nav .btn-sm { padding: 13px 26px; font-size: 13px; }

@media (max-width: 960px) {
  .site-nav { height: 64px; padding: 0 8px 0 18px; }
  .site-nav .brand-mark { width: 46px; height: 46px; }
  .site-nav .brand-name { font-size: 20px; }
  .site-nav .links { gap: 0; }
  .nav-link { padding: 8px 14px; font-size: 12.5px; }
  .site-nav .btn-sm { padding: 11px 20px; font-size: 12px; }
  :root { --nav-offset: 96px; }
}
/* ---------------------------------------------------------------
   Mobile nav — the hamburger + full-screen menu that replaces the
   nav-link row once it's hidden below 640px. Without this there
   was no way at all to reach Portfolio/Services from a phone once
   you'd landed on any single page — a real dead end, not just a
   rough edge. Defined before the breakpoint below on purpose: its
   own display:none and that media query's display:flex are equal
   specificity, so whichever comes later in the file wins — this
   has to be the earlier one, or the override never applies.
--------------------------------------------------------------- */
.nav-burger {
  display: none; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  width: 40px; height: 40px; flex: none; margin-left: 4px;
  background: transparent; border: none; padding: 0; cursor: pointer;
}
.nav-burger span {
  display: block; width: 20px; height: 2px; border-radius: 2px;
  background: rgb(var(--on-ground)); transition: transform 250ms ease, opacity 200ms ease;
}
.nav-burger.is-active span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-burger.is-active span:nth-child(2) { opacity: 0; }
.nav-burger.is-active span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.mobile-nav {
  position: fixed; inset: 0; z-index: 150;
  background: var(--ground-deep); --on-ground: var(--on-dark); /* the full-screen mobile menu sits on ink in both themes */
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 320ms ease, visibility 0s linear 320ms;
}
.mobile-nav.is-open {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 320ms ease, visibility 0s linear 0s;
}
.mobile-nav-link {
  font-family: 'Fraunces', serif; font-weight: 600; font-size: 30px;
  color: rgb(var(--on-ground) / 0.75);
  padding: 14px 0; transition: color 200ms ease;
}
.mobile-nav-link:hover, .mobile-nav-link:focus-visible { color: var(--gold); text-decoration: none; }
.mobile-nav-link.is-current { color: var(--gold); }
.mobile-nav-close {
  position: absolute; top: 22px; right: 22px;
  width: 44px; height: 44px; border-radius: 50%;
  background: transparent; border: 1px solid rgb(var(--on-ground) / 0.3);
  color: rgb(var(--on-ground)); font-size: 22px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.mobile-nav-close:hover { border-color: var(--gold); color: var(--gold); }
@media (prefers-reduced-motion: reduce) {
  .mobile-nav, .nav-burger span { transition: none; }
}
@media (max-width: 640px) {
  /* Four things share this bar on a phone: emblem, switch, Book Now
     and the burger. At the sizes above they filled it corner to
     corner with nothing between them, which read as crowding rather
     than as a bar. Everything here is one step down from that. */
  .site-nav { height: 52px; padding: 0 6px 0 12px; }
  .site-nav .brand-mark { width: 43px; height: 43px; }
  .site-nav .brand-name { font-size: 15px; }
  /* Below real phone widths, primary CTA + logo is all that fits
     cleanly in the bar itself — Home/Portfolio/Services move into
     the .nav-burger menu below instead of just disappearing. */
  .site-nav .nav-link { display: none; }
  .site-nav .btn-sm { padding: 8px 14px; font-size: 11px; }
  :root { --nav-offset: 86px; }
  .nav-burger { display: flex; }
}
@media (max-width: 400px) {
  /* "Book Your Session" doesn't fit next to the logo below ~400px —
     swap to a shorter label instead of letting it clip or wrap. */
  .site-nav .btn-label-full { display: none; }
  .site-nav .btn-label-short { display: inline; }
}
@media (max-width: 640px) {
  /* Separates two side-by-side contact blocks on desktop; once they
     wrap onto their own lines it's an orphaned floating line instead. */
  .contact-divider { display: none; }
}
@media (max-width: 640px) {
  /* 404's "NO SIGNAL" / "ERR/404" corner badges sit on the ambient
     background frame, independently centered from the foreground
     copy. On phones the copy wraps to enough lines that it grows
     into the badges' corner instead of clearing them — drop the
     decoration rather than let it cross through the text. */
  .err404-badge { display: none !important; }
}

/* ---------------------------------------------------------------
   Buttons & inputs — fully rounded, the soft counterpoint to the
   sharp photo brackets.
--------------------------------------------------------------- */
.btn {
  display: inline-block; border: none; cursor: pointer;
  font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 15px;
  padding: 16px 36px; border-radius: 999px;
  transition: background 180ms ease, transform 180ms ease;
}
.btn:hover { text-decoration: none; }
.btn-gold { background: var(--gold); color: var(--ink); }
.btn-gold:hover { background: var(--gold-hover); color: var(--ink); }
.btn-teal { background: var(--teal); color: var(--ivory); }
.btn-teal:hover { background: var(--teal-hover); color: var(--ivory); }
.btn-outline {
  background: transparent; color: rgb(var(--on-ground));
  border: 1px solid rgb(var(--on-ground) / 0.2);
}
.btn-outline:hover { text-decoration: none; border-color: var(--gold); }
.btn-sm { padding: 11px 26px; font-size: 13px; }
.btn-label-short { display: none; }

.input {
  padding: 16px 24px; border-radius: 999px;
  border: 1px solid rgb(var(--on-ground) / 0.18);
  background: rgb(var(--on-ground) / 0.03);
  color: rgb(var(--on-ground)); font-family: 'Archivo', sans-serif; font-size: 15px;
  outline: none; width: 100%;
}
.input:focus { border-color: var(--gold); }
textarea.input { border-radius: 24px; resize: none; }
.input-dark {
  background: rgb(var(--on-ground) / 0.06);
  border-color: rgb(var(--on-ground) / 0.25);
  color: rgb(var(--on-ground));
}

/* ---------------------------------------------------------------
   Type helpers
--------------------------------------------------------------- */
.eyebrow {
  font-family: 'Cinzel', serif; font-size: 12px; letter-spacing: 0.3em;
  color: var(--gold); text-transform: uppercase;
}
.eyebrow-teal { color: var(--accent-text); }
.display { font-family: 'Fraunces', serif; font-weight: 600; }
.mono { font-family: 'IBM Plex Mono', monospace; }

/* ---------------------------------------------------------------
   Sharp gold corner brackets — the photo-framing signature
--------------------------------------------------------------- */
.bracket { position: relative; }
.bracket i { position: absolute; width: 30px; height: 30px; pointer-events: none; border-color: var(--gold); z-index: 2; }
.bracket i.tl { top: 0; left: 0; border-top: 2px solid; border-left: 2px solid; }
.bracket i.tr { top: 0; right: 0; border-top: 2px solid; border-right: 2px solid; }
.bracket i.bl { bottom: 0; left: 0; border-bottom: 2px solid; border-left: 2px solid; }
.bracket i.br { bottom: 0; right: 0; border-bottom: 2px solid; border-right: 2px solid; }
.bracket.sm i { width: 20px; height: 20px; }

/* ---------------------------------------------------------------
   Scroll reveal — IntersectionObserver toggles .is-visible
--------------------------------------------------------------- */
.reveal {
  opacity: 0; transform: translateY(28px);
  transition: opacity 650ms cubic-bezier(.16,1,.3,1), transform 650ms cubic-bezier(.16,1,.3,1);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }
.reveal.d1 { transition-delay: 80ms; }
.reveal.d2 { transition-delay: 160ms; }
.reveal.d3 { transition-delay: 260ms; }
.reveal.d4 { transition-delay: 380ms; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ---------------------------------------------------------------
   Footer
--------------------------------------------------------------- */
.site-footer {
  background: var(--ground-deep); --on-ground: var(--on-dark); /* the footer is the page's dark foot in both themes */ padding: 56px 8vw;
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 20px;
  /* Strong enough to read as a seam even when the section above the
     footer is also var(--ink) (e.g. the homepage's closing section) —
     at 0.15 that case had no visible boundary at all, since only this
     line separated two identically-colored blocks. */
  border-top: 1px solid rgb(var(--ch-gold) / 0.3);
}
.site-footer .brand-name { font-family: 'Cinzel', serif; font-size: 13px; letter-spacing: 0.24em; color: rgb(var(--on-ground)); }
.site-footer .links { display: flex; gap: 28px; }
.site-footer a { font-family: 'Archivo', sans-serif; font-size: 13px; color: rgb(var(--on-ground) / 0.6); }
.site-footer .rights { font-family: 'Archivo', sans-serif; font-size: 12px; color: rgb(var(--on-ground) / 0.35); }

/* ---------------------------------------------------------------
   Marquee (continuous auto-scroll strip)
--------------------------------------------------------------- */
@keyframes marqueeScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.marquee-track { display: flex; align-items: center; width: max-content; animation: marqueeScroll 46s linear infinite; }
@media (prefers-reduced-motion: reduce) { .marquee-track { animation: none; } }
/* Width isn't fixed — each tile sets its own aspect-ratio inline
   (matching its real photo) alongside its height, so a portrait shot
   naturally comes out narrower instead of getting squeezed into the
   same box a landscape one uses. */
.marquee-tile { position: relative; margin-right: 14px; flex: none; overflow: hidden; background: var(--photo-ground); }
.marquee-tile img { width: 100%; height: 100%; object-fit: contain; }
.marquee-caption {
  position: absolute; bottom: 12px; left: 14px;
  font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.08em;
  /* Over a photograph, so it is a dark chip with light text in both
     themes. Following the ground made it a LIGHT chip in dark mode
     with light text on it, which is nothing at all. */
  color: var(--ivory); background: rgb(var(--ch-ink) / 0.55); padding: 4px 8px; pointer-events: none;
}

/* ---------------------------------------------------------------
   Scroll-cue + misc keyframes
--------------------------------------------------------------- */
@keyframes scrollBounce { 0%, 100% { transform: translateY(0); opacity: 0.55; } 50% { transform: translateY(8px); opacity: 1; } }
@keyframes stepIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.015); } }
@keyframes blinkDot { 0%, 100% { opacity: 1; } 50% { opacity: 0.15; } }
@keyframes riseIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }

/* ---------------------------------------------------------------
   Photo gallery (scroll-linked horizontal track, Home page)
--------------------------------------------------------------- */
.gallery-pin-wrapper { position: relative; }

/* Slider arrows. Hidden by default: on desktop this gallery is driven
   by page scroll, so a pair of arrows would be lying about how it
   works. They are switched on with the phone slider further down. */
.gallery-nav { display: none; }
.gallery-dots { display: none; }
.gallery-sticky { position: sticky; top: 0; left: 0; height: 100vh; overflow: hidden; display: flex; flex-direction: column; justify-content: center; }
.gallery-track { display: flex; align-items: center; gap: 16px; padding: 0 40px; width: max-content; will-change: transform; }
.gallery-tile { position: relative; overflow: hidden; flex: none; background: rgb(var(--on-ground) / 0.06); }
/* contain, never cover. The tile is now sized to the photo's own shape
   (blocks/photo-gallery/render.php), so there is nothing to crop away;
   contain is the belt-and-braces guarantee that a photo whose stored
   dimensions are missing or wrong still arrives whole rather than cut. */
.gallery-tile img { width: 100%; height: 100%; object-fit: contain; }
.gallery-progress-rail { position: absolute; bottom: 56px; left: 40px; right: 40px; height: 1px; background: rgb(var(--on-ground) / 0.15); }
.gallery-progress-fill { position: absolute; top: 0; left: 0; height: 100%; width: 0%; background: var(--gold); }

/* ---------------------------------------------------------------
   FAQ accordion
--------------------------------------------------------------- */
.faq-row { border-top: 1px solid rgb(var(--on-ground) / 0.14); padding: 26px 0; }
.faq-q { display: flex; justify-content: space-between; align-items: center; gap: 24px; cursor: pointer; }
.faq-icon { flex: none; width: 22px; height: 22px; position: relative; transition: transform 350ms ease; }
/* The plus that turns into a cross.

   --accent-text, not --teal. It is the same thing as the eyebrows and
   the mono labels: a small brand-coloured mark sitting on the page
   ground, and teal on the dark blue ground is teal on near-teal, which
   reads as black. The earlier pass that moved those to the token only
   looked at `color`, and this one paints with `background`, so it was
   missed. On a teal section it is already overridden to gold further
   down, which is why the FAQ on Services never showed the problem. */
.faq-icon span { position: absolute; background: var(--accent-text); }
.faq-icon .h { top: 50%; left: 0; width: 100%; height: 2px; transform: translateY(-50%); }
.faq-icon .v { top: 0; left: 50%; width: 2px; height: 100%; transform: translateX(-50%); }
.faq-row.is-open .faq-icon { transform: rotate(45deg); }
.faq-a { overflow: hidden; max-height: 0; opacity: 0; transition: max-height 450ms cubic-bezier(.16,1,.3,1), opacity 350ms ease; }
.faq-row.is-open .faq-a { max-height: 320px; opacity: 1; }

/* ---------------------------------------------------------------
   Portfolio filter chips
--------------------------------------------------------------- */
.portfolio-filter-bar {
  position: sticky; top: var(--nav-offset); z-index: 20;
  background: var(--bar-ground); padding: 22px var(--vv-gutter);
  display: flex; gap: 28px; flex-wrap: wrap;
  border-top: 1px solid rgb(var(--on-ground) / 0.12);
  border-bottom: 1px solid rgb(var(--on-ground) / 0.16);
  box-shadow: 0 10px 24px rgb(var(--ch-ink) / 0.07);
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
@media (max-width: 640px) {
  /* Ten chips wrap to four rows at phone widths — combined with the
     nav above it, this sticky bar was eating over half the screen for
     the entire scroll. A single scrolling row keeps every filter one
     tap away without permanently claiming that much real estate. */
  .portfolio-filter-bar {
    flex-wrap: nowrap; overflow-x: auto; gap: 22px; padding: 16px var(--vv-gutter);
    scrollbar-width: none;
  }
  .portfolio-filter-bar::-webkit-scrollbar { display: none; }
  /* The row scrolls, and with the scrollbar hidden nothing said so: the
     last category just stopped mid-word and read as a layout bug. A
     fade on the trailing edge is the usual way to say "there is more
     this way" without spending a row on arrows. */
  .portfolio-filter-bar {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  }
  .portfolio-filter-bar .filter-chip { flex: none; white-space: nowrap; }
}
.filter-chip {
  /* These are <button> in WordPress where the static build used <span>,
     because a filter you cannot reach with a keyboard is not a filter.
     The element brings the browser's own button chrome with it though,
     and the rules below only ever set border-bottom, so every chip was
     rendering as a grey outlined box. Strip the chrome first, then the
     original styling lands on it exactly as designed. */
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; margin: 0; border-radius: 0;
  font-family: 'Cinzel', serif; font-weight: 500; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  cursor: pointer; padding: 8px 0; border-bottom: 2px solid transparent;
  /* Was 0.5 — legible enough up close, but the whole row read as
     washed-out grey at a glance. This is a real contrast jump, not
     a tweak, so the unselected filters are actually easy to read. */
  color: rgb(var(--on-ground) / 0.78);
  transition: color 250ms ease, border-color 250ms ease;
}
.filter-chip:hover { color: rgb(var(--on-ground)); }
.filter-chip.is-active { color: rgb(var(--on-ground)); font-weight: 600; border-bottom-color: var(--gold); border-bottom-width: 3px; }
.portfolio-tile {
  position: relative; overflow: hidden; background: var(--photo-ground);
  transition: opacity 300ms ease, transform 300ms cubic-bezier(.2,.7,.2,1);
}
/* No cropping — a client note was explicit that cutting into a
   composed photo reads as unprofessional, and it's a fair point:
   the frame is deliberate, not incidental. Rather than force every
   tile into one uniform box (which is what made contain look bad
   here before — mismatched letterboxing every row), each tile's own
   box now matches its real orientation almost exactly (measured
   from the actual files: landscape shots are ~1600x1064, portraits
   the same ratio rotated), so contain has essentially nothing left
   to letterbox. main.js groups same-orientation tiles into their
   own rows (see regroupPortfolioByOrientation) so a row is never a
   mix of tall and wide frames either. */
/* Fallback only. Each tile now carries its own aspect-ratio inline,
   taken from the real photo, so these apply solely to a photo whose
   dimensions were never recorded. Forcing every landscape into
   1600/1064 was safe from cropping (object-fit is contain) but
   letterboxed anything shaped differently. */
.portfolio-tile[data-orient="landscape"] { aspect-ratio: 1600 / 1064; }
.portfolio-tile[data-orient="portrait"] { aspect-ratio: 1064 / 1600; }
.portfolio-tile[data-orient="square"] { aspect-ratio: 1 / 1; }
.portfolio-tile img { width: 100%; height: 100%; object-fit: contain; }
/* .is-leaving fades a tile out in place before the filter switch pulls
   it from the grid; .is-hidden then removes it from flow so the grid
   reflows around what's left. Two classes so the removal (which can't
   be animated) happens only after the fade has actually played. */
.portfolio-tile.is-leaving { opacity: 0; transform: scale(0.94); pointer-events: none; }
.portfolio-tile.is-hidden { display: none; }
@media (prefers-reduced-motion: reduce) {
  .portfolio-tile { transition: none; }
}
/* Frame number — a fixed position in the real archive, not a
   generated index. Filtering to one category shows non-sequential
   numbers (09, 14, 22...) on purpose, the way flipping to specific
   frames on a contact sheet would, rather than a plain photo grid
   that could belong to any portfolio template. */
.portfolio-frame-num {
  position: absolute; top: 12px; right: 12px; pointer-events: none;
  font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.1em;
  color: var(--ivory);
  /* A text-shadow alone still washed out against a bright sky or a
     white dress — no backdrop to guarantee contrast against. A
     solid tag behind it reads clearly over any photo, not just the
     darker ones. */
  background: rgb(var(--ch-ink) / 0.55); padding: 3px 7px; border-radius: 3px;
  /* Always on, not hover-only — the whole point of a contact-sheet
     numbering scheme is that it reads as "a real archive" the moment
     the grid loads, not something you only discover by hovering
     every tile. Hovering just brightens it a touch. */
  opacity: 0.8; transition: opacity 280ms ease;
}
.portfolio-tile:hover .portfolio-frame-num, .portfolio-tile:focus-within .portfolio-frame-num { opacity: 1; }

/* Portfolio tile caption — revealed together with the corner brackets
   on hover/focus, rather than sitting on the photo at all times (the
   old always-on dark pill, reused wholesale from the homepage marquee,
   read as a label slapped over the work rather than a considered
   detail). A bottom scrim keeps the caption legible over any photo. */
.portfolio-caption-scrim {
  position: absolute; inset: 0; pointer-events: none;
  /* Was 0.55 fading out by 42% up the tile — over a bright photo
     (a white dress, a sunny sky) that left gold and ivory text with
     nowhere near enough contrast to actually read. Darker, and dark
     for longer, so the caption has a reliable backdrop over any
     photo, not just the darker ones. */
  /* A caption shade over a photo: dark in both themes, same reason. */
  background: linear-gradient(0deg, rgb(var(--ch-ink) / 0.82) 0%, rgb(var(--ch-ink) / 0.45) 30%, rgb(var(--ch-ink) / 0) 62%);
  opacity: 0; transition: opacity 280ms ease;
}
.portfolio-tile:hover .portfolio-caption-scrim, .portfolio-tile:focus-within .portfolio-caption-scrim { opacity: 1; }
.portfolio-caption {
  position: absolute; left: 16px; right: 16px; bottom: 14px; pointer-events: none;
  display: flex; flex-direction: column; gap: 2px;
  opacity: 0; transform: translateY(6px);
  transition: opacity 280ms ease, transform 280ms cubic-bezier(.16,1,.3,1);
}
.portfolio-tile:hover .portfolio-caption, .portfolio-tile:focus-within .portfolio-caption { opacity: 1; transform: translateY(0); }
.portfolio-caption .cat {
  font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--gold);
  text-shadow: 0 1px 6px rgb(var(--ch-shadow) / 0.65);
}
.portfolio-caption .sub {
  font-family: 'Fraunces', serif; font-style: italic; font-size: 15px; color: var(--ivory);
  text-shadow: 0 1px 6px rgb(var(--ch-shadow) / 0.65);
}

/* ---------------------------------------------------------------
   Booking multi-step
--------------------------------------------------------------- */
.step-dot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid rgb(var(--on-ground) / 0.4); transition: background 300ms ease, border-color 300ms ease; }
.step-dot.is-active, .step-dot.is-done { background: var(--gold); border-color: var(--gold); }
.step-label { font-family: 'Cinzel', serif; font-size: 10px; letter-spacing: 0.14em; color: rgb(var(--on-ground) / 0.4); }
.step-label.is-active, .step-label.is-done { color: var(--gold); }
.step-line { flex: 1; height: 1px; margin: 0 10px; background: rgb(var(--on-ground) / 0.2); transition: background 300ms ease; }
.step-line.is-done { background: var(--gold); }
.service-option {
  display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; cursor: pointer;
  border: 1px solid rgb(var(--on-ground) / 0.15); background: transparent;
  transition: border-color 200ms ease, background 200ms ease;
}
.service-option.is-selected { border-color: var(--gold); background: rgb(var(--ch-gold) / 0.08); }
.booking-step { display: none; }
.booking-step.is-active { display: flex; }

/* ---------------------------------------------------------------
   Split grid — reusable asymmetric two-column layout with a
   custom ratio, replacing one-off inline grid-template-columns.
   Set --split-a / --split-b inline to change the ratio per use.
--------------------------------------------------------------- */
.split-grid {
  display: grid;
  grid-template-columns: var(--split-a, 1fr) var(--split-b, 1fr);
  gap: var(--split-gap, 56px);
  align-items: var(--split-align, center);
}
/* Services' "About Brendan" photo: a fixed height sized for its
   desktop column. Once split-grid stacks to one column below 860px
   that same 520px height applies to a much narrower box, and with
   object-fit:contain the photo shrinks to fit — leaving a tall bare
   marble gap above and below it instead of filling the frame. */
.svc-about-photo { height: 520px; }
@media (max-width: 860px) {
  .svc-about-photo { height: 260px; }
  /* Booking's Setmore panel has no max-width of its own — on desktop
     it's naturally sized by its ~1.08fr split-grid column, but once
     that column stacks to full page width, the panel stretched with
     it while its own content (a short label, headline, one button)
     stayed centered and compact. Result: a wide, sparse bracket box
     with a small cluster of content floating in the middle of it.
     Capping it here keeps it reading as a proper card. */
  .booking-panel { max-width: 480px; margin: 0 auto; }
}

/* ---------------------------------------------------------------
   Portfolio masonry — responsive column count
--------------------------------------------------------------- */
/* Explicit column counts, not auto-fill — main.js reads the live
   column count (via getComputedStyle) to know how many same-
   orientation tiles make a full row at the current width, so it
   needs a predictable, named number here rather than "however many
   300px slots happen to fit". Row height comes from each tile's own
   aspect-ratio now, not a fixed grid-auto-rows. */
.portfolio-masonry {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
@media (max-width: 900px) {
  .portfolio-masonry { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .portfolio-masonry { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------
   Consistent photo grade — a faint teal/gold wash that unifies
   photography shot on different days/cameras under one mood.
--------------------------------------------------------------- */
.bracket img, .gallery-tile img, .portfolio-tile img, .marquee-tile img {
  filter: saturate(1.08) contrast(1.04);
}
.bracket, .gallery-tile, .portfolio-tile, .marquee-tile { position: relative; overflow: hidden; }
.bracket:not([data-inline-form]):not([data-no-grade])::after, .gallery-tile::after, .portfolio-tile::after, .marquee-tile::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(180deg, rgb(var(--ch-deep-teal) / .16) 0%, rgb(var(--ch-deep-teal) / 0) 30%, rgb(var(--ch-deep-teal) / 0) 60%, rgb(var(--ch-ink) / .30) 100%),
    linear-gradient(100deg, rgb(var(--ch-gold) / .08), rgb(var(--ch-deep-teal) / 0) 40%);
  mix-blend-mode: multiply;
}

/* ---------------------------------------------------------------
   Hover micro-interactions — clickable photo tiles zoom gently
   and reveal their brackets; static anchor photos keep brackets
   always on (they're not clickable, so no hover payoff needed).
--------------------------------------------------------------- */
.gallery-tile img, .portfolio-tile img { transition: transform 900ms cubic-bezier(.2,.7,.2,1); }
.gallery-tile:hover img, .portfolio-tile:hover img { transform: scale(1.05); }
.gallery-tile, .portfolio-tile { cursor: zoom-in; }
.gallery-tile i, .portfolio-tile i { opacity: 0; transition: opacity 220ms ease; }
.gallery-tile:hover i, .portfolio-tile:hover i,
.gallery-tile:focus-within i, .portfolio-tile:focus-within i { opacity: 1; }
.card-lift { transition: transform 260ms cubic-bezier(.2,.7,.2,1), box-shadow 260ms ease; }
.card-lift:hover { transform: translateY(-4px); box-shadow: 0 18px 40px rgb(var(--ch-shadow) / .18); }

/* ---------------------------------------------------------------
   Lightbox
--------------------------------------------------------------- */
.lightbox {
  /* A photo is shown against darkness in both themes, so everything
     inside here reads ivory rather than whatever the page is using. */
  --on-ground: var(--on-dark);
  position: fixed; inset: 0; z-index: 200;
  background: rgb(var(--ch-ink) / 0.94);
  display: flex; align-items: center; justify-content: center;
  padding: 5vh 5vw;
  /* display:none/flex can't transition at all — that's why this used
     to just snap open and shut. Opacity+visibility can, and the
     visibility delay keeps it out of the tab/click order at rest
     without losing the fade: closing fades first, then goes inert;
     opening goes interactive immediately and fades in. */
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 320ms ease, visibility 0s linear 320ms;
}
.lightbox.is-open {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 320ms ease, visibility 0s linear 0s;
}
.lightbox img {
  max-width: 100%; max-height: 88vh; object-fit: contain; box-shadow: 0 30px 80px rgb(var(--ch-shadow) / .5);
  transform: scale(0.96);
  transition: transform 380ms cubic-bezier(.16,1,.3,1);
}
.lightbox.is-open img { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .lightbox, .lightbox img { transition: none; }
}
.lightbox-close, .lightbox-prev, .lightbox-next, .lightbox-caption {
  /* Above the photo, explicitly.
     The photo carries a transform for its open animation, and a
     transform creates a stacking context, so the image paints as
     though it were positioned. With nothing else setting a z-index,
     DOM order decided the rest: close and prev are written before the
     <img> and next after it, so the image covered the previous-photo
     arrow while the next-photo arrow sat on top. It looked like only
     the left arrow was broken; in fact close was behind too and only
     got away with it because the image never reaches that corner. */
  z-index: 2;
}
.lightbox-close, .lightbox-prev, .lightbox-next {
  position: absolute; background: rgb(var(--on-ground) / 0.08); border: 1px solid rgb(var(--ch-gold) / 0.4);
  color: var(--ivory); border-radius: 999px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-family: 'Archivo', sans-serif; font-size: 20px; line-height: 1;
  width: 46px; height: 46px; transition: background 180ms ease, border-color 180ms ease;
}
.lightbox-close:hover, .lightbox-prev:hover, .lightbox-next:hover { background: rgb(var(--ch-gold) / 0.18); border-color: var(--gold); }
.lightbox-close { top: 24px; right: 24px; }
.lightbox-prev { left: 24px; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 24px; top: 50%; transform: translateY(-50%); }
.lightbox-caption {
  position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%);
  font-family: 'IBM Plex Mono', monospace; font-size: 11px; letter-spacing: 0.1em;
  color: rgb(var(--on-ground) / 0.75);
}
@media (max-width: 640px) {
  .lightbox-prev, .lightbox-next, .lightbox-close { width: 40px; height: 40px; font-size: 16px; }
  .lightbox-prev { left: 10px; } .lightbox-next { right: 10px; } .lightbox-close { top: 12px; right: 12px; }
}

/* ---------------------------------------------------------------
   Section hairline — a quiet gold thread at hard color-block
   boundaries, so section changes feel finished, not abrupt.
--------------------------------------------------------------- */
.section-seam { border-top: 1px solid rgb(var(--ch-gold) / 0.14); }

/* =================================================================
   RESPONSIVE — real mobile handling. Two-column layouts stack,
   the portfolio masonry drops columns, the scroll-pinned gallery
   degrades to a plain swipeable strip (position:sticky scroll-
   jacking is unreliable on mobile browsers).
================================================================= */
@media (max-width: 860px) {
  .split-grid { grid-template-columns: 1fr !important; gap: 32px; }
  .split-grid > * { order: initial !important; }

  /* Positioned relative to the whole section for the desktop two-column
     layout, so it sits behind the headline; once split-grid stacks to a
     single column it floats over the photos instead. Simplest fix is to
     drop it below the width where that pairing breaks. */
  .vision-quote-mark { display: none; }

  section { padding-left: 6vw !important; padding-right: 6vw !important; }

  /* The pinned desktop gallery becomes a swipeable slider.
     Three things had to give, and only the first two were done:

       position and height, so it stops pinning;

       overflow, which was left at hidden. The track inside is a single
       long row, so the whole thing was simply clipped;

       width, which was left at max-content. That is what made it
       unscrollable rather than merely clipped: the track was 5137px
       wide with scrollWidth equal to clientWidth, so there was nothing
       to scroll. One photo, no way to reach the other nine. */
  .gallery-sticky {
    position: static !important;
    height: auto !important;
    overflow: visible !important;
    display: block !important;
  }
  .gallery-pin-wrapper { height: auto !important; }

  .gallery-track {
    transform: none !important;
    width: auto !important;          /* the scroll container, not a long row */
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory;   /* mandatory, so it settles on a photo */
    scroll-padding-left: 6vw;
    gap: 12px;
    padding: 0 6vw 14px;
    scrollbar-width: none;
  }
  .gallery-track::-webkit-scrollbar { display: none; }

  /* Overriding an inline style, so !important is the only way. 78vw
     leaves the next photo peeking, which is what tells someone there
     is more to swipe. height auto with the tile's own aspect-ratio
     keeps every shape intact, so nothing is cropped or letterboxed. */
  /* One height for every slide, width from the photo's own shape.
     Sizing by width instead made a portrait slide twice the height of
     a landscape one, and the per-tile align-self that staggers them so
     nicely on desktop then scattered them up and down a 470px track
     with 202px photos in it. Fixing the height and letting
     aspect-ratio drive the width keeps the row even, keeps every shape
     intact, and crops nothing. */
  /* ONE FIXED SLIDE WIDTH, asked for on 22 Sep as "slides 1.2".
     78vw centre-snapped leaves about a sixth of the photo either side
     showing, which is what makes a slider look like a slider before
     anyone touches it.

     The slide used to shrink-wrap its photo instead, for good reasons
     that turned out to cost more than they were worth: it kept every
     shape exact, but it also made every slide a different width, so
     "1.2 slides" had no meaning and the peek at the edge was anything
     from nothing to half a photo depending on which way round the next
     one happened to be.

     So the slide is a fixed frame and the photo sits inside it,
     bounded on both axes and keeping its own proportions: a portrait
     uses the full height, a landscape the full width. Nothing is
     cropped, which is the rule that actually matters. The space beside
     a tall photo is the section's own ground showing through, not a
     bar drawn around it. */
  .gallery-tile {
    scroll-snap-align: center;
    flex: 0 0 78vw !important;
    width: 78vw !important;
    height: min(52vh, 340px) !important;
    aspect-ratio: auto !important;
    align-self: center !important;
    display: flex; align-items: center; justify-content: center;
    background: transparent;
  }
  .gallery-tile img {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: initial;
    display: block;
    margin: auto;
  }
  .gallery-track { align-items: center; }

  .gallery-sticky { position: relative !important; }

  /* The progress rail belongs to the desktop gallery, where the page
     scroll drives it. It is positioned against the pin wrapper, so on
     a phone it lands across the dots. The dots say the same thing
     better here. */
  .gallery-progress-rail { display: none; }

  /* Pagination dots. Ten small marks under the photos: they say how
     many there are, which one you are on, and by existing at all that
     the thing is a slider. That last part was the client's complaint.
     Dots rather than the arrows they replace, because a dot cannot
     scroll to the wrong place. */
  /* The bottom padding is the section's own breathing room on a phone:
     the dots were sitting on the edge where the teal ends, which read
     as the slider being cut off rather than finished. */
  .gallery-dots {
    display: flex; justify-content: center; align-items: center;
    gap: 9px;
    padding: 22px 6vw 52px;
  }
  .gallery-dot {
    width: 7px; height: 7px; padding: 0;
    border: none; border-radius: 50%;
    background: rgb(var(--on-ground) / 0.28);
    cursor: pointer;
    transition: background 220ms ease, transform 220ms ease;
  }
  /* The active dot grows as well as changing colour: on a small screen
     in daylight, colour alone is easy to miss. */
  .gallery-dot.is-active {
    background: var(--gold);
    transform: scale(1.5);
  }
  .gallery-dot:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

  @media (prefers-reduced-motion: reduce) {
    .gallery-dot { transition: none; }
  }

  /* Reveal-on-scroll has to come off these.
     It fades each tile in when it intersects the viewport, which works
     down a page and not at all across a scroller: a tile waiting off to
     the right has no intersection, so it sits at opacity 0. The photo
     peeking at the edge is the whole affordance that says "swipe me",
     and it was invisible. The slider carries its own motion; it does
     not need a fade as well. */
  .gallery-tile.reveal { opacity: 1; transform: none; transition: none; }
}

/* ---------------------------------------------------------------
   Link cards (Contact's Call/Email/Instagram, Sitemap's page list).
   Originally built to float over a full-bleed photo — its near-
   opaque marble fill made sense as a frosted panel there. Both of
   its actual uses today sit directly on flat marble instead, where
   that same fill is the exact same color as the page behind it, so
   the "card" rendered as a bare outline with nothing inside it. A
   real white fill plus a shadow gives it an actual presence again.
--------------------------------------------------------------- */
.hero-card-wrap { padding: 64px; }
.hero-card {
  position: relative;
  /* Was #FFFFFF. The client's rule is no pure white anywhere, so this
     uses Paper instead -- still lighter than the marble it sits on, so
     the card keeps the lift the shadow was written for. */
  background: var(--ground-raised); border: 1px solid rgb(var(--ch-gold) / 0.4);
  padding: 44px 40px; display: flex; flex-direction: column; gap: 18px;
  box-shadow: 0 24px 48px -18px rgb(var(--ch-ink) / 0.14);
  transition: transform 280ms cubic-bezier(.2,.7,.2,1), box-shadow 280ms ease, border-color 280ms ease;
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.hero-card:hover, .hero-card:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 32px 60px -16px rgb(var(--ch-ink) / 0.2);
  border-color: var(--gold);
}
@media (prefers-reduced-motion: reduce) {
  .hero-card { transition: none; }
  .hero-card:hover, .hero-card:focus-within { transform: none; }
}
/* Contact page's Call/Email/Instagram row. A fixed 3-column grid with
   no narrower fallback forced each column to stay wide enough that
   the row (and the page along with it) overflowed horizontally on
   phones — auto-fit lets it drop to 2 then 1 column on its own. */
.contact-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 28px; }
@media (max-width: 640px) {
  .hero-card-wrap { padding: 28px; }
  .hero-card { padding: 28px 24px; max-width: none !important; }
}

/* ---------------------------------------------------------------
   Hero 2.0 — centered badge/headline/CTA on marble, with a
   tilted photo strip continuously sliding right underneath.
--------------------------------------------------------------- */
.hero2 {
  background: var(--ground); position: relative; overflow: hidden;
  min-height: 100vh; display: flex; flex-direction: column; justify-content: center;
  padding-top: var(--nav-offset);
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
/* min-width: 0 overrides the flex default of "never shrink below my
   content's natural width" — without it, the badge/headline text push
   this column wider than the viewport on narrow phones instead of
   wrapping to fit it. */
.hero2-top { padding: 0 6vw 32px; text-align: center; min-width: 0; }
.hero2-badge {
  display: inline-block; max-width: 100%; font-family: 'Archivo', sans-serif; font-size: 13px; font-weight: 600;
  color: var(--accent-text); border: 1px solid rgb(var(--ch-gold) / 0.45); border-radius: 999px;
  padding: 8px 20px; margin-bottom: 18px;
}
.hero2-headline {
  font-size: clamp(32px, 4.6vw, 62px); line-height: 1.12; color: rgb(var(--on-ground));
  margin: 0 auto 16px;
}
.hero2-sub {
  font-family: 'Archivo', sans-serif; font-size: 16px; line-height: 1.55;
  color: rgb(var(--on-ground) / 0.6); max-width: 46ch; margin: 0 auto;
}
.hero2-strip { position: relative; height: 230px; }
.hero2-strip-track {
  position: absolute; left: 0; top: 0; display: flex; align-items: center; gap: 22px;
  width: max-content; padding: 0 6vw;
  animation: heroStripRight 42s linear infinite;
}
@keyframes heroStripRight { from { transform: translateX(-50%); } to { transform: translateX(0); } }
.hero2-tile {
  /* Width isn't set — it follows from height + each tile's own
     aspect-ratio (set inline per tile, matching its real photo), so
     a portrait shot gets a narrower card instead of the same fixed
     box as everything else. */
  flex: none; height: 226px; overflow: hidden; border-radius: 18px;
  border: 2px solid rgb(var(--ch-gold) / 0.55); background: var(--ground);
  box-shadow: 0 22px 44px rgb(var(--ch-ink) / 0.16);
}
/* contain, not cover — the whole photo stays intact inside the frame,
   sitting on a marble mat rather than being cropped to fill the box. */
.hero2-tile img { width: 100%; height: 100%; object-fit: contain; filter: saturate(1.08) contrast(1.04); }
.hero2-tile:nth-child(odd) { transform: rotate(-4deg); }
.hero2-tile:nth-child(even) { transform: rotate(3deg); }
.hero2-cta {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 5;
  box-shadow: 0 18px 40px rgb(var(--ch-gold) / 0.35);
}
@media (prefers-reduced-motion: reduce) { .hero2-strip-track { animation: none; } }
@media (max-width: 860px) {
  /* 100vh is a desktop idea. The hero's content here is about 400px
     on a phone and 454px on a tablet, so a full viewport left 348px
     and 474px of empty marble around it: the visitor scrolled past a
     mostly blank screen before reaching anything.

     A fixed 600px instead of a viewport unit, because the content does
     not grow with the screen. min-height, not height, so a long
     headline in another language still pushes it taller rather than
     spilling out. */
  .hero2 { min-height: 600px; }

  .hero2-headline { font-size: clamp(28px, 5.5vw, 44px); }
  .hero2-strip { height: 190px; }
  .hero2-tile { height: 182px; border-radius: 14px; }
  /* The CTA is a fixed overlay on a strip that keeps shrinking under
     it — without scaling down too, it ends up wide enough to eclipse
     a whole tile or two instead of sitting in the gap between them. */
  .hero2-cta { padding: 13px 26px; font-size: 13.5px; }
}
@media (max-width: 640px) {
  .hero2-top { padding-bottom: 24px; }
  .hero2-strip { height: 160px; }
  .hero2-tile { height: 148px; border-radius: 12px; }
  .hero2-cta { padding: 11px 20px; font-size: 12.5px; }
}
@media (max-width: 420px) {
  /* Even "Book Now" is tight against 112px tiles below this width;
     the short label keeps the pill from swallowing a whole photo. */
  .hero2-cta .btn-label-full { display: none; }
  .hero2-cta .btn-label-short { display: inline; }
}


/* ---------------------------------------------------------------
   Vision duo — two photos instead of one, a large bracket-framed
   shot with a smaller "print" tucked over its corner for depth.
--------------------------------------------------------------- */
.vision-duo { position: relative; width: 100%; height: 560px; }
.vision-duo-main {
  position: absolute; top: 0; left: 0; width: 76%; height: 460px;
  overflow: hidden; background: var(--photo-ground);
}
.vision-duo-main img { width: 100%; height: 100%; object-fit: contain; }
.vision-duo-accent {
  position: absolute; bottom: 0; right: 0; width: 54%; height: 260px;
  overflow: hidden; border: 8px solid var(--ground); background: var(--photo-ground);
  box-shadow: 0 24px 50px rgb(var(--ch-ink) / 0.22);
}
.vision-duo-accent img { width: 100%; height: 100%; object-fit: contain; filter: saturate(1.08) contrast(1.04); }
@media (max-width: 640px) {
  .vision-duo { height: 420px; }
  .vision-duo-main { height: 320px; }
  .vision-duo-accent { width: 62%; height: 190px; border-width: 6px; }
}

/* ---------------------------------------------------------------
   Booking ledger head — a small dossier-style label row at the top
   of the form panel (leader-dotted, like the rates ledger), echoing
   the section's own giant "04" watermark instead of a photo prop.
--------------------------------------------------------------- */
.booking-ledger-head {
  display: flex; align-items: baseline; gap: 12px;
  margin-bottom: 28px; padding-bottom: 16px;
  border-bottom: 1px solid rgb(var(--on-ground) / 0.14);
}
.booking-ledger-head .mono { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: rgb(var(--on-ground) / 0.45); }
.booking-ledger-head .booking-ledger-leader {
  flex: 1 1 auto; border-bottom: 1px dotted rgb(var(--on-ground) / 0.3);
  min-width: 24px; transform: translateY(-4px);
}

/* ---------------------------------------------------------------
   Services showcase — an interactive list instead of three equal
   cards. Selecting a service expands its detail and swaps the
   sticky preview photo, which deep-links straight into that
   category on the Portfolio page (see initServicesShowcase / the
   ?cat= handling in initPortfolioFilter, main.js).
--------------------------------------------------------------- */
.svc-showcase { display: grid; grid-template-columns: 1.15fr 1fr; gap: 64px; margin-top: 56px; align-items: start; }

.svc-list { display: flex; flex-direction: column; border-top: 1px solid rgb(var(--on-ground) / 0.14); }
.svc-row {
  all: unset; box-sizing: border-box; cursor: pointer; width: 100%; text-align: left;
  display: flex; gap: 20px; align-items: baseline;
  padding: 26px 4px 26px 22px; border-bottom: 1px solid rgb(var(--on-ground) / 0.14);
  border-left: 2px solid transparent;
  transition: padding 400ms cubic-bezier(.16,1,.3,1), border-color 300ms ease;
}
.svc-row-num { font-size: 13px; color: rgb(var(--on-ground) / 0.35); flex: none; width: 22px; transition: color 300ms ease; }
.svc-row-body { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.svc-row-title {
  font-size: clamp(23px, 2.4vw, 32px); line-height: 1.15;
  color: rgb(var(--on-ground) / 0.42); transition: color 300ms ease;
}
.svc-row-desc {
  font-family: 'Archivo', sans-serif; font-size: 15px; line-height: 1.65; color: rgb(var(--on-ground) / 0.7);
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 450ms cubic-bezier(.16,1,.3,1), opacity 350ms ease, margin-top 350ms ease;
}
.svc-row-bullets {
  display: flex; flex-direction: column; gap: 5px;
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 450ms cubic-bezier(.16,1,.3,1), opacity 350ms ease, margin-top 350ms ease;
}
.svc-row-bullets .mono { font-size: 10.5px; color: var(--accent-text); }
.svc-row.is-active { border-left-color: var(--gold); padding-top: 34px; padding-bottom: 34px; }
.svc-row.is-active .svc-row-num { color: var(--gold); }
/* The active row goes to FULL strength; the inactive ones sit at 42%
   of the same colour, two rules up. So this is the ground's opposite,
   not ink. It was left as ink on the assumption that the gold marked
   the row's background, and it does not: the gold is a left border,
   and the title sits on the section like everything else. On the dark
   ground that made the selected service the one you could not read. */
.svc-row.is-active .svc-row-title { color: rgb(var(--on-ground)); }
/* Ceilings sit well above the tallest this text actually wraps to
   (measured 4 lines / 99px at mobile widths) rather than matching it
   exactly — a tight cap clips the last line mid-glyph instead of at
   a clean line break, which reads as the description overlapping
   the bullets below it. */
.svc-row.is-active .svc-row-desc { max-height: 160px; opacity: 1; margin-top: 12px; }
.svc-row.is-active .svc-row-bullets { max-height: 140px; opacity: 1; margin-top: 12px; }

.svc-preview-wrap { position: sticky; top: calc(var(--nav-offset) + 24px); }
.svc-preview { display: block; height: 500px; background: var(--photo-ground); }
.svc-preview img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 600ms ease; }
.svc-preview img.is-active { opacity: 1; }
.svc-preview-cta {
  display: block; margin-top: 14px; text-align: right;
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: rgb(var(--on-ground) / 0.4);
  transition: color 200ms ease;
}
.svc-preview-wrap:hover .svc-preview-cta { color: var(--gold); }

@media (max-width: 860px) {
  .svc-showcase { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
  .svc-preview-wrap { position: static; order: -1; }
  .svc-preview { height: 320px; }
  .svc-row-title { font-size: clamp(21px, 4.4vw, 26px); }
}

/* ---------------------------------------------------------------
   Testimonials — proper section header + staggered cards, not an
   editorial pull-quote block. Monogram avatars stand in honestly
   for photos we don't actually have of these clients.
--------------------------------------------------------------- */
/* ---------------------------------------------------------------
   Testimonials slider — centered carousel: the active quote sits
   full size up front, the other two peek in scaled down and dimmed
   at its sides. Click a side card, a dot, or an arrow to bring a
   different one forward (see initTestimonialSlider, main.js).
--------------------------------------------------------------- */
.testi-slider { position: relative; overflow: hidden; margin-top: 24px; padding: 16px 0 6px; }
.testi-card {
  position: absolute; top: 0; left: 0; right: 0; margin: 0 auto; z-index: 1;
  width: 400px; max-width: 82vw; height: 380px; overflow: hidden;
  background: var(--ground-raised); border: 1px solid var(--gold);
  padding: 36px 28px 32px; cursor: pointer;
  display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 18px;
  transition: transform 550ms cubic-bezier(.16,1,.3,1), opacity 450ms ease, box-shadow 450ms ease;
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.testi-card::before {
  content: "\201C"; position: absolute; top: -14px; left: 20px; z-index: 0;
  font-family: 'Fraunces', serif; font-style: italic; font-size: 128px;
  color: rgb(var(--ch-teal) / 0.10); line-height: 1; pointer-events: none;
}
.testi-card > * { position: relative; z-index: 1; }
.testi-card.is-active {
  position: relative; z-index: 3; cursor: default;
  box-shadow: 0 34px 64px rgb(var(--ch-ink) / 0.24);
}
.testi-avatar-row { display: flex; align-items: center; gap: 14px; }
.testi-avatar {
  width: 46px; height: 46px; border-radius: 50%; border: 1px solid rgb(var(--ch-gold) / 0.5);
  display: flex; align-items: center; justify-content: center; flex: none;
  font-family: 'Fraunces', serif; font-size: 17px; color: var(--accent-text); background: rgb(var(--ch-teal) / 0.06);
}
.testi-name { font-family: 'Archivo', sans-serif; font-weight: 700; font-size: 15px; color: rgb(var(--on-ground)); }
.testi-role { font-family: 'Archivo', sans-serif; font-size: 12.5px; color: rgb(var(--on-ground) / 0.5); }
.testi-quote {
  align-self: center; overflow: hidden;
  font-family: 'Fraunces', serif; font-style: italic; font-weight: 500;
  font-size: 17px; line-height: 1.55; color: rgb(var(--on-ground) / 0.8); margin: 0;
}

.testi-nav { display: flex; align-items: center; justify-content: center; gap: 24px; margin-top: 8px; }
.testi-arrow {
  all: unset; box-sizing: border-box; cursor: pointer; flex: none;
  width: 46px; height: 46px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgb(var(--on-ground) / 0.35); color: rgb(var(--on-ground));
  transition: border-color 200ms ease, background 200ms ease, color 200ms ease;
}
.testi-arrow:hover { border-color: var(--gold); background: rgb(var(--ch-gold) / 0.12); color: var(--gold); }
.testi-arrow svg { width: 15px; height: 15px; }
.testi-arrow.is-prev svg { transform: scaleX(-1); }
.testi-dots { display: flex; align-items: center; gap: 10px; }
.testi-dot {
  all: unset; box-sizing: border-box; cursor: pointer;
  width: 8px; height: 8px; border-radius: 50%; background: rgb(var(--on-ground) / 0.3);
  transition: background 250ms ease, transform 250ms ease;
}
.testi-dot.is-active { background: var(--gold); transform: scale(1.4); }

@media (max-width: 640px) {
  /* Was a flat width:340px, which is wider than a 328px viewport --
     the card ran off the right edge on small phones. Cap it instead
     so it shrinks with the screen. Height goes auto because a fixed
     400px clipped longer quotes once the card got narrower. */
  .testi-card { width: 100%; max-width: 340px; height: auto; min-height: 340px; padding: 30px 24px 26px; }
}


/* ---------------------------------------------------------------
   Scroll-expand hero — the featured photo starts small and grows
   to full-bleed as you scroll through a pinned section; the same
   sticky-scroll technique already used for the photo gallery, so
   it degrades the same safe way on mobile. No scroll-jacking —
   it just reads native scroll position, so every input method
   (wheel, touch, keyboard, trackpad) works automatically.
--------------------------------------------------------------- */
.hero-expand-wrapper { position: relative; height: 220vh; background: var(--ground-deep); }
.hero-expand-sticky { position: sticky; top: 0; height: 100vh; overflow: hidden; }
.hero-expand-bg { position: absolute; inset: 0; }
.hero-expand-bg img { width: 100%; height: 100%; object-fit: cover; opacity: 0.35; }
.hero-expand-bg::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgb(var(--on-ground) / .5) 0%, rgb(var(--on-ground) / .7) 100%);
}
.hero-expand-media {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 340px; height: 420px; max-width: 96vw; max-height: 88vh;
  box-shadow: 0 30px 80px rgb(var(--ch-shadow) / .45);
}
.hero-expand-media img { width: 100%; height: 100%; object-fit: cover; }
.hero-cycle { position: relative; width: 100%; height: 100%; }
.hero-cycle-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 1600ms ease;
}
.hero-cycle-img.is-active { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .hero-cycle-img { transition: none; } }
.hero-expand-scrim {
  /* Sits over the photo (once it's expanded to full-bleed) and under
     the text — without this, the headline was being read directly
     against the raw photo with nothing guaranteeing contrast. */
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgb(var(--on-ground) / 0) 0%, rgb(var(--on-ground) / .12) 38%, rgb(var(--on-ground) / .8) 100%);
}
.hero-expand-content {
  position: absolute; left: 56px; right: 56px; bottom: 72px; max-width: 760px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 22px;
  opacity: 0; transform: translateY(18px); pointer-events: none;
  transition: opacity 500ms ease, transform 500ms ease;
}
.hero-expand-wrapper.is-expanded .hero-expand-content { opacity: 1; transform: translateY(0); pointer-events: auto; }
.hero-expand-cue {
  position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  background: none; border: none; cursor: pointer; padding: 8px;
}
@media (prefers-reduced-motion: reduce) {
  .hero-expand-media, .hero-expand-content { transition: none; }
}
@media (max-width: 767px) {
  /* Reliability over cleverness on the first thing a phone visitor sees.
     Photo and text stack normally here instead of overlapping, so the
     fixed 100vh/overflow:hidden from the desktop pin must be released —
     otherwise the text block has nowhere to go and gets clipped away. */
  .hero-expand-wrapper { height: auto; }
  .hero-expand-sticky { position: relative; height: auto; overflow: visible; }
  .hero-expand-media { position: relative; top: 0; left: 0; transform: none; width: 100%; height: 64vh; max-width: none; max-height: none; }
  .hero-expand-bg, .hero-expand-cue, .hero-expand-scrim { display: none; }
  .hero-expand-content { position: relative; left: 0; right: 0; bottom: 0; opacity: 1; transform: none; pointer-events: auto; padding: 32px 6vw 48px; }
}

/* ---------------------------------------------------------------
   Scroll-to-top button
--------------------------------------------------------------- */
.scroll-top-btn {
  position: fixed; right: 24px; bottom: 24px; z-index: 60;
  width: 46px; height: 46px; border-radius: 999px;
  /* Always a dark disc with ivory on it. Following the ground turned
     it into a pale disc with ivory text in dark mode; the gold ring is
     what keeps it visible against the dark page. */
  background: rgb(var(--ch-ink) / 0.75); border: 1px solid rgb(var(--ch-gold) / 0.5);
  color: var(--ivory); display: flex; align-items: center; justify-content: center;
  cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity 220ms ease, transform 220ms ease, background 180ms ease, border-color 180ms ease;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.scroll-top-btn.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }
.scroll-top-btn:hover { background: rgb(var(--ch-gold) / 0.18); border-color: var(--gold); }

/* ---------------------------------------------------------------
   Interactive CTA — label slides out, label+arrow slides in from
   the opposite edge. Reserved for the handful of primary CTAs
   (spend the boldness in one place, not on every button).
--------------------------------------------------------------- */
.btn-fx { position: relative; overflow: hidden; }
.btn-fx .btn-fx-a, .btn-fx .btn-fx-b {
  display: inline-flex; align-items: center; gap: 8px;
  transition: transform 320ms cubic-bezier(.2,.7,.2,1), opacity 320ms cubic-bezier(.2,.7,.2,1);
}
.btn-fx .btn-fx-a { transform: translateX(0); opacity: 1; }
.btn-fx .btn-fx-b {
  position: absolute; inset: 0; align-items: center; justify-content: center;
  transform: translateX(18px); opacity: 0;
}
.btn-fx:hover .btn-fx-a { transform: translateX(-18px); opacity: 0; }
.btn-fx:hover .btn-fx-b { transform: translateX(0); opacity: 1; }
.btn-fx svg { width: 15px; height: 15px; flex: none; }
@media (prefers-reduced-motion: reduce) {
  .btn-fx .btn-fx-a, .btn-fx .btn-fx-b { transition: none; }
}

/* ---------------------------------------------------------------
   Hero background photo — a full-bleed image + dark scrim behind a
   hero section's text, for pages whose hero has no illustration of
   its own (Portfolio, Services). Pair with data-no-grade if the
   image sits inside a .bracket (skips the multiply tint — see the
   note by .portfolio-tile above).
--------------------------------------------------------------- */
.hero-bg-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Always ink, never the ground's opposite. This darkens a photo so
   the title on top of it can be read, so flipping it in dark mode
   washed the picture out instead of shading it. */
.hero-bg-scrim { position: absolute; inset: 0; background: rgb(var(--ch-ink) / 0.74); }

/* ---------------------------------------------------------------
   "What We Shoot" cards (Services) — larger bracket-framed photos
   that zoom gently on hover, matching the hover language already
   used on gallery/portfolio tiles.
--------------------------------------------------------------- */
.shoot-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; max-width: 1100px; margin: 0 auto; }
.shoot-photo { height: 320px; background: var(--photo-ground); overflow: hidden; }
.shoot-photo img { width: 100%; height: 100%; object-fit: contain; transition: transform 700ms cubic-bezier(.2,.7,.2,1); }
.shoot-card:hover .shoot-photo img { transform: scale(1.06); }
@media (max-width: 860px) {
  .shoot-grid { grid-template-columns: 1fr; max-width: 460px; }
}

/* ---------------------------------------------------------------
   Rates — a photo alongside each row instead of the plain text-only
   ledger. Deliberately NOT the vertical full-bleed-photo card grid
   tried earlier (three rounds, explicitly rejected in favor of the
   plain list) — this keeps the ledger's actual structure (one row
   per service, price always visible, no hover-reveal) and just adds
   a small real photo to each row rather than replacing the format.
--------------------------------------------------------------- */
.rate-item {
  display: flex; align-items: center; gap: 24px;
  padding: 20px 10px; text-decoration: none;
  border-bottom: 1px solid rgb(var(--on-ground) / 0.18);
  transition: background 280ms ease, padding-left 280ms cubic-bezier(.2,.7,.2,1);
}
.rate-item:first-child { border-top: 1px solid rgb(var(--on-ground) / 0.18); }
.rate-item:hover, .rate-item:focus-visible {
  background: rgb(var(--on-ground) / 0.05);
  padding-left: 18px;
  text-decoration: none;
}
.rate-item:hover *, .rate-item:focus-visible * { text-decoration: none; }
.rate-item-photo {
  flex: none; display: block; width: 88px; height: 88px; overflow: hidden;
  border-radius: 6px; border: 1px solid rgb(var(--ch-gold) / 0.4);
}
.rate-item-photo img { width: 100%; height: 100%; object-fit: contain; transition: transform 500ms ease; }
.rate-item:hover .rate-item-photo img, .rate-item:focus-visible .rate-item-photo img { transform: scale(1.08); }
.rate-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.rate-item-top { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.rate-item-name { font-family: 'Fraunces', serif; font-weight: 600; font-size: 20px; color: rgb(var(--on-ground)); }
.rate-item-price { font-family: 'Fraunces', serif; font-weight: 600; font-size: 20px; color: var(--gold); flex: none; font-variant-numeric: tabular-nums; }
.rate-item-duration {
  display: block; font-family: 'IBM Plex Mono', monospace; font-size: 10.5px;
  letter-spacing: 0.08em; text-transform: uppercase; color: rgb(var(--ch-gold) / 0.85);
}
.rate-item-desc { display: block; font-family: 'Archivo', sans-serif; font-size: 13.5px; line-height: 1.5; color: rgb(var(--on-ground) / 0.6); margin-top: 2px; }
@media (max-width: 640px) {
  .rate-item { gap: 16px; padding: 16px 6px; }
  .rate-item-photo { width: 60px; height: 60px; }
  .rate-item-name, .rate-item-price { font-size: 16.5px; }
  .rate-item-desc { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .rate-item, .rate-item-photo img { transition: none; }
}

/* ---------------------------------------------------------------
   Closing CTA band — a giant faint watermark word behind the
   content, the same oversized-type texture already used elsewhere
   on the site (VISTA, the "04" step number, the quote mark),
   instead of plain centered text on a flat color.
--------------------------------------------------------------- */
.cta-band { position: relative; overflow: hidden; padding: 130px 8vw; text-align: center; }
.cta-watermark {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  font-family: 'Cinzel', serif; font-size: min(26vw, 380px); letter-spacing: 0.02em;
  white-space: nowrap; pointer-events: none; line-height: 1; z-index: 0;
}
.cta-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 20px; }

/* ============================================================
   ACF BLOCK WRAPPERS — added for the WordPress build. The static
   site wrote each section's background/padding as an inline style
   since every page had exactly one of these; as reusable blocks
   they get real classes instead.
   ============================================================ */
/* Background/text color come from whichever .section-marble/-teal/-ink/
   -deep-teal class the block adds alongside this one (its "Background"
   control in the editor) -- this class is layout-only now. */
.vv-rates-section { padding: 130px 8vw; position: relative; overflow: hidden; }
.vv-rates-watermark { position: absolute; right: -3vw; top: -7vw; font-family: 'Fraunces', serif; font-style: italic; font-size: 380px; color: rgb(var(--on-ground) / 0.05); line-height: 1; pointer-events: none; }
/* .section-head is a generic centered section intro (badge/eyebrow
   + heading, optional subhead) reused by the Rates/FAQ/Testimonials
   blocks and by any hand-built section (e.g. "What We Shoot"). It
   deliberately sets no color of its own — it inherits from whichever
   .section-marble/.section-teal/.section-ink wraps it, so the same
   markup reads correctly on light or dark backgrounds. */
.section-head { position: relative; text-align: center; max-width: 640px; margin: 0 auto 12px; }
.section-head + .section-head { margin-bottom: 56px; }
.section-head h2 { font-size: clamp(28px, 3.2vw, 44px); margin: 0; }
.section-head p { font-family: 'Archivo', sans-serif; font-size: 16px; margin: 0; opacity: 0.7; }
.vv-rates-list { position: relative; max-width: 820px; margin: 0 auto; }
.vv-rates-cta { text-align: center; margin-top: 56px; }
.vv-rates-cta p { margin: 14px 0 0; font-family: 'Archivo', sans-serif; font-size: 13px; opacity: 0.6; }

/* The FAQ block is reused on a teal section (Services) and a plain
   marble section (Home) with the exact same items — .faq-list carries
   the skin-neutral layout/typography, .is-on-teal adds only the
   ivory-border/gold-icon overrides the base (marble-friendly) rules
   don't already cover. */
/* Background/text color come from the block's own "Background"
   control (.section-marble/-teal/-ink/-deep-teal) -- layout-only here. */
.vv-faq-section { padding: 130px 8vw; text-align: center; }
.faq-list { max-width: 820px; margin: 0 auto; text-align: left; display: flex; flex-direction: column; }
.faq-list .faq-q h4 { font-size: clamp(19px, 2vw, 25px); margin: 0; }
.faq-list .faq-a p { font-family: 'Archivo', sans-serif; font-size: 16px; line-height: 1.7; max-width: 60ch; margin: 14px 0 0; opacity: 0.85; }
.faq-list.is-on-teal .faq-row { border-top-color: rgb(var(--on-ground) / 0.18); }
.faq-list.is-on-teal .faq-row:last-child { border-bottom: 1px solid rgb(var(--on-ground) / 0.18); }

/* Background/text color come from the block's own "Background"
   control (.section-marble/-teal/-ink/-deep-teal) -- layout-only here. */
.vv-testi-section { padding: 130px 8vw; }

/* Booking page's "how it works" bullet list (a real wp:list block
   with this className, standing in for the static build's hand-rolled
   flex rows with a small gold dot). */
.vv-dot-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.vv-dot-list li { position: relative; padding-left: 20px; font-family: 'Archivo', sans-serif; font-size: 14.5px; line-height: 1.5; color: rgb(var(--on-ground) / 0.7); }
.vv-dot-list li::before { content: ""; position: absolute; left: 0; top: 8px; width: 6px; height: 6px; border-radius: 50%; background: var(--gold); }

/* ============================================================
   SECTION SKINS — every full-bleed section declares its own
   background + text color explicitly through one of these three,
   instead of relying on inherited defaults (which flip per page)
   or a one-off inline background/color pair.
   ============================================================ */
/* Each ground names its own opposite once. Everything inside reads it
   through --on-ground, so a border rule written once is correct on all
   four, and correct again in dark mode without being touched. */
.section-marble { background: var(--ground); color: rgb(var(--on-light)); --on-ground: var(--on-light); }
.section-teal { background: var(--ground-mid); color: rgb(var(--on-dark)); --on-ground: var(--on-dark); }
.section-ink { background: var(--ground-deep); color: rgb(var(--on-dark)); --on-ground: var(--on-dark); }

/* ============================================================
   PAGE HERO — shared interior-page hero (Services/Booking/Contact).
   The static build hand-tuned each page's hero text size/color as
   an inline style; this consolidates that into one component.
   ============================================================ */
.page-hero { position: relative; overflow: hidden; padding: 180px 8vw 90px; text-align: center; }
.page-hero.is-plain { padding: 180px 8vw; }
.page-hero-inner { position: relative; }
.page-hero-title { margin: 16px auto 0; font-size: clamp(38px, 6vw, 78px); line-height: 1.08; max-width: 780px; }
.page-hero-sub { margin: 22px auto 0; font-style: italic; font-size: clamp(18px, 2vw, 24px); opacity: 0.7; max-width: 600px; }

/* Flush left, for the pages the approved design sets that way:
   Portfolio, Services, Privacy, Terms and Sitemap. Booking and Contact
   stay centred. The auto side margins have to go as well as the text
   alignment, or the heading block stays centred in the column while
   its text hugs the left of it. */
.page-hero.is-left { text-align: left; }
.page-hero.is-left .page-hero-title { margin-left: 0; margin-right: 0; max-width: 900px; }
.page-hero.is-left .page-hero-sub { margin-left: 0; margin-right: 0; max-width: 640px; }

/* ============================================================
   MONO LABELS — the small caps/teal eyebrow-style tag used above
   contact details, form panels, etc. (".mono-label"), and its
   quieter/muted variant for secondary prompts.
   ============================================================ */
.mono-label { font-family: 'IBM Plex Mono', monospace; font-size: 11px; letter-spacing: 0.14em; color: var(--accent-text); text-transform: uppercase; }
.mono-label-muted { font-family: 'IBM Plex Mono', monospace; font-size: 11px; letter-spacing: 0.06em; color: rgb(var(--on-ground) / 0.4); text-transform: uppercase; }
.section-ink .mono-label-muted, .section-teal .mono-label-muted { color: rgb(var(--on-ground) / 0.5); }

/* ============================================================
   CTA BAND — background/text color comes from whichever
   .section-marble/-teal/-ink/-deep-teal class the block adds
   alongside this one (its "Background" control in the editor),
   not from .cta-band itself.
   ============================================================ */
.cta-watermark { color: rgb(var(--on-ground) / 0.05); }
.cta-contact-row { display: flex; align-items: center; gap: 40px; flex-wrap: wrap; justify-content: center; }
.cta-contact-item { display: flex; flex-direction: column; gap: 10px; align-items: center; }
.cta-contact-divider { width: 1px; height: 52px; background: rgb(var(--ch-gold) / 0.28); }
.cta-contact-value { font-family: 'Fraunces', serif; font-weight: 600; font-size: clamp(20px, 2.6vw, 36px); }

/* Shared "small caption line with a top border" used to close out
   both the About Brendan bio and the homepage's closing CTA. */
.award-line { display: flex; align-items: center; gap: 10px; padding-top: 18px; margin-top: 4px; border-top: 1px solid rgb(var(--ch-gold) / 0.22); }
.award-line span { font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.5; }

/* ============================================================
   CONTACT CARDS — Call / Email / Instagram row on the Contact page.
   ============================================================ */
.hero-card-label { font-family: 'IBM Plex Mono', monospace; font-size: 11px; letter-spacing: 0.14em; color: var(--accent-text); text-transform: uppercase; }
.hero-card-value { font-family: 'Fraunces', serif; font-weight: 600; font-size: 22px; color: rgb(var(--on-ground)); word-break: break-word; }
.hero-card-desc { font-family: 'Archivo', sans-serif; font-size: 14px; color: rgb(var(--on-ground) / 0.6); }
.hero-card { text-decoration: none; align-items: flex-start; }

/* ============================================================
   ABOUT BRENDAN — bio copy paragraphs (Services page).
   ============================================================ */
.prose { font-family: 'Archivo', sans-serif; font-size: 17px; line-height: 1.75; opacity: 0.85; margin: 0; max-width: 52ch; }

/* ============================================================
   WHAT WE SHOOT — the three service-type cards (Services page).
   ============================================================ */
.shoot-card-title { font-family: 'Fraunces', serif; font-weight: 600; font-size: 21px; color: rgb(var(--on-ground)); margin: 18px 0 8px; }
.shoot-card-desc { font-family: 'Archivo', sans-serif; font-size: 14.5px; line-height: 1.6; color: rgb(var(--on-ground) / 0.65); margin: 0 0 14px; }
.shoot-card-link { font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 13.5px; color: var(--accent-text); }

/* Generic vertical rhythm for hand-built full-bleed content sections
   (About Brendan, What We Shoot, Booking's two-column panel, Contact's
   cards) — pairs with .section-marble/.section-teal/.section-ink. */
.section-pad { padding: 130px 8vw; position: relative; overflow: hidden; }

/* Generic vertical-stack layout primitive (flex column + gap),
   used wherever a hand-built section needs one without a one-off
   inline flex declaration. */
.stack { display: flex; flex-direction: column; gap: 20px; }

.cta-heading { font-style: italic; max-width: 680px; margin: 0 auto; }
.cta-links { display: flex; gap: 28px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
.cta-links a { font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 13.5px; color: var(--gold); }

.vv-contact-cta { text-align: center; margin-top: 64px; display: flex; flex-direction: column; align-items: center; gap: 18px; }

/* A muted footnote paragraph with a top border — "prefer to talk it
   through first", or any similar secondary prompt + link row. */
.footnote-block { display: flex; flex-direction: column; gap: 10px; padding-top: 22px; margin-top: 4px; border-top: 1px solid var(--hairline); }
.footnote-links { display: flex; gap: 24px; flex-wrap: wrap; }
.footnote-links a { font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 14px; color: var(--accent-text); }

/* Setmore's embedded scheduling widget, given room to sit inside
   our bracket-framed panel instead of looking like a stray link
   (moved here from a page-level <style> tag in the static build). */
.setmore-panel { display: flex; flex-direction: column; align-items: center; gap: 24px; padding: 64px 40px; text-align: center; }
#Setmore_button_iframe { font-family: 'Archivo', sans-serif !important; filter: unset !important; }

/* Badge/icon color overrides for the ACF-block sections on a dark
   ground (the base .hero2-badge/.faq-icon rules assume marble). Keyed
   off the ground class itself, not a specific section, so any of the
   three dark options (Teal/Ink/Deep Teal) picks this up correctly. */
.section-teal .hero2-badge, .section-ink .hero2-badge, .section-deep-teal .hero2-badge { color: var(--ivory); border-color: rgb(var(--ch-gold) / 0.55); }
.faq-list.is-on-teal .faq-icon span { background: var(--gold); }

/* Empty-state prompt shown in the admin/preview before any Rates/
   FAQ/Testimonial items exist yet. */
.vv-empty-state { text-align: center; opacity: 0.6; font-family: 'Archivo', sans-serif; }

/* Footer's rights line — muted ivory, not full-opacity body text. */
.footer-copyright { color: rgb(var(--on-ground) / 0.5); }

/* ============================================================
   HOME PAGE — remaining sections ported from the static build's
   index.html (Vision, Photo Gallery header, Marquee wrapper). The
   underlying component classes (.vision-duo*, .gallery-*, .svc-*,
   .marquee-track/-tile/-caption, .booking-ledger-*, .input) already
   exist above from the full stylesheet port — these are just the
   handful of one-off section styles the static build wrote inline.
   ============================================================ */
.vision-quote-mark { position: absolute; right: 6vw; top: 56px; font-family: 'Fraunces', serif; font-style: italic; font-size: 180px; color: rgb(var(--ch-teal) / 0.08); line-height: 1; pointer-events: none; }

.gallery-header { padding: 120px 40px 48px; display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; flex-wrap: wrap; }
.gallery-header-text { display: flex; flex-direction: column; gap: 14px; }
.gallery-scroll-hint { display: flex; align-items: center; gap: 10px; }
.gallery-scroll-hint span:first-child { font-size: 12px; color: rgb(var(--on-ground) / 0.45); letter-spacing: 0.06em; }
.gallery-scroll-hint span:last-child { font-size: 14px; color: var(--gold); }

/* Background/text color come from the block's own "Background" control
   (.section-marble/-teal/-ink/-deep-teal) -- layout-only here. (This
   used to hardcode `background: var(--paper)`, a variable that was
   never actually defined -- WP's preset color is
   --wp--preset--color--paper -- so it silently fell back to
   transparent and let the dark page background show through the
   tilted strip's corner gaps. Folding this into the same ground
   system as every other section both fixes that and makes it
   editable.) */
.marquee-section { padding: 60px 0; overflow: hidden; position: relative; }
.marquee-strip { transform: rotate(-1.4deg) scale(1.06); border-top: 2px solid var(--gold); border-bottom: 2px solid var(--gold); }

/* Home's "Book your session" inline-form panel — a lighter mirror
   of the Booking page's bracket panel, sitting on marble inside a
   teal section instead. */
.inline-form-panel { position: relative; background: var(--ground-raised); padding: 48px 44px;
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.inline-form-watermark { position: absolute; left: -4vw; bottom: -10vw; font-family: 'Fraunces', serif; font-style: italic; font-size: 260px; color: rgb(var(--ch-marble) / 0.05); line-height: 1; pointer-events: none; }
.inline-form-confirm { display: none; flex-direction: column; gap: 10px; align-items: flex-start; min-height: 280px; justify-content: center; }
.inline-form-confirm.is-visible { display: flex; }
.inline-form form { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.inline-form form.is-hidden { display: none; }
.inline-form .btn { align-self: flex-start; }

.section-deep-teal { background: var(--ground-gallery); color: rgb(var(--on-dark)); --on-ground: var(--on-dark); }
.inline-form-confirm p { font-size: 26px; color: rgb(var(--on-ground)); margin: 0; }

/* Locks background scroll while the mobile nav overlay is open
   (toggled via JS classList, not element.style, to keep inline
   styles out of the DOM entirely). */
body.nav-open { overflow: hidden; }

.site-footer .brand { display: flex; align-items: center; }

/* ============================================================
   PROSE SECTION — Privacy / Terms long copy.
   Background/text color come from the block's own "Background"
   control (.section-marble/-teal/-ink/-deep-teal); layout only here.
   The inner headings and paragraphs are real core blocks, so these
   rules target the elements core outputs rather than custom classes.
   ============================================================ */
.vv-prose-section { padding: 0 8vw 130px; }

/* Plain block flow with real margins, NOT flex + gap.
   This was flex with gap:32px, plus a -20px margin pulling each
   paragraph up toward its heading. On the front end the gap absorbed
   that. In the editor it does not: Gutenberg renders inner blocks
   inside its own wrapper, so the headings and paragraphs are no longer
   direct flex children and get no gap at all -- the -20px then landed
   as pure overlap and the headings sat on top of the text. Margins
   behave identically in both places, so they are what this uses. */
/* Flush left, not centred.

   max-width stays: a 70ch measure is what makes long legal copy
   readable, and stretching it across a wide monitor would undo that.
   What goes is the auto side margins, which were centring that column
   inside the section while the page heading above it sits hard left
   (.page-hero.is-left, set on Privacy and Terms). The heading started
   at the left edge and the text under it started somewhere in the
   middle, so the two never lined up.

   The section's own 8vw padding is the left edge now, which is the
   same one the heading uses. */
.legal-prose {
	max-width: 70ch;
	margin: 0;
	font-family: 'Archivo', sans-serif;
	font-size: 16px;
	line-height: 1.8;
}
/* Opacity rather than a hard color, so the same copy stays legible
   whichever section skin the block is set to. */
.legal-prose p { margin: 0 0 20px; opacity: 0.78; }
/* Bigger space above a heading than below it, so each heading reads as
   belonging to the paragraph that follows rather than floating between
   two of them. */
.legal-prose .wp-block-heading { font-size: 22px; margin: 40px 0 12px; }
.legal-prose .wp-block-heading:first-child { margin-top: 0; }
.legal-prose > *:last-child { margin-bottom: 0; }
.legal-prose .legal-meta { font-size: 12px; letter-spacing: 0.04em; opacity: 0.45; margin: 0 0 30px; }
.legal-prose .legal-intro { margin: 0 0 38px; }

/* ============================================================
   SITEMAP GRID
   ============================================================ */
.vv-sitemap-section { padding: 0 8vw 130px; }
.vv-sitemap-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
	gap: 24px;
	max-width: 900px;
	margin: 0 auto 60px;
}
.vv-sitemap-legal { max-width: 900px; margin: 0 auto; }
.vv-legal-links { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 16px; }
.vv-legal-link {
	text-decoration: none;
	font-family: 'Fraunces', serif;
	font-weight: 600;
	font-size: 17px;
	color: inherit;
	padding: 14px 22px;
	border: 1px solid rgb(var(--on-ground) / 0.15);
	transition: border-color 250ms ease, background 250ms ease;
}
.vv-legal-link:hover { border-color: var(--gold); background: rgb(var(--ch-gold) / 0.06); }
.section-teal .vv-legal-link, .section-ink .vv-legal-link, .section-deep-teal .vv-legal-link { border-color: rgb(var(--on-ground) / 0.2); }

/* ============================================================
   404 SCREEN — full viewport, centered.
   ============================================================ */
.vv-404 {
	position: relative;
	min-height: 100vh;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: 120px 8vw;
}
.vv-404-title {
	margin: 20px 0 0;
	font-style: italic;
	font-size: clamp(38px, 6vw, 80px);
	line-height: 1.1;
	max-width: 760px;
}
.vv-404-body {
	margin: 22px 0 0;
	font-family: 'Archivo', sans-serif;
	font-size: 16px;
	line-height: 1.7;
	opacity: 0.65;
	max-width: 46ch;
}
.vv-404-actions {
	display: flex;
	gap: 24px;
	margin-top: 36px;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
}
.vv-404-link {
	font-family: 'Archivo', sans-serif;
	font-weight: 600;
	font-size: 14px;
	color: inherit;
	text-decoration: none;
}
.vv-404-link:hover { color: var(--gold); }

/* ============================================================
   MOBILE FIXES — 15 Sep
   Reported from a real 328px viewport. Each block below fixes a
   specific overflow or cramped layout, not a general tidy-up.
   ============================================================ */

/* ---- Nav: the burger was being pushed outside the pill ----
   .site-nav is a fixed bar with both left and right pinned, so its
   width is fixed — but .brand was `flex: none`, so the logo and
   wordmark never gave up any space. Below ~400px the wordmark, the
   CTA and the burger together are wider than the bar, and since
   nothing could shrink, the burger simply overflowed off-screen and
   became unreachable. Letting the brand shrink fixes it; the other
   two stay at their natural size on purpose, because a clipped
   button or a half burger is worse than a truncated wordmark. */
@media (max-width: 640px) {
  .site-nav { gap: 8px; }
  .nav-burger { width: 32px; height: 32px; gap: 4px; margin-left: 2px; }
  .nav-burger span { width: 17px; }
  /* .nav-link is already hidden here, but the empty .links flex item
     stayed in the row and space-between handed it a share of the gap,
     so the CTA drifted toward the middle instead of sitting with the
     burger. Drop the container and push the pair right as one group. */
  .site-nav .links { display: none; }
  .theme-toggle { margin-left: auto; }
  .site-nav .btn-sm { margin-left: 0; }
  .nav-burger { margin-left: 0; }
  .site-nav .brand { flex: 0 1 auto; min-width: 0; gap: 8px; }
  .site-nav .brand-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .site-nav .btn-sm { flex: none; }
  .nav-burger { flex: none; }
}
@media (max-width: 400px) {
  /* Below this the emblem alone carries the brand. Truncating to
     "Verity Vis…" looks like a bug; dropping the wordmark reads as
     a deliberate compact state. */
  .site-nav .brand-name { display: none; }
}

/* ---- Footer: the link row ran off the right edge ----
   .site-footer .links had no flex-wrap, so four links sat on one
   line and "Contact" was cut off past the viewport. The phone number
   then broke mid-number across three lines because the anchor itself
   was allowed to wrap. Wrap the row, never the individual link. */
@media (max-width: 860px) {
  .site-footer { flex-direction: column; align-items: flex-start; }
  .site-footer .links { flex-wrap: wrap; gap: 14px 22px; }
}
@media (max-width: 640px) {
  .site-footer { padding: 44px 8vw; gap: 18px; }
  .site-footer .links { gap: 12px 18px; }
  .site-footer .links a { white-space: nowrap; font-size: 12.5px; }
  .site-footer .rights { font-size: 11.5px; line-height: 1.8; }
}

/* ---- Rates: price moved below the service name on mobile ----
   Side by side, a price like "$100 to $500" took so much of the row
   that the name had almost nothing left and the price wrapped
   awkwardly. Stacked, each gets a full line. */
@media (max-width: 640px) {
  .rate-item-top { flex-direction: column; align-items: flex-start; gap: 2px; }
  .rate-item-price { font-size: 18px; }
  .rate-item-name { font-size: 17px; line-height: 1.25; }
}

/* ---- Gallery: the photos sat further in than their own heading ----
   At this breakpoint the section itself already gets side padding from
   the `section { padding-left/right: 6vw !important }` rule further up,
   so anything these two add is stacked ON TOP of it: the header was
   landing at 6vw + 8vw and the photo track at 6vw + 40px. That
   mismatch is the indent visible on the photos. Both now drop their
   own side padding and inherit the section's, so heading and photos
   share one left edge. */
@media (max-width: 860px) {
  .gallery-header { padding: 84px 0 32px; }
  .gallery-track { padding-left: 0; padding-right: 0; }
}
@media (max-width: 640px) {
  .gallery-header { padding: 64px 0 24px; gap: 14px; }
}

/* ---- Section rhythm on phones ----
   130px of vertical padding is roughly a fifth of a phone screen at
   each end of every section. Tightened so a section's content, not
   its whitespace, is what fills the viewport. */
@media (max-width: 640px) {
  .section-pad,
  .vv-rates-section,
  .vv-faq-section,
  .vv-testi-section { padding-top: 84px; padding-bottom: 84px; }
  .vv-prose-section,
  .vv-sitemap-section { padding-bottom: 84px; }
  .page-hero { padding: 132px 8vw 56px; }
  .page-hero.is-plain { padding: 132px 8vw 84px; }
}

/* ---- Hero: less dead space above the headline on phones ----
   .hero2 is a 100vh flex column that already centres its content, so
   a full --nav-offset on top pushed the headline below true centre
   and left an empty band under the nav bar. */
@media (max-width: 640px) {
  .hero2 { padding-top: 64px; }
}
@media (max-width: 420px) {
  .hero2 { padding-top: 52px; }
}

/* ============================================================
   CONTACT FORM (Contact Form 7)
   CF7's own stylesheet is dequeued (see inc/contact-form.php), so
   everything the form looks like comes from here. Selectors target
   our own wrapper classes wherever possible rather than CF7's
   internals, which change between plugin versions.
   ============================================================ */
/* No .bracket here on purpose. That class is the photography frame --
   it draws the gold corner rules AND a dark multiply grade meant to
   unify photos shot on different days. Over a form that grade read as
   a grey wash across every field, and the corner rules read as a
   stray black border. A form panel gets a plain lifted card instead. */
/* The Contact page already has a hero and the contact cards above
   this, each with their own full section padding -- a third stacked
   gap read as a dead band. The section keeps its bottom padding. */
.vv-form-section { padding-top: 0; }

.vv-form-panel {
  position: relative;
  background: var(--ground-raised);
  padding: 44px 40px;
  border: 1px solid rgb(var(--on-ground) / 0.07);
  box-shadow: 0 20px 44px -22px rgb(var(--ch-ink) / 0.16);
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}

.vv-field { display: flex; flex-direction: column; margin-bottom: 26px; }
.vv-field label { margin-bottom: 12px; }
.vv-field label {
  font-family: 'IBM Plex Mono', monospace; font-size: 11px;
  letter-spacing: 0.12em; text-transform: uppercase; color: rgb(var(--on-ground) / 0.5);
}
/* CF7 wraps every control in a <span class="wpcf7-form-control-wrap">,
   which is inline by default and would collapse the field width. */
.vv-field .wpcf7-form-control-wrap { display: block; width: 100%; }
.vv-field input[type="text"],
.vv-field input[type="email"],
.vv-field textarea {
  width: 100%; box-sizing: border-box;
  font-family: 'Archivo', sans-serif; font-size: 15px; color: rgb(var(--on-ground));
  background: var(--field-ground);
  border: 1px solid rgb(var(--on-ground) / 0.15);
  border-radius: 999px; padding: 14px 22px;
  transition: border-color 200ms ease, background 200ms ease;
  /* Same rule: the field paints its own ground. */
  --on-ground: var(--on-light);
}
/* A pill is right for one-line fields and wrong for a multi-line one --
   a rounded textarea pushes its first and last lines inward. */
.vv-field textarea { border-radius: 18px; resize: vertical; min-height: 140px; line-height: 1.6; }
.vv-field input:focus, .vv-field textarea:focus {
  outline: none; border-color: var(--accent-text); background: var(--field-ground-focus);
}
.vv-field input::placeholder, .vv-field textarea::placeholder { color: rgb(var(--on-ground) / 0.35); }

.vv-submit { margin-top: 24px; }
.vv-submit input[type="submit"], .vv-submit .btn {
  font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 14px;
  background: var(--teal); color: var(--ivory);
  border: none; border-radius: 999px; padding: 15px 34px; cursor: pointer;
  transition: background 200ms ease;
}
.vv-submit input[type="submit"]:hover { background: var(--teal-hover); }

/* Honeypot: moved off-screen rather than display:none, because some
   bots skip fields that are explicitly hidden.
   An empty bordered box was showing above the first real field. Two
   things could produce it -- this honeypot escaping its wrapper, or
   CF7's always-rendered response container picking up the border below
   -- so both are pinned down rather than guessing which one it was.
   CF7 wraps every control in its own <span>, so the wrapper and the
   input are covered here too, not just the outer div. */
.vv-hp,
.vv-hp .wpcf7-form-control-wrap,
.vv-hp input {
  position: absolute !important;
  left: -9999px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  border: 0 !important;
  opacity: 0 !important;
  overflow: hidden !important;
  pointer-events: none !important;
}

/* Keeping the response container hidden until a submit is CF7's own
   `form.init .wpcf7-response-output { display: none }`, so there is
   nothing to add here. */

/* CF7 validation + response messages.

   The per-field "This one is required." lines and the "Please check the
   highlighted fields" banner are hidden: four separate strings of red
   text for one empty form is a lot of noise on a three-field form.
   The red border below is kept deliberately so an invalid field is
   still obvious -- feedback without the wall of text. The success
   message is NOT hidden; it is the only confirmation the visitor gets
   that the message actually sent. */
.wpcf7-not-valid-tip { display: none !important; }

.vv-field input.wpcf7-not-valid,
.vv-field textarea.wpcf7-not-valid {
  border-color: #b3261e;
  background: rgb(var(--ch-error) / 0.04);
}
.wpcf7-response-output {
  font-family: 'Archivo', sans-serif; font-size: 14px; line-height: 1.6;
  margin: 22px 0 0 !important; padding: 14px 18px !important;
  border: 1px solid rgb(var(--on-ground) / 0.15) !important; border-radius: 12px;
}
.wpcf7 form.sent .wpcf7-response-output { border-color: var(--accent-text) !important; color: var(--accent-text); }
/* Validation banner hidden (the red field borders already say it);
   a real send failure still speaks up, since that one the visitor
   cannot see for themselves. */
.wpcf7 form.invalid .wpcf7-response-output { display: none !important; }
.wpcf7 form.failed .wpcf7-response-output,
.wpcf7 form.spam .wpcf7-response-output { border-color: #b3261e !important; color: #b3261e; }
.wpcf7-spinner { margin-left: 12px; }

@media (max-width: 640px) {
  .vv-form-panel { padding: 30px 22px; }
  .vv-field input[type="text"], .vv-field input[type="email"] { padding: 13px 18px; }
  .vv-field textarea { padding: 14px 18px; min-height: 120px; }
  .vv-submit input[type="submit"] { width: 100%; }
}

/* ============================================================
   SETMORE EMBED
   Setmore's booking page rendered inline, not behind a popup
   button. See blocks/booking-panel/render.php for why.
   ============================================================ */
.setmore-embed {
  position: relative;
  width: 100%;
  /* Setmore's flow is tall (service list, then calendar, then a form),
     and an iframe cannot size itself to its content across origins, so
     the height has to be set here. Tuned so the calendar step fits
     without the visitor scrolling inside the frame. */
  height: 720px;
  border: 1px solid rgb(var(--on-ground) / 0.1);
  border-radius: 14px;
  overflow: hidden;
  background: var(--ground-raised);
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.setmore-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
.setmore-fallback {
  margin: 14px 0 0;
  font-family: 'Archivo', sans-serif;
  font-size: 12.5px;
  opacity: 0.55;
  text-align: center;
}
.setmore-fallback a { color: inherit; text-decoration: underline; }
.setmore-fallback a:hover { color: var(--gold); }

@media (max-width: 860px) {
  .setmore-embed { height: 640px; }
}
@media (max-width: 640px) {
  /* On a phone the frame is the whole point of the page, so give it
     most of the viewport rather than a fixed box the visitor has to
     scroll inside twice. */
  .setmore-embed { height: 78vh; min-height: 560px; border-radius: 10px; }
}

/* Booking page layout: intro copy above, calendar full width below.
   See blocks/booking-panel/render.php for why this is not a split. */
.booking-layout { display: flex; flex-direction: column; gap: 48px; }
/* Tighter once the intro is two columns: the gap was sized for a tall
   single column of text and is just dead space under a short one. */
@media (min-width: 900px) { .booking-layout { gap: 32px; } }
/* The intro used the full width of the page but only 70ch of it, so it
   ran to 449px tall and pushed the calendar to y=1164 on a 900px
   screen. The booking page opened on a wall of text with no sign that
   there was anything to book.

   Not simply unconstrained: at 1400px a paragraph becomes one 200
   character line, which is where prose stops being readable. Two
   columns use the width the page already has, keep each line to a
   sensible measure, and roughly halve the height, which is what
   actually brings "Live scheduling" above the fold. */
.booking-intro { max-width: none; }

@media (min-width: 900px) {
  .booking-intro {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "eyebrow  list"
      "heading  list"
      "body     list"
      "body     footnote";
    align-content: start;
    gap: 10px 56px;
  }
  .booking-intro > .mono-label { grid-area: eyebrow; }
  .booking-intro > .display { grid-area: heading; }
  .booking-intro > .prose { grid-area: body; }
  .booking-intro > .vv-dot-list { grid-area: list; align-self: start; }
  .booking-intro > .footnote-block { grid-area: footnote; align-self: end; }

  /* One column each now that the list has half the page rather than
     the whole of it. */
  .booking-intro .vv-dot-list { grid-template-columns: 1fr; }
}
/* The intro's bullets and contact links read better side by side than
   stacked once they have the full width to sit in. */
.booking-intro .vv-dot-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px 28px; }
.booking-panel-wide { width: 100%; }
.booking-panel-wide .setmore-panel { padding: 40px 32px; }
/* Full width means the frame can be shorter without cramping: Setmore
   lays its service list out in columns when it has the room. */
.booking-panel-wide .setmore-embed { height: 640px; }

@media (max-width: 860px) {
  .booking-layout { gap: 36px; }
  .booking-panel-wide .setmore-panel { padding: 32px 22px; }
  .booking-panel-wide .setmore-embed { height: 620px; }
}
@media (max-width: 640px) {
  .booking-intro .vv-dot-list { grid-template-columns: 1fr; }
  .booking-panel-wide .setmore-panel { padding: 24px 14px; }
  .booking-panel-wide .setmore-embed { height: 78vh; min-height: 560px; }
}

/* ============================================================
   PORTFOLIO GRID
   Justified rows: every photo in a row shares one height, widths
   differ by shape, the row fills the width exactly. Nothing is
   cropped and nothing is letterboxed.
   The whole mechanism is flex-grow set per tile to that photo's
   aspect ratio (see blocks/portfolio-grid/render.php), so widths
   distribute in proportion to shape and the heights fall out equal.
   ============================================================ */
.vv-portfolio-section { padding: 0 var(--vv-gutter) 100px; }

/* The filter bar is a full-bleed sticky band in the approved design,
   sitting directly under the nav rather than inside the content
   column. In the block it has to live inside this section, which
   already insets its children by 8vw, so the bar was being indented
   twice: 8vw from the section and 8vw of its own padding. That is why
   the chips started well to the right of the "showing" line below
   them. Pulling it back out by the section's own inset puts the band
   edge to edge again and leaves it with the single 8vw it was
   designed with. */
.vv-portfolio-section > .portfolio-filter-bar { margin: 0 calc(-1 * var(--vv-gutter)) 56px; }

.portfolio-count-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 0 0 24px; margin-bottom: 32px;
  border-bottom: 1px solid rgb(var(--on-ground) / 0.14);
}
.portfolio-count-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--gold); flex: none; }
.portfolio-count-bar .mono { font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; opacity: 0.62; }

.portfolio-rows { display: flex; flex-direction: column; gap: 16px; }
.portfolio-row { display: flex; gap: 16px; align-items: flex-start; }

/* A row that did not fill up must NOT stretch its photos across the
   full width: two landscapes blown up to fill a three-wide row look
   like a mistake next to the full rows above and below. flex-grow
   carries the ratio, so cap the row's own width by how many it
   actually holds. */
/* data-count rather than :has(). :has() would work in current
   browsers, but the count is already known when the row is built in
   both PHP and JS, so an attribute is the cheaper and more portable
   way to say the same thing.

   Read against data-per-row on the container, because "did this row
   fill up" depends on the row size in force, and that changes with the
   viewport: two photos is a short row at three across and a full one
   at two across. Rows are packed to capacity now, so in practice only
   the last row of a category is ever short. */
.portfolio-rows[data-per-row="3"] .portfolio-row[data-count="1"] { max-width: 34%; }
.portfolio-rows[data-per-row="3"] .portfolio-row[data-count="2"] { max-width: 67%; }
.portfolio-rows[data-per-row="2"] .portfolio-row[data-count="1"] { max-width: 50%; }
.portfolio-rows[data-per-row="4"] .portfolio-row[data-count="1"] { max-width: 25%; }
.portfolio-rows[data-per-row="4"] .portfolio-row[data-count="2"] { max-width: 50%; }
.portfolio-rows[data-per-row="4"] .portfolio-row[data-count="3"] { max-width: 75%; }

/* The aspect-ratio is set inline per tile from the real photo, which
   overrides these. They stay as the fallback for a photo whose
   dimensions were never stored. */
/* margin: 0 matters more than it looks. A <figure> carries a browser
   default of 1em 40px, and nothing here ever reset it, so every tile
   was spending 80px of its row on margin. Three to a row that is 240px
   gone: on a 384px phone the photos came out 22px wide, and even on a
   desktop they were about a quarter narrower than the row they were
   supposed to justify. */
.portfolio-tile { flex: 1 1 0; min-width: 0; margin: 0; }

/* The fade between categories (assets/js/portfolio.js). Tiles leave
   with .is-leaving, defined above; the new set starts from here and
   transitions back to normal, on the same 300ms the tiles already
   carry. */
.portfolio-tile.is-entering { opacity: 0; transform: translateY(10px); }

/* ============================================================
   PHOTOGRAPHERS
   /portfolio/ is one card per photographer, three to a row; each
   photographer's page is the grid above with their round photo, name,
   email and an About window on top. See inc/photographers.php.
   Colours are tokens only, so dark mode needs nothing here: every
   surface that paints its own background sets its own --on-ground,
   which is the rule from COLOUR-MODES.
   ============================================================ */
.vv-photographers { padding-top: 90px; }
.vv-photographers-head { display: flex; flex-direction: column; gap: 14px; max-width: 760px; margin-bottom: 48px; }
.vv-photographers-title { font-size: clamp(24px, 2.6vw, 34px); line-height: 1.2; margin: 0; }

.vv-photographer-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; }
@media (max-width: 1000px) { .vv-photographer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
@media (max-width: 640px) { .vv-photographer-grid { grid-template-columns: minmax(0, 1fr); } }

/* A plain light card: the raised ground, which is paper (never pure
   white, rule three) in light mode and the lifted blue in dark. */
.vv-photographer-card {
  display: flex; flex-direction: column;
  background: var(--ground-raised);
  border: 1px solid rgb(var(--on-ground) / 0.08);
  box-shadow: 0 18px 40px -26px rgb(var(--ch-ink) / 0.22);
  color: rgb(var(--on-ground)); text-decoration: none;
  transition: transform 300ms cubic-bezier(.2,.7,.2,1), box-shadow 300ms ease, border-color 300ms ease;
  --on-ground: var(--on-light);
}
.vv-photographer-card:hover, .vv-photographer-card:focus-visible {
  color: rgb(var(--on-ground)); text-decoration: none;
  transform: translateY(-4px);
  border-color: rgb(var(--ch-gold) / 0.5);
  box-shadow: 0 26px 52px -26px rgb(var(--ch-ink) / 0.32);
}

/* The cover: a 3:2 frame on the card's own colour, the photo whole
   inside it (chosen to be the closest to 3:2 they have). No grade, no
   grey: whatever space is left is just the card. */
.vv-photographer-card-cover { display: flex; align-items: center; justify-content: center; aspect-ratio: 3 / 2; margin: 10px 10px 0; overflow: hidden; }
.vv-photographer-card-cover img {
  max-width: 100%; max-height: 100%; width: auto; height: auto; display: block;
  filter: saturate(1.08) contrast(1.04);
  transition: transform 900ms cubic-bezier(.2,.7,.2,1);
}
.vv-photographer-card:hover .vv-photographer-card-cover img { transform: scale(1.03); }
.vv-photographer-card-cover.is-empty { border: 1px dashed rgb(var(--on-ground) / 0.2); }
.vv-photographer-card-soon { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.55; }

.vv-photographer-card-body { display: flex; flex-direction: column; gap: 10px; padding: 20px 22px 24px; }
.vv-photographer-card-who { display: flex; align-items: center; gap: 12px; min-width: 0; }
.vv-photographer-card-name { font-size: 24px; line-height: 1.15; }
.vv-photographer-card-meta { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.62; }
.vv-photographer-card-cta {
  margin-top: 4px;
  font-family: 'Cinzel', serif; font-weight: 500; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent-text);
}
.vv-photographer-card-cta span { display: inline-block; transition: transform 220ms ease; }
.vv-photographer-card:hover .vv-photographer-card-cta span { transform: translateX(4px); }

/* The round profile picture. The one crop on the site, on purpose: it
   is a small avatar, and the whole photo is a click away in the About
   window. The no-crop test lists this selector by name for that reason. */
.vv-photographer-avatar {
  flex: none; width: 72px; height: 72px; border-radius: 50%;
  object-fit: cover; object-position: 50% 25%;
  border: 2px solid rgb(var(--ch-gold) / 0.7);
  background: var(--photo-ground);
}
.vv-photographer-avatar.is-small { width: 44px; height: 44px; border-width: 1px; }

/* One photographer's page: who they are and how to reach them, above
   their work. */
.vv-portfolio-section.is-photographer { padding-top: 56px; }
.vv-photographer-head { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 28px; margin-bottom: 36px; }
.vv-photographer-back { flex-basis: 100%; font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: rgb(var(--on-ground) / 0.7); text-decoration: none; }
.vv-photographer-back:hover { color: var(--accent-text); text-decoration: none; }
.vv-photographer-id { flex: 1 1 auto; display: flex; align-items: center; gap: 20px; min-width: 0; }
.vv-photographer-id-text { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.vv-photographer-name { font-size: clamp(28px, 3.4vw, 44px); line-height: 1.1; margin: 0; }
.vv-photographer-email { font-size: 13px; letter-spacing: 0.04em; color: rgb(var(--on-ground) / 0.75); text-decoration: none; overflow-wrap: anywhere; }
.vv-photographer-email:hover { color: var(--accent-text); text-decoration: underline; }
.vv-photographer-actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* The About window. A native <dialog>: Esc closes it, focus stays
   inside, and it sits above everything without a z-index fight.

   ONE SIZE, ALWAYS (3 Oct). The window is the same height for every
   photographer, the photo frame is the same size whatever the photo's
   shape, and a long bio scrolls inside the text column (thin gold
   scrollbar) instead of stretching the window. Earlier versions sized
   to the bio and to the photo, so no two photographers' windows looked
   alike. */
.vv-photographer-modal {
  width: min(880px, calc(100vw - 32px));
  height: min(520px, calc(100vh - 48px));
  max-height: none;
  padding: 0; border: 0; overflow: hidden;
  background: var(--ground-raised);
  color: rgb(var(--on-ground));
  box-shadow: 0 40px 80px -30px rgb(var(--ch-ink) / 0.5);
  --on-ground: var(--on-light);
}
.vv-photographer-modal::backdrop { background: rgb(var(--ch-ink) / 0.62); }
.vv-photographer-modal[open] { animation: vv-modal-in 220ms ease; }
@keyframes vv-modal-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.vv-photographer-modal-close {
  position: absolute; top: 12px; right: 14px; z-index: 3;
  width: 40px; height: 40px; border: 0; border-radius: 50%; cursor: pointer;
  background: var(--ground-raised); color: rgb(var(--on-ground));
  box-shadow: 0 4px 14px -4px rgb(var(--ch-ink) / 0.3);
  font-size: 26px; line-height: 1;
}
.vv-photographer-modal-close:hover { color: var(--accent-text); }

.vv-photographer-modal-body { display: grid; grid-template-columns: minmax(0, 1fr); height: 100%; }
.vv-photographer-modal-body.has-photo { grid-template-columns: 320px minmax(0, 1fr); }

/* The photo frame: the full height of the window, a fixed width, and
   the photo fills it. A profile picture, like the round one on the
   page, so it is cropped to the frame on purpose: that is what makes
   every photographer's the same size. Portfolio photos are never
   treated this way. The no-crop test lists this selector by name. */
.vv-photographer-modal-photo { margin: 0; height: 100%; min-height: 0; overflow: hidden; background: rgb(var(--on-ground) / 0.06); }
.vv-photographer-modal-photo img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 25%; }

/* The text column scrolls on its own. */
.vv-photographer-modal-text {
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 52px 44px 44px;
  scrollbar-width: thin;
  scrollbar-color: var(--gold) rgb(var(--on-ground) / 0.06);
}
.vv-photographer-modal-text::-webkit-scrollbar { width: 8px; }
.vv-photographer-modal-text::-webkit-scrollbar-track { background: rgb(var(--on-ground) / 0.06); border-radius: 999px; margin: 16px 0; }
.vv-photographer-modal-text::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 999px; }
.vv-photographer-modal-text::-webkit-scrollbar-thumb:hover { background: var(--gold-hover); }

.vv-photographer-modal-name { font-size: clamp(28px, 3.2vw, 40px); line-height: 1.1; margin: 0; padding-right: 36px; }
/* A long word or a pasted link with no spaces breaks onto the next line
   instead of running out of the window. */
.vv-photographer-modal-name, .vv-photographer-modal-bio { overflow-wrap: anywhere; word-break: break-word; }
.vv-photographer-modal-bio { max-width: none; }
.vv-photographer-modal-bio p { margin: 0 0 1em; }
.vv-photographer-modal-bio p:last-child { margin-bottom: 0; }
.vv-photographer-modal-email { align-self: flex-start; }
@media (prefers-reduced-motion: reduce) { .vv-photographer-modal[open] { animation: none; } }

@media (max-width: 760px) {
  .vv-photographers { padding-top: 64px; }
  .vv-photographer-avatar { width: 60px; height: 60px; }
  /* Stacked: the photo on top in a fixed frame, the text below taking
     the rest and scrolling. The window is one height here too. */
  .vv-photographer-modal { height: min(640px, calc(100vh - 32px)); }
  .vv-photographer-modal-body, .vv-photographer-modal-body.has-photo { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
  .vv-photographer-modal-body.has-photo { grid-template-rows: 240px minmax(0, 1fr); }
  .vv-photographer-modal-text { padding: 28px 22px 32px; }
  .vv-photographer-modal-body:not(.has-photo) .vv-photographer-modal-text { padding-top: 60px; }
}

/* ============================================================
   BACK TO TOP
   One button for the whole site, added by site.js once the page has
   been scrolled well past the first screen.
   ============================================================ */
.vv-to-top {
  position: fixed; right: 22px; bottom: 22px; z-index: 60;
  width: 48px; height: 48px; border-radius: 50%; border: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--gold); color: var(--ink);
  box-shadow: 0 12px 28px -10px rgb(var(--ch-ink) / 0.45);
  opacity: 0; transform: translateY(14px); pointer-events: none;
  transition: opacity 250ms ease, transform 250ms ease, background 180ms ease;
}
.vv-to-top.is-visible { opacity: 1; transform: none; pointer-events: auto; }
.vv-to-top:hover { background: var(--gold-hover); }
.vv-to-top:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }
.vv-to-top svg { width: 20px; height: 20px; }
@media (max-width: 640px) { .vv-to-top { right: 16px; bottom: 16px; width: 44px; height: 44px; } }
@media (prefers-reduced-motion: reduce) { .vv-to-top { transition: none; } }

/* Page to page: a short cross-fade, so moving from the photographer
   cards to one photographer (and back) reads as one place changing
   rather than a hard cut. The second half of the client's 23 Sep
   "animation between pages/employees". Browsers without cross-document
   view transitions simply navigate as before. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 260ms; }
}

/* ============================================================
   CLIENT DELIVERY GALLERY (the page the client opens)
   Standalone page, so it carries its own layout rather than
   inheriting the site's header and nav.
   ============================================================ */
.vv-delivery-page { background: var(--ground); color: rgb(var(--on-ground)); }
/* Customer view: photos cannot be dragged out or long-pressed to save
   on a phone. See initDeliveryGuard() in site.js for what this is and
   is not. */
.vv-guard .vv-delivery img { -webkit-user-drag: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.vv-delivery { max-width: 1240px; margin: 0 auto; padding: 40px 6vw 100px; }

.vv-delivery-head { display: flex; align-items: center; gap: 14px; padding-bottom: 32px; border-bottom: 1px solid rgb(var(--on-ground) / 0.12); }
.vv-delivery-head .brand-mark { width: 54px; height: 54px; border-radius: 50%; border: 1px solid rgb(var(--ch-teal) / 0.35); object-fit: cover; }
.vv-delivery-head .brand-name { font-family: 'Fraunces', serif; font-style: italic; font-weight: 600; font-size: 22px; }

.vv-delivery-intro { padding: 48px 0 40px; }
.vv-delivery-intro .display { font-size: clamp(30px, 4vw, 52px); margin: 10px 0 0; }
.vv-delivery-meta { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.55; margin: 16px 0 0; }
.vv-delivery-actions { margin-top: 28px; }
.vv-delivery-size { opacity: 0.7; font-weight: 400; }
.vv-delivery-hint { font-family: 'Archivo', sans-serif; font-size: 13px; opacity: 0.6; margin: 14px 0 0; }
.btn.is-disabled { opacity: 0.55; cursor: default; pointer-events: none; }

/* Justified-ish grid with each photo at its own shape. Same principle
   as the portfolio: the client's own photos must not be cropped in the
   gallery where they come to collect them. */
.vv-delivery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 18px;
  margin-top: 16px;
}
/* ---- The tile: a card with two parts (9 Oct) ----
   Top: a frame of one fixed shape (4:3) with the photo shown whole
   inside it, never cropped (Rule #1), on a quiet ground. Every card in
   a row is the same height, whatever the shape of the photo.
   Bottom: a strip of its own for Download, or Select and the price,
   and Print. It used to sit over the photo on a gradient, which made
   it unreadable on light photos and invisible in dark mode. */
.vv-delivery-item {
  position: relative; margin: 0; display: flex; flex-direction: column;
  background: var(--ground-raised); border: 1px solid rgb(var(--on-ground) / 0.1);
  border-radius: 6px; overflow: hidden;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.vv-delivery-item:hover { border-color: rgb(var(--on-ground) / 0.22); box-shadow: 0 14px 30px -22px rgb(var(--ch-ink) / 0.55); }
.vv-delivery-open, .vv-delivery-video { aspect-ratio: 4 / 3; background: var(--photo-ground); }
.vv-delivery-item img { width: 100%; height: 100%; object-fit: contain; display: block; }
.vv-delivery-item figcaption {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 50px; padding: 8px 10px 8px 14px;
  border-top: 1px solid rgb(var(--on-ground) / 0.08);
  font-family: 'Archivo', sans-serif;
}
.vv-delivery-get {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 600; color: rgb(var(--on-ground)); text-decoration: none;
}
.vv-delivery-get svg { flex: none; }
.vv-delivery-get:hover { color: var(--accent-text); text-decoration: none; }
.vv-delivery-get .vv-delivery-size { font-weight: 400; opacity: 0.6; font-size: 12px; }

.vv-delivery-gate, .vv-delivery-message { max-width: 520px; margin: 12vh auto; text-align: center; }
.vv-delivery-gate .display, .vv-delivery-message .display { font-size: clamp(26px, 3.4vw, 40px); margin: 12px 0 16px; }
.vv-delivery-form { display: flex; flex-direction: column; gap: 14px; margin-top: 26px; }
.vv-delivery-error { color: #b3261e; font-family: 'Archivo', sans-serif; font-size: 14px; }
.vv-delivery-contact { margin-top: 22px; font-family: 'Archivo', sans-serif; font-size: 14px; }
.vv-delivery-foot { margin-top: 70px; padding-top: 28px; border-top: 1px solid rgb(var(--on-ground) / 0.12); text-align: center; font-family: 'Archivo', sans-serif; font-size: 14px; }

@media (max-width: 640px) {
  .vv-delivery { padding: 26px 5vw 70px; }
  .vv-delivery-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
  .vv-delivery-intro { padding: 20px 0 18px; }
  .vv-delivery-intro .display { margin-top: 6px; }
  .vv-delivery-head { padding-bottom: 18px; }
  .vv-delivery-actions { margin-top: 18px; }
  .vv-delivery-meta { margin-top: 10px; }
}

/* ---- delivery grid: the tile is a button now, so the lightbox is
   reachable by keyboard as well as by pointer. It has to look like
   nothing, because visually it is still just the photo. ---- */
.vv-delivery-open {
  display: block; width: 100%; height: 100%;
  padding: 0; margin: 0; border: 0; background: none;
  cursor: zoom-in; appearance: none; -webkit-appearance: none;
}
.vv-delivery-open:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

.vv-delivery-more { margin: 28px 0 8px; text-align: center; }
.vv-delivery-more .btn[disabled] { opacity: 0.6; cursor: default; }

/* ============================================================
   Delivery: sections, videos, buying and prints (October 2026)

   Sections are the client's "subfolders": tabs along the top of one
   gallery. Videos have no preview, by his choice, so their tile is a
   plain card. A paid gallery puts Select and a price where Download
   was, and a bar along the bottom carries the running total.
   ============================================================ */
.vv-delivery .btn-ghost {
  background: transparent; color: rgb(var(--on-ground));
  border: 1px solid rgb(var(--on-ground) / 0.25);
}
.vv-delivery .btn-ghost:hover { border-color: var(--gold); }
.vv-delivery-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }

.vv-delivery-thanks {
  margin: 28px 0 0; padding: 16px 20px; border-left: 3px solid var(--gold);
  background: rgb(var(--on-ground) / 0.05);
  font-family: 'Archivo', sans-serif; font-size: 15px; line-height: 1.5;
}
.vv-delivery-adminnote {
  margin: 24px 0 0; padding: 12px 16px; border: 1px dashed rgb(var(--on-ground) / 0.3);
  font-family: 'Archivo', sans-serif; font-size: 14px; line-height: 1.5;
}
.vv-delivery-adminnote p { margin: 0; }
.vv-delivery-adminnote p + p { margin-top: 6px; opacity: 0.8; }
.vv-delivery-adminnote a { color: var(--accent-text); }
.vv-shop-preview {
  margin: 0; padding: 14px; text-align: center; border: 1px dashed rgb(var(--on-ground) / 0.3);
  font-family: 'Archivo', sans-serif; font-size: 13px; opacity: 0.8;
}

/* Tabs */
.vv-delivery-tabs {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 0 0 22px; padding: 0 0 18px;
  border-bottom: 1px solid rgb(var(--on-ground) / 0.12);
}
.vv-delivery-tab {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 16px; border-radius: 999px;
  border: 1px solid rgb(var(--on-ground) / 0.18);
  color: rgb(var(--on-ground)); text-decoration: none;
  font-family: 'Archivo', sans-serif; font-size: 14px; font-weight: 500;
}
.vv-delivery-tab:hover { border-color: var(--gold); text-decoration: none; }
.vv-delivery-tab.is-current { background: rgb(var(--on-ground)); color: var(--ground); border-color: transparent; }
.vv-delivery-tab-count { font-family: 'IBM Plex Mono', monospace; font-size: 11px; opacity: 0.6; }

/* Video tile: no preview, just what it is and how big. */
.vv-delivery-video {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 16px; text-align: center; color: rgb(var(--on-ground));
}
.vv-delivery-video-icon { opacity: 0.5; }
.vv-delivery-video-name {
  font-family: 'Archivo', sans-serif; font-size: 14px; font-weight: 600;
  overflow-wrap: anywhere; max-width: 100%;
}
.vv-delivery-video-size { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.55; }

/* The older rule above gives the photo button height:100%, which made
   it fill the whole card and push the strip out of sight. */
.vv-delivery-item .vv-delivery-open { position: relative; height: auto; flex: none; overflow: hidden; }
/* Taken out of the flow so a tall photo cannot stretch the 4:3 frame;
   contain still shows all of it. */
.vv-delivery-item .vv-delivery-open img { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Select and price: the whole label is the target, price in bold. */
.vv-buy {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer; min-width: 0;
  font-size: 13px; font-weight: 500; color: rgb(var(--on-ground));
}
.vv-buy input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--gold); cursor: pointer; }
.vv-buy-price { font-weight: 700; font-size: 14px; }
.vv-delivery-item.is-selected { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
.vv-delivery-item.is-selected figcaption { background: rgb(239 151 46 / 0.12); }

.vv-print-add {
  flex: none; margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 12px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgb(var(--on-ground) / 0.22); background: transparent;
  color: rgb(var(--on-ground)); font-family: 'Archivo', sans-serif; font-size: 12.5px; font-weight: 600;
  transition: border-color 150ms ease, background 150ms ease, color 150ms ease;
}
.vv-print-add:hover { border-color: var(--gold); color: var(--accent-text); }
.vv-print-add.is-added { background: var(--gold); border-color: var(--gold); color: var(--ink); }
.vv-shop-more { margin: 14px 0 0; }

/* "Added to your print order": sits above the selection bar when there
   is one, so the two never cover each other. */
.vv-toast {
  position: fixed; left: 50%; bottom: 96px; z-index: 30;
  transform: translate(-50%, 20px); opacity: 0; pointer-events: none;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap; justify-content: center;
  width: max-content; max-width: calc(100vw - 32px);
  padding: 12px 14px 12px 20px; border-radius: 999px;
  background: var(--ink); color: var(--ivory);
  font-family: 'Archivo', sans-serif; font-size: 14px;
  box-shadow: 0 16px 40px -16px rgb(var(--ch-ink) / 0.6);
  transition: opacity 200ms ease, transform 200ms ease;
}
/* The back-to-top button would sit on the selection bar; lift it clear. */
body:has(.vv-shopbar) .vv-to-top { bottom: 96px; }
@media (max-width: 640px) { body:has(.vv-shopbar) .vv-to-top { bottom: 120px; } }
.vv-toast.is-on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
@media (max-width: 640px) {
  .vv-toast { bottom: 120px; border-radius: 18px; padding: 12px 16px; }
}
.vv-print-count[hidden] { display: none; }
.vv-print-count {
  display: inline-block; min-width: 20px; margin-left: 6px; padding: 1px 6px; border-radius: 999px;
  background: var(--gold); color: var(--ink); font-size: 12px; line-height: 18px;
}

/* The bar along the bottom of a paid gallery. Fixed rather than
   sticky: a sticky bar at the very end of the page rode up over the
   footer. The page gets room at the bottom so nothing hides under it. */
.vv-delivery:has(.vv-shopbar) { padding-bottom: 170px; }
.vv-shopbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  padding: 14px 6vw;
  background: var(--ground-raised);
  border-top: 1px solid rgb(var(--on-ground) / 0.12);
  box-shadow: 0 -12px 30px -18px rgb(var(--ch-ink) / 0.4);
}
.vv-shopbar-inner {
  /* Lines up with the gallery above: its 1240px box less its padding. */
  max-width: calc(1240px - 12vw); margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  font-family: 'Archivo', sans-serif; font-size: 15px;
}
.vv-shopbar-tools { display: flex; align-items: center; gap: 16px; }
.vv-shopbar .btn[disabled] { opacity: 0.5; cursor: default; }
.vv-linkbtn {
  border: 0; background: none; padding: 4px 0; cursor: pointer;
  color: rgb(var(--on-ground)); font-family: 'Archivo', sans-serif; font-size: 14px;
  text-decoration: underline; text-underline-offset: 3px;
}
.vv-linkbtn:hover { color: var(--accent-text); }

/* Checkout dialogs. Same family as the photographer modal: native
   <dialog>, Esc closes, focus stays inside. */
.vv-shop-dialog {
  width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
  padding: 34px 32px 26px; border: 0; overflow: auto;
  background: var(--ground-raised); color: rgb(var(--on-ground));
  box-shadow: 0 40px 80px -30px rgb(var(--ch-ink) / 0.5);
}
.vv-shop-dialog::backdrop { background: rgb(var(--ch-ink) / 0.62); }
.vv-shop-close {
  position: absolute; top: 10px; right: 12px; width: 38px; height: 38px;
  border: 0; border-radius: 50%; background: none; cursor: pointer;
  color: rgb(var(--on-ground)); font-size: 26px; line-height: 1;
}
.vv-shop-close:hover { color: var(--accent-text); }
.vv-shop-title { font-family: 'Fraunces', serif; font-size: 28px; margin: 0 0 18px; padding-right: 30px; }
.vv-shop-lines { list-style: none; margin: 0 -4px; padding: 0 4px; max-height: 48vh; overflow: auto; }

/* One line of the order: thumb, then name and remove on top, size,
   quantity and price underneath. */
.vv-line {
  display: flex; gap: 14px; padding: 14px 0;
  border-bottom: 1px solid rgb(var(--on-ground) / 0.1);
  font-family: 'Archivo', sans-serif; font-size: 14px;
}
.vv-line-thumb {
  flex: none; width: 64px; height: 64px; border-radius: 6px; overflow: hidden;
  background: rgb(var(--on-ground) / 0.06);
}
.vv-line-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.vv-line-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.vv-line-top { display: flex; align-items: center; gap: 8px; }
.vv-line-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.vv-line-opts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.vv-line-price { margin-left: auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.2; white-space: nowrap; }
.vv-line-price strong { font-size: 15px; }
.vv-line-price small { font-size: 11.5px; opacity: 0.6; }
.vv-line-price small:empty { display: none; }
/* Print lines: the price gets a row of its own under size and
   quantity, so "3 × $5.00" appearing never pushes it onto a ragged
   second line. Breakdown on the left, the line total large on the right. */
.vv-line:not(.is-simple) .vv-line-price {
  flex-basis: 100%; margin-left: 0; flex-direction: row-reverse;
  justify-content: space-between; align-items: baseline;
  padding-top: 8px; border-top: 1px dashed rgb(var(--on-ground) / 0.12);
}
.vv-line:not(.is-simple) .vv-line-price strong { font-size: 18px; }
.vv-line:not(.is-simple) .vv-line-price small { font-size: 13px; }

/* The download list is a single row per item. */
.vv-line.is-simple { align-items: center; padding: 12px 0; }
.vv-line.is-simple .vv-line-price { margin-left: 0; font-size: 15px; }
.vv-chip {
  flex: none; padding: 2px 8px; border-radius: 999px;
  font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  background: rgb(var(--on-ground) / 0.08);
}

/* Size: a styled select with its own chevron. */
.vv-select-wrap { position: relative; display: inline-block; }
.vv-select-wrap::after {
  content: ""; position: absolute; right: 12px; top: 50%; width: 7px; height: 7px; margin-top: -5px;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg); pointer-events: none; opacity: 0.7;
}
.vv-shop-select {
  -webkit-appearance: none; appearance: none; cursor: pointer;
  height: 38px; padding: 0 34px 0 14px; border-radius: 999px;
  border: 1px solid rgb(var(--on-ground) / 0.22); background: var(--ground); color: rgb(var(--on-ground));
  font: inherit; font-size: 13.5px; font-weight: 500;
}
.vv-shop-select:hover { border-color: var(--gold); }
.vv-shop-select:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Quantity stepper */
.vv-qty {
  display: inline-flex; align-items: center; height: 38px; border-radius: 999px;
  border: 1px solid rgb(var(--on-ground) / 0.22); background: var(--ground); overflow: hidden;
}
.vv-qty button {
  width: 34px; height: 100%; border: 0; background: none; cursor: pointer;
  color: rgb(var(--on-ground)); font-size: 18px; line-height: 1;
}
.vv-qty button:hover { background: rgb(var(--on-ground) / 0.08); color: var(--accent-text); }
.vv-qty input {
  width: 34px; height: 100%; border: 0; padding: 0; text-align: center; background: none;
  color: rgb(var(--on-ground)); font: inherit; font-weight: 600; -moz-appearance: textfield;
}
.vv-qty input::-webkit-outer-spin-button, .vv-qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.vv-qty input:focus { outline: none; background: rgb(var(--on-ground) / 0.06); }

/* Remove */
.vv-trash {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%; border: 0; background: none; cursor: pointer;
  color: rgb(var(--on-ground) / 0.55); transition: background 150ms ease, color 150ms ease;
}
.vv-trash:hover { color: #fff; background: #c2412d; }
.vv-trash:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }

.vv-shop-sums { margin-top: 6px; }

/* ---- Prints card in the gallery intro (9 Oct) ----
   Says what a print is and how to start, so "Order prints" is not a
   mystery button. */
.vv-prints-promo {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-top: 22px; padding: 16px 18px; max-width: 760px;
  border: 1px solid rgb(var(--on-ground) / 0.14); border-radius: 10px;
  background: var(--ground-raised);
  font-family: 'Archivo', sans-serif;
}
.vv-prints-promo-icon {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border-radius: 50%;
  background: rgb(239 151 46 / 0.14); color: var(--gold);
}
.vv-prints-promo-text { flex: 1; min-width: 220px; display: flex; flex-direction: column; gap: 3px; }
.vv-prints-promo-text strong { font-size: 15px; }
.vv-prints-promo-text span { font-size: 13.5px; line-height: 1.5; opacity: 0.75; }
.vv-prints-promo .btn { flex: none; display: inline-flex; align-items: center; }


/* Wide screens: title and actions on the left, the prints card in a
   column on the right, so the gallery starts higher up the page. */
@media (min-width: 900px) {
  .vv-delivery-intro.has-promo {
    display: grid; grid-template-columns: minmax(0, 1fr) 300px; column-gap: 48px; align-items: start;
  }
  .vv-delivery-intro.has-promo .vv-prints-promo {
    margin-top: 6px; max-width: none; flex-direction: column; align-items: flex-start; gap: 12px; padding: 20px;
  }
  .vv-delivery-intro.has-promo .vv-prints-promo-text { min-width: 0; }
  .vv-delivery-intro.has-promo .vv-prints-promo .btn { width: 100%; justify-content: center; }
}

/* ---- Drawers: the selection and the print order (9 Oct) ----
   A native <dialog> held to the left edge, full height. The list
   scrolls; the head and the total with PayPal stay put. */
.vv-shop-dialog.vv-drawer {
  position: fixed; inset: 0 auto 0 0; margin: 0;
  width: min(460px, 100vw); max-width: 100vw; height: 100vh; height: 100dvh; max-height: none;
  padding: 0; border-radius: 0; overflow: hidden;
  flex-direction: column;
  box-shadow: 30px 0 60px -30px rgb(var(--ch-ink) / 0.6);
}
.vv-shop-dialog.vv-drawer[open] { display: flex; animation: vv-drawer-in 260ms cubic-bezier(.2,.8,.2,1); }
@keyframes vv-drawer-in { from { transform: translateX(-100%); } to { transform: none; } }
/* Tabs along the top of the drawer: Downloads | Prints. */
.vv-drawer-top {
  flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px 0 24px; border-bottom: 1px solid rgb(var(--on-ground) / 0.1);
}
.vv-drawer-top .vv-shop-close { position: static; flex: none; margin-bottom: 6px; }
.vv-drawer-tabs { display: flex; gap: 4px; }
.vv-drawer-tab {
  display: inline-flex; align-items: center; gap: 8px; padding: 12px 14px 13px;
  border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; background: none; cursor: pointer;
  color: rgb(var(--on-ground) / 0.6); font-family: 'Archivo', sans-serif; font-size: 15px; font-weight: 600;
}
.vv-drawer-tab:hover { color: rgb(var(--on-ground)); }
.vv-drawer-tab[aria-selected="true"] { color: rgb(var(--on-ground)); border-bottom-color: var(--gold); }
.vv-drawer-tab:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
.vv-drawer-tab-count {
  min-width: 22px; padding: 1px 7px; border-radius: 999px; text-align: center;
  background: rgb(var(--on-ground) / 0.08); font-size: 12px; line-height: 18px;
}
.vv-drawer-tab[aria-selected="true"] .vv-drawer-tab-count { background: var(--gold); color: var(--ink); }
.vv-drawer-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.vv-drawer-panel[hidden] { display: none; }
.vv-drawer-panel .vv-drawer-head { display: block; padding: 20px 24px 12px; }

/* The print order comes in from the right. */
.vv-shop-dialog.vv-drawer.is-right { inset: 0 0 0 auto; box-shadow: -30px 0 60px -30px rgb(var(--ch-ink) / 0.6); }
.vv-shop-dialog.vv-drawer.is-right[open] { animation-name: vv-drawer-in-right; }
@keyframes vv-drawer-in-right { from { transform: translateX(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .vv-shop-dialog.vv-drawer[open] { animation: none; } }

.vv-drawer-head {
  flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 26px 24px 16px; border-bottom: 1px solid rgb(var(--on-ground) / 0.1);
}
.vv-drawer-head .vv-shop-title { margin: 0; padding: 0; font-size: 26px; }
.vv-drawer-sub { margin: 4px 0 0; font-family: 'Archivo', sans-serif; font-size: 13.5px; opacity: 0.65; }
.vv-drawer-head .vv-shop-close { position: static; flex: none; margin: -6px -8px 0 0; }
.vv-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 4px 24px 16px;
  scrollbar-width: thin; scrollbar-color: var(--gold) transparent; }
.vv-drawer .vv-shop-lines { max-height: none; overflow: visible; margin: 0; padding: 0; }
.vv-drawer .vv-shop-empty { margin: 18px 0 0; }
.vv-drawer-foot {
  flex: none; padding: 16px 24px 20px; background: var(--ground-raised);
  border-top: 1px solid rgb(var(--on-ground) / 0.1);
  box-shadow: 0 -14px 24px -20px rgb(var(--ch-ink) / 0.5);
}
.vv-drawer-foot .vv-shop-total { margin: 0 0 14px; }
.vv-drawer-foot .vv-shop-sums { margin: 0 0 6px; }
.vv-drawer-foot .vv-shop-sums .vv-shop-row:first-child { margin-top: 0; }
.vv-drawer-foot .vv-shop-total + .vv-shop-paypal,
.vv-drawer-foot .vv-shop-sums + .vv-shop-total { margin-top: 10px; }
.vv-drawer-foot .vv-shop-small { margin-top: 10px; font-size: 12px; }

/* Selection list: a thumbnail on every row. */
.vv-line.is-simple .vv-line-thumb { width: 52px; height: 52px; }
.vv-line-thumb.is-video { display: inline-flex; align-items: center; justify-content: center; opacity: 0.7; }

@media (max-width: 640px) {
  .vv-shop-dialog.vv-drawer { width: 100vw; }
  .vv-drawer-top { padding: 8px 8px 0 12px; }
  .vv-drawer-tab { padding: 12px 10px 13px; font-size: 14px; }
  .vv-drawer-panel .vv-drawer-head { padding: 16px 16px 10px; }
  .vv-drawer-head { padding: 20px 16px 12px; }
  .vv-drawer-body { padding: 4px 16px 12px; }
  .vv-drawer-foot { padding: 12px 16px 16px; }
  .vv-prints-promo { padding: 14px; gap: 12px; }
  .vv-prints-promo-icon { width: 40px; height: 40px; }
}
.vv-shop-sums .vv-shop-row { margin-top: 8px; opacity: 0.8; font-size: 14px; }
.vv-shop-row, .vv-shop-total {
  display: flex; justify-content: space-between; margin: 14px 0 0;
  font-family: 'Archivo', sans-serif; font-size: 15px;
}
.vv-shop-total { font-size: 18px; margin-bottom: 18px; }
.vv-shop-paypal { min-height: 50px; }
.vv-shop-msg { color: #b3261e; font-family: 'Archivo', sans-serif; font-size: 14px; margin: 10px 0 0; }
.vv-shop-msg:empty { display: none; }
.vv-shop-small, .vv-shop-empty { font-family: 'Archivo', sans-serif; font-size: 13px; opacity: 0.65; margin: 12px 0 0; line-height: 1.5; }

@media (max-width: 640px) {
  .vv-shopbar { padding: 12px 5vw; }
  .vv-shopbar-inner { font-size: 14px; }
  .vv-shop-dialog { padding: 30px 18px 20px; }
  .vv-shop-lines { max-height: none; overflow: visible; }
  .vv-line { gap: 12px; }
  .vv-line-thumb { width: 56px; height: 56px; }
  .vv-delivery-tab { padding: 7px 12px; font-size: 13px; }
  .vv-buy-label { display: none; }
  .vv-delivery-item figcaption { padding: 6px 6px 6px 10px; min-height: 46px; }
  .vv-print-add { padding: 0 9px; }
  .vv-print-add span { display: none; }
}

/* ============================================================
   Portfolio on small phones (<= 576px)

   Three to a row leaves each photo about 100px wide on a 384px
   screen, which is a contact sheet, not a portfolio. Two to a row
   gives each one roughly 150px and the work actually reads.

   This is now done by REPACKING the rows into twos, in
   assets/js/portfolio.js, not by wrapping rows of three. Wrapping
   was the cheaper trick and it is what the client reported on
   18 Sep: a row of three wrapped two-up leaves its third photo
   alone on a line with the right half of the screen empty, on every
   single row. Packing rows to two instead means every row is full
   and justified, so there is nothing left to wrap.

   What is left below is the fallback for a visitor with no
   JavaScript, who still has the server's rows of three. It is
   scoped to :not(.is-grouped) so it stands down the moment the
   browser has repacked.
   ============================================================ */
@media (max-width: 576px) {
  .portfolio-rows:not(.is-grouped) .portfolio-row { flex-wrap: wrap; }

  /* flex-grow has to be beaten with !important because each tile
     carries its own grow inline, set from the photo's aspect ratio.
     That is exactly right for a justified row and exactly wrong
     here: left alone, the odd third photo in a wrapped row would
     stretch to the full width and tower over the pair above it. */
  .portfolio-rows:not(.is-grouped) .portfolio-tile {
    flex-grow: 0 !important;
    flex-basis: calc(50% - 8px);
  }

  /* Caps assume a row that did not fill up. A wrapped row has,
     it just could not fit on one line. */
  .portfolio-rows:not(.is-grouped) .portfolio-row[data-count="1"],
  .portfolio-rows:not(.is-grouped) .portfolio-row[data-count="2"] { max-width: none; }

  /* The caption is sized for a desktop tile. In a 150px box the
     italic line ran to three lines and covered the photo. Desktop
     keeps the approved 15px; only the phone shrinks. */
  .portfolio-caption { left: 10px; right: 10px; bottom: 10px; gap: 1px; }
  .portfolio-caption .cat { font-size: 9px; letter-spacing: 0.08em; }
  .portfolio-caption .sub { font-size: 11px; line-height: 1.25; }

  .portfolio-frame-num { font-size: 9px; padding: 2px 5px; top: 8px; right: 8px; }
}

/* ---- delivery header: brand and how to reach him, together on the
   left. The person looking at this page is the one most likely to
   need to ask something about their photos, so the details sit in
   the header rather than only at the very bottom. ---- */
.vv-delivery-brand { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

.vv-delivery-contactline {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  font-family: 'IBM Plex Mono', monospace; font-size: 12px;
  color: rgb(var(--on-ground) / 0.55);
}
.vv-delivery-contactline a { color: inherit; text-decoration: none; border-bottom: 1px solid rgb(var(--on-ground) / 0.18); }
.vv-delivery-contactline a:hover,
.vv-delivery-contactline a:focus { color: var(--accent-text); border-bottom-color: var(--accent-text); }

@media (max-width: 640px) {
  /* The download button is the widest thing on the page and at full
     size it wrapped onto two lines on a phone, which made the size
     in brackets look like a separate line of text. */
  .vv-delivery-actions .btn { font-size: 13px; padding: 12px 20px; }
  .vv-delivery-actions .vv-delivery-size { font-size: 12px; }

  .vv-delivery-contactline { font-size: 11px; gap: 6px; }
  .vv-delivery-head .brand-mark { width: 42px; height: 42px; }
}

@media (max-width: 400px) {
  /* At the narrowest real phones the label alone needs the room. */
  .vv-delivery-actions .btn { font-size: 12.5px; padding: 11px 16px; }
}

/* ============================================================
   CURRENT OFFER POPUP  (blocks/offer-modal)

   The [hidden] rules matter more than they look. A browser hides
   [hidden] with display:none from its own stylesheet, and any
   display in an author stylesheet beats it. Both of the overlays
   below set display, so without these two lines every visitor
   would get the popup and the intro permanently open.
   ============================================================ */
.vv-offer {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  /* No padding here. An absolutely positioned child resolves inset: 0
     against the PADDING box, so padding on this element would pull the
     scrim in off the screen edges. The card keeps itself off the edges
     with its own width instead. */
  /* Same open/close pattern as .lightbox: opacity and visibility can
     transition where display cannot, and the delay on visibility
     keeps a closed dialog out of the tab order without losing the
     fade on the way out. */
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 320ms ease, visibility 0s linear 320ms;
}
.vv-offer[hidden] { display: none; }
.vv-offer.is-open {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 320ms ease;
}

/* The page layout is "constrained", which puts max-width: 1240px on
   every direct child of the content area that is not aligned. These
   two are fixed overlays, not page content, and capped at 1240px their
   scrim stopped short of the screen edges on any wider window: the
   page stayed bright down both sides. That is the client's rule 5
   (edge to edge, right to the screen borders) broken again.

   Both carry `alignfull`, which is how every other full-bleed block in
   this theme opts out, and which makes WordPress's rule stop matching
   them at all. This line is the safety net for the day somebody edits
   that class away, scoped through `body` only to outrank the rule it
   is undoing: WordPress writes its selector inside :where(), so that
   rule carries a single class of specificity and this beats it.

   Only max-width. The same rule sets auto side margins with
   !important, which this cannot override and does not need to: on a
   fixed element with inset: 0 and no max-width, auto margins are 0. */
body .vv-offer, body .vv-intro { max-width: none; }

/* A button, not a div: clicking outside a dialog closes it everywhere
   else on the web, and a button gets that behaviour with the keyboard
   and the pointer for free. */
.vv-offer-scrim {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; padding: 0; border: none; cursor: pointer;
  background: rgb(var(--ch-ink) / 0.78);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}

/* Paper ground and a gold hairline, the same parts as .hero-card and
   .testi-card. Those two are square, and this was too until the client
   asked for rounded corners on 21 Sep ("rounded corners in general for
   the pop-up would be nice"). His call, and it sits fine: the radius
   matches .hero2-tile, so it is a shape the site already uses.

   88vw, not a full-width sheet. Also his: "on mobile the pop-up takes
   up the width of the screen and should be slightly smaller". One
   width covers every screen now, which is why there is no width
   override left in the phone block further down. */
.vv-offer-card {
  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  width: min(520px, 88vw);
  max-height: 88vh; overflow-y: auto;
  background: var(--ground-raised);
  border: 1px solid rgb(var(--ch-gold) / 0.45);
  /* Wider gold edge along the top, the way a letterhead is ruled. It
     is also what carries the brand at a glance: by the time the eye
     reaches the eyebrow, the card already reads as this site. */
  border-top: 3px solid var(--gold);
  border-radius: 18px;
  box-shadow: 0 40px 90px rgb(var(--ch-ink) / 0.45);
  /* Rises the last few pixels as it fades in, rather than appearing
     flat. The transform is on the card and the fade on the wrapper, so
     the scrim does not slide with it. */
  transform: translateY(16px) scale(0.985);
  transition: transform 380ms cubic-bezier(.2, .7, .2, 1);
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.vv-offer.is-open .vv-offer-card { transform: none; }

.vv-offer-close {
  position: absolute; top: 14px; right: 14px; z-index: 2;
  width: 36px; height: 36px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgb(var(--ch-gold) / 0.45);
  background: var(--ground-raised);
  color: rgb(var(--on-ground)); font-size: 20px; line-height: 1;
  cursor: pointer;
  transition: background 180ms ease, border-color 180ms ease, color 180ms ease;
  /* This surface sets its own background, so it sets its own
     opposite too. Inheriting the section's would put ivory text
     on a pale card. */
  --on-ground: var(--on-light);
}
.vv-offer-close:hover { background: var(--gold); border-color: var(--gold); color: var(--ink); }

/* The photo is sized by its own shape: a max in each direction and no
   fixed box, so it is never cropped (rule one) and never letterboxed
   either. The brackets are on the span, which hugs the photo, and they
   are square, so the photo is square too. */
.vv-offer-photo { padding: 26px 26px 0; display: flex; justify-content: center; }
.vv-offer-frame { display: inline-block; line-height: 0; }
.vv-offer-frame img {
  width: auto; height: auto;
  max-width: 100%; max-height: 30vh;
}

.vv-offer-body { padding: 38px 40px 40px; display: flex; flex-direction: column; gap: 16px; }
.vv-offer-card.has-photo .vv-offer-body { padding-top: 30px; }

/* The eyebrow sits on its own rule, which is what separates a heading
   with a label above it from a heading with a stray line above it. */
.vv-offer-eyebrow {
  display: block; padding-bottom: 16px;
  border-bottom: 1px solid rgb(var(--ch-gold) / 0.35);
}
.vv-offer-heading {
  font-size: clamp(1.55rem, 1.2rem + 1.4vw, 2.05rem);
  line-height: 1.12; margin: 0; letter-spacing: -0.01em;
}
.vv-offer-text {
  margin: 0; font-size: 15px; line-height: 1.7;
  color: rgb(var(--on-ground) / 0.74);
}

/* The code reads as something to write down, so it gets the ledger
   treatment the rates use rather than looking like more prose. */
.vv-offer-code {
  margin: 2px 0 0;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.vv-offer-code-label {
  font-family: 'Cinzel', serif; font-size: 11px;
  letter-spacing: 0.26em; text-transform: uppercase;
  color: rgb(var(--on-ground) / 0.5);
}
.vv-offer-code-value {
  font-size: 14px; letter-spacing: 0.16em;
  padding: 9px 18px;
  color: rgb(var(--on-ground));
  background: rgb(var(--ch-gold) / 0.14);
  border: 1px dashed rgb(var(--ch-gold) / 0.75);
}

/* Left on a computer, centred on a phone.
   Centred everywhere was the 21 Sep answer and it was half right. On a
   phone it is correct: the button goes full width there, so the way
   past it belongs under the middle of it. On a computer it left the
   button floating under left-aligned eyebrow, heading and body, which
   is the one thing on the card not lining up with the rest. So the
   base case follows the text, and the phone overrides it below. */
.vv-offer-actions { display: flex; align-items: center; justify-content: flex-start; gap: 20px; flex-wrap: wrap; margin-top: 10px; }
.vv-offer-actions .btn { flex: none; }

/* Deliberately not a button shape. There is one action on this card and
   this is the way out of it, so it should not compete. */
.vv-offer-dismiss {
  border: none; background: none; padding: 0; cursor: pointer;
  font-family: 'Archivo', sans-serif; font-size: 14px;
  color: rgb(var(--on-ground) / 0.55);
  text-decoration: underline; text-underline-offset: 3px;
}
.vv-offer-dismiss:hover { color: rgb(var(--on-ground)); }

body.modal-open { overflow: hidden; }

/* The way back to an offer somebody has already closed. The client
   asked for one on 21 Sep, "probably in the services page", so it is a
   label on the block rather than a fixed position: set the label and
   the button appears wherever the block is placed, leave it empty and
   there is no button at all. */
.vv-offer-trigger {
  display: flex; justify-content: center;
  padding: 56px var(--vv-gutter);
}

@media (max-width: 560px) {
  /* This was a full-width sheet pinned to the bottom of the screen.
     The client asked for neither: "it is also aligned with the bottom
     instead of middle oddly". So the phone now gets the same centred
     card as everything else, just with tighter padding. The width and
     the centring both come from the rules above, which is why nothing
     here touches them. */
  .vv-offer-card { max-height: 86vh; }
  .vv-offer-body { padding: 28px 24px 30px; }
  .vv-offer-photo { padding: 24px 24px 0; }
  .vv-offer-frame img { max-height: 24vh; }
  /* Full-width button, so the closing link belongs centred under it
     rather than tucked against the left edge. */
  .vv-offer-actions { gap: 16px; justify-content: center; }
  .vv-offer-actions .btn { width: 100%; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
  .vv-offer, .vv-offer-card { transition: none; }
}

/* ============================================================
   OPENING ANIMATION  (blocks/intro-animation)

   Full screen, over everything including the fixed nav (z-index 50)
   and the lightbox (200). It is the first thing on screen and the
   only thing on screen.
   ============================================================ */
.vv-intro {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  background: var(--ink);
  cursor: pointer;
  opacity: 1;
  transition: opacity 400ms ease;
}
.vv-intro[hidden] { display: none; }
.vv-intro.is-leaving { opacity: 0; pointer-events: none; }

/* Absolute rather than a centred flex item.

   The flex centring belonged to the contained version, where the media
   was smaller than the stage and had to be placed in it. Now that it
   covers, there is nothing left to centre and a flex item asked for
   width:100% inside a centring parent is one shrink rule away from
   being narrower than the screen. Pinning both to the four edges takes
   that whole question off the table. */
.vv-intro-stage {
  position: absolute; inset: 0;
}

/* Fills the window, cropping the overflow.

   This used to be contain (width:auto + max-width:100%), which showed
   whatever the client exported whole and put black bars in whatever
   was left over. He asked for it to fill the screen: on a wide monitor
   the bars were down the sides, and a 16:9 file on a 21:9 display
   leaves a lot of them.

   Cover means the short edge fills and the long edge is trimmed, so
   there is no empty space at any window shape. The trade is that the
   edges of the frame are cut on shapes far from the file's own, which
   is why the block takes a separate mobile file: a portrait phone
   crops a landscape video hard, and the answer to that is a portrait
   export, not a letterbox.

   Rule 1, photos are never cropped, is about the portfolio. This is
   the opening animation and the client asked for it to fill. */
.vv-intro-media {
  display: block;
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Centre the crop, so a logo in the middle of the frame survives. */
  object-position: center center;
}

/* The sound button, beside Skip. Same shape as it, one notch quieter,
   because turning sound on is the optional half of the pair. */
.vv-intro-sound {
  position: absolute; z-index: 2;
  left: max(22px, env(safe-area-inset-left, 0px));
  bottom: max(22px, env(safe-area-inset-bottom, 0px));
  width: 42px; height: 42px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgb(var(--ch-ivory) / 0.3);
  border-radius: 999px;
  background: rgb(var(--ch-ink) / 0.4);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  color: rgb(var(--ch-ivory));
  font-size: 15px; line-height: 1; cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease;
}
.vv-intro-sound:hover { border-color: var(--gold); color: var(--gold); }
.vv-intro-sound:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.vv-intro-sound[hidden] { display: none; }

/* Muted shows the crossed speaker, because that is the state. Press it
   and it shows the open one. */
.vv-intro-sound-on { display: none; }
.vv-intro-sound.is-on .vv-intro-sound-on { display: block; }
.vv-intro-sound.is-on .vv-intro-sound-off { display: none; }
.vv-intro-sound.is-on { border-color: var(--gold); color: var(--gold); }

@media (prefers-reduced-motion: reduce) {
  .vv-intro-sound { transition: none; }
}

.vv-intro-skip {
  position: absolute; z-index: 2;
  right: max(22px, env(safe-area-inset-right, 0px));
  bottom: max(22px, env(safe-area-inset-bottom, 0px));
  padding: 10px 22px; border-radius: 999px;
  border: 1px solid rgb(var(--on-ground) / 0.3);
  background: rgb(var(--ch-ink) / 0.4);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  color: var(--ivory);
  font-family: 'Cinzel', serif; font-size: 11px;
  letter-spacing: 0.24em; text-transform: uppercase;
  cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease;
}
.vv-intro-skip:hover { border-color: var(--gold); color: var(--gold); }

/* Set by the inline script in the block, cleared when the overlay
   goes. On <html> as well as <body> because iOS Safari will still
   scroll the document behind a fixed overlay otherwise. */
html.vv-intro-lock, html.vv-intro-lock body { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .vv-intro { transition: none; }
}
