/* Self-hosted rather than loaded from Google.
   The Google Fonts <link> was a render-blocking stylesheet on a third-party
   origin, which meant two extra DNS lookups and TLS handshakes before any text
   could paint. These are the same latin-subset variable builds, served from our
   own origin. All three are SIL Open Font Licence; see fonts/OFL-NOTICE.txt.
   One file per family covers every weight, because they are variable fonts. */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/fraunces.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/dmsans.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("fonts/montserrat.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Coastal Cocktails by Ollie — one stylesheet for all five pages.
   Palette sampled from the brand assets (10.png banner + the badge logo). */

:root {
  /* Sampled from Ollie's current Instagram logo lockup (= 10.png in the
     source folder), which is the mark he's using now. Contrast checked
     against --bone: brown 6.5:1 (AA body), small print 4.8:1 (AA).
     --sun FAILS text contrast at 1.5:1 — graphic element only, never type. */
  --bone:        #E3E2DE;
  --cream:       #F2F0EB;
  --sand:        #D5D2C9;   /* palette swatch; borders now use --line */
  --ink:         #4A2A1D;   /* body text, a touch darker than the logo brown */
  --ink-soft:    #6E5748;
  --brand:       #71402F;   /* the logo brown, exact */
  --terracotta:  #B84A24;   /* button fill: cream label clears 4.6:1 */
  --terracotta-d:#9E3A1A;   /* hover + small text on light: 4.8:1 AA */
  --amber:       #E8A94B;
  --sun:         #FFA755;   /* logo gradient, exact */
  --teal:        #1284A6;   /* logo wave, exact */

  --display: "Fraunces", "Hoefler Text", Georgia, serif;
  --body: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;

  --ease: cubic-bezier(.16, 1, .3, 1);
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --section: clamp(4rem, 9vw, 7.5rem);
  --maxw: 1180px;

  /* ---- surfaces ----------------------------------------------------------
     Three light planes instead of two, so a section can sit BELOW the page
     (recessed band), ON it, or ABOVE it (raised card). Previously every light
     section was the same --bone, which is why the pages read as one flat
     sheet with no grouping. --shell is bone shifted ~4% darker and a touch
     warmer; --ink on it is 6.2:1, so body text still clears AA. */
  --shell:       #DAD7D0;   /* recessed band: tinted section backgrounds */
  --paper:       #F7F5F1;   /* raised: cards that should feel lifted */

  /* ---- lines -------------------------------------------------------------
     Translucent ink rather than the fixed --sand grey. The same token then
     reads correctly on shell, bone AND paper instead of being too heavy on
     the light ones -- the old opaque border was the loudest thing in a card,
     louder than its own heading. --line-light is the same idea on dark. */
  --line:        rgba(74, 42, 29, .13);
  --line-soft:   rgba(74, 42, 29, .08);
  --line-strong: rgba(74, 42, 29, .2);
  --line-light:  rgba(247, 243, 236, .14);

  /* ---- elevation ---------------------------------------------------------
     Warm-tinted, never neutral grey: a grey shadow on a sand-coloured page
     reads as dirt. Two-layer (contact + ambient) so the edge stays crisp.
     e1 is a resting lift, e2 a hover, e3 the one genuinely floating element. */
  --e1: 0 1px 2px rgba(58, 36, 25, .05), 0 6px 16px -6px rgba(58, 36, 25, .10);
  --e2: 0 2px 4px rgba(58, 36, 25, .06), 0 18px 36px -12px rgba(58, 36, 25, .18);
  --e3: 0 4px 8px rgba(58, 36, 25, .08), 0 32px 60px -20px rgba(58, 36, 25, .26);

  /* ---- radius scale ------------------------------------------------------
     Deliberately restrained: this is an editorial serif brand, and blanket
     large corners are exactly the generic-card look we are getting away from.
     Small controls stay near-square; only large surfaces (image crops, feature
     panels) get enough radius to read as intentional rather than accidental. */
  --r-xs: 2px;    /* tags */
  --r-sm: 4px;    /* buttons, inputs */
  --r:    8px;    /* image crops, video, gallery tiles */
  --r-lg: 12px;   /* cards and feature panels */

  /* Gap between a section's heading block and the content under it. Was three
     different inline margin-tops (2 / 2.5 / 3rem) for the same relationship. */
  --flow: clamp(2rem, 4vw, 3.25rem);
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bone);
  color: var(--ink);
  font-family: var(--body);
  font-size: clamp(1rem, .96rem + .2vw, 1.0625rem);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img, video { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

h1, h2, h3, .display {
  font-family: var(--display);
  font-weight: 600;
  font-variation-settings: "SOFT" 20, "WONK" 0;
  line-height: 1.04;
  letter-spacing: -.02em;
  margin: 0 0 .5em;
  text-wrap: balance;
}
h1 { font-size: clamp(2.6rem, 7.2vw, 5.2rem); }
h2 { font-size: clamp(2rem, 4.4vw, 3.4rem); }
h3 { font-size: clamp(1.2rem, 1.8vw, 1.5rem); letter-spacing: -.01em; }
p { margin: 0 0 1.1em; max-width: min(62ch, 100%); }

.wrap { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: var(--section); }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4rem); }

/* ---------- section planes ----------
   Every section in the site is `<section class="section wrap">`, i.e. the
   section IS the 1180px container, so a background on it would stop at the
   content edge. Rather than restructure a dozen sections in the generators to
   add an inner div, the tint is painted by a spread box-shadow and revealed
   with a horizontal-only clip: full-bleed colour, no extra element, and none
   of the horizontal overflow a 100vw child causes when a scrollbar is present.
   The clip is inset(0 ...) vertically, and the only thing that moves out of a
   section is the split figure's 14px parallax drift, which sits well inside
   the section's 4-7.5rem padding, so nothing gets cut.

   Used as a rhythm, not an alternation: a tint marks a change of subject or
   carries a group that needs a plane to sit on (the ruled feature clusters and
   the testimonials, both of which have had their individual boxes removed). */
.section--tint {
  background: var(--shell);
  /* vw, not vmax: vmax on a tall viewport asks for a spread of hundreds of
     thousands of pixels, which Chrome will mis-paint. A spread of one full
     viewport width already reaches well past both edges. Painted overflow
     never contributes to scroll width, so this cannot produce a sideways
     scrollbar the way a 100vw-wide child would. */
  box-shadow: 0 0 0 100vw var(--shell);
  clip-path: inset(0 -100vw);
}
/* Where a tinted band meets the page, a hairline stops the 4% step reading as
   a rendering artefact on low-contrast screens. */
.section--tint { border-block: 1px solid var(--line-soft); }

/* One value for the heading-block -> content gap, everywhere. It was three
   different inline margin-tops for the same relationship. */
.section__body { margin-top: var(--flow); }
/* Intro block above a grid: centred, and narrow enough to stay readable. */
.section__intro { max-width: 58ch; }
.lede { font-size: clamp(1.1rem, 1.4vw, 1.3rem); color: var(--ink-soft); }
.center { text-align: center; margin-inline: auto; }
.center p { margin-inline: auto; }

/* A plain category label above an expressive heading -- never a question, a
   joke or a sentence; see COPY.md. Only appears where the heading is a line
   rather than a label, so it never sits above something that is already
   labelling itself. That coupling is what keeps it on about half the sections
   and stops the page reading as a stamped template. */
.eyebrow {
  font-size: .75rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--terracotta-d);
  margin: 0 0 1rem;
}
.eyebrow--light { color: var(--amber); }


/* ---------- buttons ---------- */
.btn {
  --btn-bg: var(--terracotta);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  padding: 1rem 1.9rem;
  background: var(--btn-bg);
  color: var(--cream);
  font: 500 .95rem/1 var(--body);
  letter-spacing: .04em;
  text-decoration: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  overflow: hidden;
  transition: background .25s var(--ease), transform .25s var(--ease),
              box-shadow .25s var(--ease);
}
/* Amber sheen sweep. The transition lives on the :hover state, NOT here --
   with it on the base rule the sheen animated back the way it came when the
   pointer left, so every hover produced two sweeps and read as a flicker.
   Parked off both edges (-120% / +120%) inside an overflow:hidden button, so
   resetting instantly is invisible. */
.btn::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 20%, rgba(232,169,75,.55) 50%, transparent 80%);
  transform: translateX(-120%);
}
.btn:hover { background: var(--terracotta-d); transform: translateY(-2px); }
.btn:hover::after {
  transform: translateX(120%);
  transition: transform .5s var(--ease);
}
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--ink);
}
.btn--ghost:hover { background: var(--ink); color: var(--cream); transform: translateY(-2px); }
.btn--light { background: var(--cream); color: var(--ink); }
.btn--light:hover { background: #fff; }

/* ---------- header ---------- */
.hdr {
  position: fixed; inset: 0 0 auto; z-index: 60;
  display: flex; align-items: center; gap: 1.5rem;
  padding: 1.35rem var(--gutter);
  color: var(--cream);
  transition: background .3s var(--ease), padding .3s var(--ease), color .3s var(--ease),
              box-shadow .3s var(--ease);
}
.hdr.is-stuck {
  background: var(--bone);
  color: var(--ink);
  padding-block: .7rem;
  box-shadow: 0 1px 0 var(--line), 0 8px 24px -16px rgba(58,36,25,.4);
}
/* Pages without a top hero start solid (main.js), so their content must clear
   the fixed header. */
main:not(:has(> .hero:first-child)) { padding-top: 3.6rem; }

.hdr__brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; margin-right: auto; }
.hdr__mark { width: 34px; height: 31px; flex: none; }
/* Wordmark set to match Ollie's actual logo: heavy geometric sans, uppercase,
   tight tracking. Editorial headlines stay on Fraunces -- a sans logo over
   serif headlines, the usual two-voice brand system. */
.hdr__name {
  font-family: "Montserrat", var(--body); font-weight: 800;
  font-size: .98rem; line-height: 1; letter-spacing: -.015em;
  text-transform: uppercase;
}
.hdr__name small {
  display: block; margin-top: .3em;
  font: 500 .55rem/1.4 var(--body); letter-spacing: .2em;
  text-transform: uppercase; opacity: .78;
}
.nav { display: flex; gap: 1.6rem; }
.nav a {
  position: relative;
  text-decoration: none; font-size: .9rem; padding-block: .2rem;
}
/* underline grows from the left; the same gesture is reused on footer links,
   so the two read as one system rather than two unrelated hovers */
.nav a::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .22s var(--ease);
}
.nav a:hover::after, .nav a[aria-current="page"]::after { transform: scaleX(1); }

/* reading progress: --sp is set by main.js, 0 at the top, 1 at the bottom.
   Never runs under reduced motion, where it simply stays at 0 and invisible. */
.hdr::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: linear-gradient(90deg, var(--terracotta), var(--amber));
  transform: scaleX(var(--sp, 0)); transform-origin: 0 50%;
  opacity: 0;
  transition: opacity .3s var(--ease);
}
.hdr.is-stuck::after { opacity: 1; }
.hdr__tel { text-decoration: none; font-size: .9rem; font-weight: 500; }
/* Between ~900 and ~1150px there is not room for logo + nav + phone + button,
   so the phone text drops first and the nav tightens. Below 900 the mobile
   action bar takes over entirely. */
@media (max-width: 1150px) {
  .hdr__tel { display: none; }
  .nav { gap: 1.05rem; }
  .nav a { font-size: .85rem; }
  .hdr > .btn { padding: .8rem 1.2rem; white-space: nowrap; }
}
@media (max-width: 900px) {
  .nav, .hdr__tel, .hdr > .btn { display: none; }
}

/* ---------- mobile nav sheet ----------
   The header's only mobile control. Everything else in the header is hidden
   below 900px, so this is the one thing standing between a phone visitor and
   the rest of the site. */
.hdr__burger {
  display: none;
  width: 44px; height: 44px;      /* full touch target, not a 24px icon */
  margin-left: auto;
  padding: 0; border: 0; background: none; color: inherit;
  cursor: pointer;
  position: relative;
}
/* Three bars from one element: the middle is the span, the outer two are its
   pseudo-elements. Nothing to keep in sync when the colour inverts under
   .is-stuck, because all three inherit currentColor. */
.hdr__burger span, .hdr__burger span::before, .hdr__burger span::after {
  position: absolute; left: 50%; width: 22px; height: 2px;
  background: currentColor; border-radius: 2px;
  translate: -50% 0;
}
.hdr__burger span { top: 50%; }
.hdr__burger span::before { content: ""; top: -7px; left: 0; translate: 0 0; }
.hdr__burger span::after  { content: ""; top:  7px; left: 0; translate: 0 0; }

.navsheet {
  /* full-height panel down the right, not a full-screen takeover: the page
     stays visible behind it so the sheet reads as a layer, not a new page */
  margin: 0 0 0 auto;
  width: min(22rem, 86vw); max-width: none;
  height: 100%; max-height: 100%;
  padding: calc(1.35rem + env(safe-area-inset-top)) var(--gutter)
           calc(1.5rem + env(safe-area-inset-bottom));
  border: 0;
  background: var(--cream);
  color: var(--ink);
  box-shadow: var(--e3);
  flex-direction: column;
}
.navsheet[open] { display: flex; }
.navsheet::backdrop { background: rgba(30,17,10,.55); }

.navsheet__close {
  align-self: flex-end;
  width: 44px; height: 44px;
  padding: 0; border: 0; background: none; color: var(--ink-soft);
  font: 400 1.9rem/1 var(--body); cursor: pointer;
}
.navsheet__nav { display: grid; margin-top: .5rem; }
.navsheet__nav a {
  padding: 1rem 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  font-family: var(--display); font-size: 1.3rem; letter-spacing: -.01em;
}
.navsheet__nav a[aria-current="page"] { color: var(--terracotta-d); }
.navsheet__foot { margin-top: auto; padding-top: 1.75rem; }
/* The number is printed, not just wrapped in a tel: link. On a phone it was
   otherwise only in the footer, and a "Call Ollie" button is a dead end if
   the handset does not pick up the tel: handler. */
.navsheet__tel {
  display: block; margin-bottom: 1rem;
  font-family: var(--display); font-size: 1.5rem;
  text-decoration: none; color: var(--ink);
}
.navsheet__cta { width: 100%; justify-content: center; }

@media (max-width: 900px) { .hdr__burger { display: block; } }
/* The sheet is a mobile control only; if the viewport grows while it is open,
   close it out of the way rather than leaving a panel over a full nav. */
@media (min-width: 901px) { .navsheet[open] { display: none; } }

/* ---------- hero ---------- */
.hero {
  position: relative;
  min-height: 92svh;
  display: flex; align-items: flex-end;
  padding: 0 0 clamp(2.5rem, 6vw, 5rem);
  overflow: hidden;
  background: var(--ink);
}
/* --hp is the hero's scroll progress (0 at rest, 1 once it has fully left),
   set by main.js. The media box carries 25% of extra height ABOVE the hero and
   drifts down into it, so the image travels slower than the copy. The slack is
   what stops the bottom edge lifting away and exposing the background. */
.hero__media {
  position: absolute; inset: -25% 0 0 0;
  transform: translate3d(0, calc(var(--hp, 0) * 25%), 0);
  will-change: transform;
}
.hero__media img, .hero__media video {
  width: 100%; height: 100%; object-fit: cover; object-position: center 42%;
}
/* The hero clip is 720px wide: crisp and natively framed on a phone, but
   upscaled and muddy on a desktop canvas. So each viewport gets the asset
   that actually reads there. */
/* The still always renders, so the hero is never blank. The video is only
   revealed once main.js has given it a src, which it does on small screens
   only -- previously it was display:none on desktop yet still downloaded
   422KB, about 40% of a desktop visit's transfer, for nothing. */
.hero__still { display: block; }
.hero__vid { display: none; }
.hero__vid.is-ready { display: block; position: absolute; inset: 0; }
/* Scrim sits on .hero, not .hero__media, so it stays put while the media
   parallaxes underneath it. Both pseudo-elements paint below .hero__inner,
   which is the only child carrying a z-index. */
.hero::after {
  content: "";
  position: absolute; inset: 0;
  /* Two layers in one pseudo-element on purpose: ::after is the last child, so
     it is the only one guaranteed to paint above the (positioned) media box.
     Top layer deepens as the hero leaves, turning the handoff into the dark
     .claims strip into a dissolve; bottom layer is the legibility scrim. */
  background:
    linear-gradient(rgba(30,17,10,calc(var(--hp, 0) * .38)) 0 100%),
    linear-gradient(to top, rgba(30,17,10,.82) 0%, rgba(30,17,10,.35) 42%, rgba(30,17,10,.28) 70%, rgba(30,17,10,.5) 100%);
}
.hero__inner { position: relative; z-index: 1; width: 100%; color: var(--cream); }
.hero h1 { color: var(--cream); max-width: 15ch; margin-bottom: .35em; }
/* Semantically a subheading of the h1, so it is marked up as h2 and carries the
   search terms the h1 deliberately does not. Reset off the display styling. */
.hero__sub {
  max-width: min(52ch, 100%);
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  line-height: 1.5;
  letter-spacing: normal;
  color: rgba(247,243,236,.9);
  margin: 0 0 1.1em;
}
.hero__cta { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem; margin-top: 1.8rem; }
.hero__note { font-size: .85rem; color: rgba(247,243,236,.75); }

/* Hero headline rises from behind a mask, line by line. The outer span is the
   mask, the inner one moves; the padding/margin pair stops descenders (p, y)
   being clipped by the overflow. Motion is transform-only, so a browser that
   skips the animation still shows the headline in place. */
.rise > span {
  display: inline-block; overflow: hidden; vertical-align: bottom;
  padding-bottom: .12em; margin-bottom: -.12em;
}
.rise > span > span { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .rise > span > span {
    animation: line-rise .75s var(--ease) backwards;
    animation-delay: calc(var(--i) * var(--step, 90ms) + 150ms);
  }
  /* Service-page headlines are masked per word rather than per phrase, so the
     step has to be tighter or a ten-word headline takes a full second to land
     and reads as a typewriter instead of a reveal. */
  .hero--page .rise { --step: 45ms; }
  .hero__sub { animation: rise .6s var(--ease) .5s backwards; }
  .hero__cta { animation: rise .6s var(--ease) .62s backwards; }
  .hero__media img, .hero__media video { animation: kenburns 24s ease-out both; }
}
@keyframes rise { from { opacity: 0; transform: translateY(.8em); } }

/* Reserved for the two conversion moments on each page (the estimator and the
   closing band), not every heading. Applied to all nine it read as one gesture
   repeated, and it fought the section fade happening underneath it. Words are
   wrapped by main.js; with no script there is no class and the heading is
   simply there. */
.rise-on-view > span { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .rise-on-view.is-in > span {
    animation: rise .55s var(--ease) backwards;
    animation-delay: calc(var(--i) * 55ms);
  }
}
@keyframes line-rise { from { transform: translateY(105%); } }
@keyframes kenburns { from { transform: scale(1); } to { transform: scale(1.08); } }

/* service-page heroes: shorter than home, so the copy below starts sooner */
.hero--page { min-height: 62svh; padding-top: 7rem; }
.hero--page h1 { font-size: clamp(2.3rem, 5.4vw, 4rem); max-width: 18ch; }

/* ---------- trust strip (Azteca-style stacked claims) ---------- */
.claims {
  background: var(--ink);
  color: var(--cream);
  padding-block: 1.1rem;
  overflow: hidden;
}
.claims ul {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .5rem 2rem; margin: 0; padding: 0; list-style: none;
  font-size: .72rem; letter-spacing: .17em; text-transform: uppercase;
}
.claims li { display: flex; align-items: center; gap: 2rem; }
.claims li + li::before { content: "·"; opacity: .5; margin-left: -1.15rem; }

/* Home: a slow marquee, so the band under the hero is never dead. The list is
   duplicated and the track travels exactly half its width, which is what makes
   the loop seamless. */
.claims__track { display: flex; width: max-content; }
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; margin: -1px; padding: 0; border: 0;
}
.claims--marquee ul { flex-wrap: nowrap; justify-content: flex-start; padding-inline: 1rem; }
.claims--marquee ul::after { content: "·"; opacity: .5; } /* separator across the seam */
@media (prefers-reduced-motion: no-preference) {
  .claims--marquee .claims__track { animation: marquee 42s linear infinite; }
  .claims--marquee:hover .claims__track { animation-play-state: paused; }
  /* Service pages keep a single centred list that staggers in on scroll. The
     list itself must not also move, or it stacks with its own items. */
  .claims.reveal.is-in > ul { animation: none; }
  .claims.reveal.is-in li { animation: rise .5s var(--ease) backwards; }
  .claims.reveal.is-in li:nth-child(1) { animation-delay: 40ms; }
  .claims.reveal.is-in li:nth-child(2) { animation-delay: 130ms; }
  .claims.reveal.is-in li:nth-child(3) { animation-delay: 220ms; }
  .claims.reveal.is-in li:nth-child(4) { animation-delay: 310ms; }
}
@keyframes marquee { to { transform: translateX(-50%); } }
/* The global motion reset collapses animations to ~0s, which would freeze the
   marquee mid-travel and show the duplicate offset. Drop the copy and re-centre
   instead. Fires wherever that reset fires, mobile included. */
@media (prefers-reduced-motion: reduce), (max-width: 900px) {
  .claims--marquee .claims__track { width: 100%; justify-content: center; }
  .claims--marquee ul + ul, .claims--marquee ul::after { display: none; }
  .claims--marquee ul { flex-wrap: wrap; justify-content: center; }
  /* The track repeats the four claims five times so one copy is wider than any
     viewport. Standing still, that is 20 items wrapping into a block ten rows
     deep. Keep the first set only, which is exactly the static strip the
     service pages already use. (This was wrong under reduced-motion before it
     was wrong on mobile; nobody had looked at that path.) */
  .claims--marquee ul li:nth-child(n+5) { display: none; }
}

/* Utility text, and it now sits under the hero rather than above it, so it
   reads as "where am I" after the page has introduced itself. Quiet on
   purpose: no rule, no chip, no container. */
.crumbs {
  padding-top: clamp(1.5rem, 3vw, 2.25rem); padding-bottom: 0;
  font-size: .8rem; letter-spacing: .04em; color: var(--ink-soft);
}
.crumbs + section, .crumbs + script + section { padding-top: clamp(2rem, 4vw, 3.25rem); }
.crumbs a { color: var(--terracotta-d); }
.crumbs span { opacity: .6; margin-inline: .35rem; }
.crumbs [aria-current] { opacity: 1; margin-inline: 0; }

/* ---------- generic media + text ---------- */
.split {
  display: grid; gap: clamp(2rem, 5vw, 4.5rem); align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 860px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--reverse > :first-child { order: 2; }
}
.split figure {
  margin: 0;
  overflow: hidden;
  border-radius: var(--r);
  /* A resting shadow, not just on hover. These are the largest objects on the
     page and they were flat crops sitting directly on the background, so the
     photography read as printed onto the page rather than placed on it. */
  box-shadow: var(--e2);
  /* --fy is a small counter-scroll offset set by main.js while the block is in
     view; it defaults to 0, so with no script the figure just sits still. */
  transform: translate3d(0, var(--fy, 0px), 0);
}
.split img { width: 100%; border-radius: inherit; }

/* The image settles out of a slight zoom as the block enters, which pairs with
   the --fy drift above. The un-animated resting state is a 1.06 scale rather
   than hidden, so a browser that never adds .is-in still shows the photo. */
@media (prefers-reduced-motion: no-preference) {
  .split img { transition: transform .9s var(--ease); }
  .split.reveal:not(.is-in) img { transform: scale(1.06); }
}

/* The figure wipes and the text column staggers, so the two halves of the
   block arrive differently instead of sliding up together like everything
   else. .split--reverse mirrors the wipe, so the alternating rhythm on
   weddings and bottled is felt as well as seen. */
@media (prefers-reduced-motion: no-preference) {
  .split.reveal.is-in > figure { animation: wipe-up .8s var(--ease) both; }
  .split--reverse.reveal.is-in > figure { animation-name: wipe-down; }
  .split.reveal.is-in > figure img { animation: img-settle .9s var(--ease) both; }
  /* the text column itself must not also move, or the two stack up */
  .split.reveal.is-in > div { animation: none; }
  .split.reveal.is-in > div > * { animation: rise .55s var(--ease) backwards; }
  .split.reveal.is-in > div > :nth-child(1) { animation-delay: 60ms; }
  .split.reveal.is-in > div > :nth-child(2) { animation-delay: 140ms; }
  .split.reveal.is-in > div > :nth-child(3) { animation-delay: 220ms; }
  .split.reveal.is-in > div > :nth-child(4) { animation-delay: 300ms; }
  .split.reveal.is-in > div > :nth-child(n+5) { animation-delay: 380ms; }
}
@keyframes wipe-up   { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0); } }
@keyframes wipe-down { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }
@keyframes img-settle { from { transform: scale(1.06); } }

/* ---------- grids ---------- */
.grid { display: grid; gap: clamp(1.25rem, 2.5vw, 2rem); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

/* ---------- service cards (the one place that earns a real card) ----------
   These five are the primary navigation into the money pages, so they are the
   only elements on a light background allowed to sit ABOVE the page plane.
   Everything else that used to be a bordered box (testimonials, feature
   lists) has been demoted, which is what lets these read as primary.

   The border is gone: a raised surface plus a contact shadow already draws the
   edge, and the old opaque --sand outline was heavier than the card's own
   heading. What is left is a hairline at 8% purely to hold the edge on a
   tinted band, where paper-on-shell is a small step. */
.card {
  background: var(--paper);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-lg);
  overflow: hidden;
  display: flex; flex-direction: column;
  text-decoration: none;
  box-shadow: var(--e1);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
/* Lift only. The old hover also scaled the whole card 1.02, which resampled
   the photo and nudged the text baselines every time the pointer crossed it. */
.card:hover, .card:focus-visible { transform: translateY(-5px); box-shadow: var(--e2); }
.card img { aspect-ratio: 4/3; object-fit: cover; width: 100%; }
.card__body {
  padding: clamp(1.25rem, 2.2vw, 1.75rem);
  display: flex; flex-direction: column; flex: 1;
}
.card h3 { margin-bottom: .4rem; }
.card p { font-size: .95rem; color: var(--ink-soft); margin: 0; }
/* pinned to the bottom, so the arrow row lines up across cards of unequal copy */
.card__more {
  display: inline-block; margin-top: auto; padding-top: 1rem;
  font-size: .85rem; color: var(--terracotta-d); letter-spacing: .04em;
  transition: transform .25s var(--ease);
}
.card:hover .card__more, .card:focus-visible .card__more { transform: translateX(4px); }

/* ---------- the five services, deliberately unequal ----------
   auto-fit put five cards into a 3 + 2 layout at full width: the last two
   stretched to ~510px each, so their 4:3 crops rendered half again as tall as
   the row above. It read as a bug rather than a decision.

   Six columns fixes it and buys hierarchy at the same time: the two biggest
   revenue lines take half the width each on a wide landscape crop, the other
   three run three-up beneath. Same five cards, an actual reading order. */
.grid--services { grid-template-columns: repeat(6, 1fr); }
.grid--services > :nth-child(-n+2) { grid-column: span 3; }
.grid--services > :nth-child(n+3)  { grid-column: span 2; }
.grid--services > :nth-child(-n+2) img { aspect-ratio: 16/10; }
.grid--services > :nth-child(-n+2) h3 { font-size: clamp(1.35rem, 2.2vw, 1.75rem); }
.grid--services > :nth-child(-n+2) p { font-size: 1rem; }
@media (max-width: 900px) {
  /* Two-up, with the two leads still full width so they stay the leads. The
     last card widens as well: three cards into two columns otherwise leaves
     the fifth alone in a half-width slot with a hole beside it, which is the
     same orphan the six-column grid was introduced to fix. */
  .grid--services { grid-template-columns: repeat(2, 1fr); }
  .grid--services > :nth-child(-n+2) { grid-column: span 2; }
  .grid--services > :nth-child(n+3)  { grid-column: span 1; }
  .grid--services > :last-child { grid-column: span 2; }
  .grid--services > :last-child img { aspect-ratio: 16/9; }
}
@media (max-width: 620px) {
  .grid--services { grid-template-columns: 1fr; }
  .grid--services > * { grid-column: span 1 !important; }
  .grid--services > :nth-child(n+3) img { aspect-ratio: 16/9; }
}

/* ---------- ruled clusters ----------
   The "what's included" / "what we handle" / "milestones" lists were bare
   divs on flat bone: six items with nothing separating them and no anchor, so
   they ran together. Wrapping each in a card would have been six more boxes
   competing with the real cards on the home page.

   A rule above each item groups them as one cluster and separates them from
   each other, with no fill, no outline and no radius. It costs one border,
   stacks correctly at any column count, and stays clearly secondary to .card.
   The rule is short and terracotta rather than a full-width hairline, so the
   cluster reads as a designed set rather than a table. */
.grid--ruled, .steps { row-gap: clamp(2.25rem, 4vw, 3rem); }
.grid--ruled > * { padding-top: 1.35rem; position: relative; }
.grid--ruled > *::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--line-strong);
}
/* the accent segment sits on top of the hairline, at the heading's left edge */
.grid--ruled > *::after {
  content: "";
  position: absolute; top: 0; left: 0; width: 2.25rem; height: 2px;
  background: var(--terracotta);
  transform: translateY(-.5px);
}
.grid--ruled h3 { margin-bottom: .45rem; }
.grid--ruled p { font-size: .95rem; color: var(--ink-soft); margin: 0; }

/* ---------- numbered steps ----------
   The same ruled language as the clusters above, so the two secondary
   patterns read as one system rather than two inventions. The difference is
   the anchor: a cluster item gets a short terracotta segment, a step gets its
   numeral, which is what makes the sequence legible as a sequence. */
.steps { grid-template-columns: 1fr; }
@media (min-width: 820px) { .steps { grid-template-columns: repeat(3, 1fr); } }
.steps > * { padding-top: 1.35rem; position: relative; }
.steps > *::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--line-strong);
}
.step__n {
  font-family: var(--display); font-size: 2.6rem; line-height: 1;
  color: var(--amber); display: block; margin: .3rem 0 .7rem;
}

/* ---------- estimate tool ---------- */
.est { background: var(--ink); color: var(--cream); }
.est h2 { color: var(--cream); }
/* The panel sits inside an already-dark section, so it was a box inside a box
   inside a box: panel outline, then a bordered input, then the capture block's
   own divider, three hairlines deep. The panel's outline is gone and it is
   held by its surface alone -- lifted to 8% so it still separates from --ink,
   with a top highlight doing the work the border used to. Nesting is now two
   levels, and the inputs are the only outlined thing, which is correct because
   they are the only thing you can type into. */
.est__panel {
  display: grid; gap: clamp(1.5rem, 4vw, 3.25rem);
  grid-template-columns: 1fr;
  background: rgba(247,243,236,.06);
  border: 1px solid rgba(247,243,236,.16);
  border-radius: var(--r-lg);
  padding: clamp(1.5rem, 4vw, 2.75rem);
}
@media (min-width: 820px) { .est__panel { grid-template-columns: 1fr 1fr; } }
/* masterclasses + bottled: one column of fields, no live figure to sit beside,
   so the panel narrows rather than stretching a short form across the measure */
.est__panel--single { max-width: 720px; margin-inline: auto; }
@media (min-width: 820px) { .est__panel--single { grid-template-columns: 1fr; } }
.field { display: block; margin-bottom: 1.15rem; }
.field span {
  display: block; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase;
  margin-bottom: .45rem; color: rgba(247,243,236,.72);
}
.field input, .field select {
  width: 100%; padding: .85rem 1rem;
  background: rgba(247,243,236,.08);
  border: 1px solid rgba(247,243,236,.25);
  border-radius: var(--r-sm);
  color: var(--cream); font: inherit;
}
.field input:focus-visible, .field select:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
.field select option { color: var(--ink); }

.est__out { display: flex; flex-direction: column; justify-content: center; }
.est__label { font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; color: rgba(247,243,236,.7); }
.est__prompt {
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  color: rgba(247, 243, 236, .78);
  max-width: min(34ch, 100%);
  margin: 0;
}
.est__summary {
  margin: .3rem 0 .15rem;
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  font-weight: 500;
  color: var(--cream);
}
.est__range {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(2.2rem, 5.5vw, 3.6rem); line-height: 1.05;
  margin: .4rem 0 .6rem; color: var(--amber);
  font-variant-numeric: tabular-nums;
}
.est__fine { font-size: .88rem; color: rgba(247,243,236,.7); max-width: min(34ch, 100%); }
/* A rule inside the output column, not a filled panel-width well. Keeps the
   form to two columns -- your event on the left, your ballpark and then your
   details on the right -- which is the shape it was designed in. */
.est__capture {
  margin-top: 1.6rem;
  border-top: 1px solid rgba(247,243,236,.16);
  padding-top: 1.5rem;
}
.est__row { display: grid; gap: .8rem; grid-template-columns: 1fr; }
@media (min-width: 560px) { .est__row { grid-template-columns: 1fr 1fr; } }
/* Two columns inside a half-width column inside a panel is ~150px a field
   between 820 and 1000px, which is too narrow for an email address. Stack
   them through that band only; the two-up layout returns above it. */
@media (min-width: 820px) and (max-width: 1000px) {
  .est__row { grid-template-columns: 1fr; }
}
.hp { position: absolute; left: -9999px; }

/* ---------- gallery ---------- */
/* Eight identical squares on an even gap is a contact sheet, not an edit.
   A fixed four-column grid with the first frame taking a 2x2 block gives the
   set a lead image and a rhythm, and costs one nth-child rule. Below 620px it
   is a plain two-up, where a feature tile would just be a big square. */
.gal {
  display: grid; gap: clamp(.6rem, 1.2vw, .9rem);
  grid-template-columns: repeat(4, 1fr);
}
.gal button {
  padding: 0; border: 0; background: none; cursor: zoom-in; overflow: hidden;
  border-radius: var(--r);
  box-shadow: var(--e1);
  transition: box-shadow .3s var(--ease);
}
/* Eight tiles into a four-column grid with a 2x2 lead needs 12 cells and
   fills 11, so the bottom-right corner was a notch. Widening the last tile to
   two columns lands it on exactly 12, and gives the run a closing accent to
   answer the opening one. The same arithmetic works at three and two columns,
   which is why the spans are declared once and only the column count changes
   below. */
.gal button:first-child { grid-column: span 2; grid-row: span 2; }
.gal button:last-child { grid-column: span 2; }
.gal button:first-child img, .gal button:last-child img { aspect-ratio: auto; }
.gal img {
  width: 100%; height: 100%; aspect-ratio: 1; object-fit: cover;
  transition: transform .45s var(--ease);
}
.gal button:hover { box-shadow: var(--e2); }
.gal button:hover img { transform: scale(1.045); }
@media (max-width: 900px) { .gal { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 620px) {
  .gal { grid-template-columns: repeat(2, 1fr); }
  .gal button:first-child { grid-column: span 2; grid-row: span 1; }
  .gal button:first-child img { aspect-ratio: 16/10; }
}
dialog.lightbox {
  border: 0; padding: 0; background: transparent;
  max-width: min(94vw, 1100px); max-height: 92vh;
}
dialog.lightbox::backdrop { background: rgba(30,17,10,.88); }
dialog.lightbox img { max-height: 92vh; width: auto; margin-inline: auto; border-radius: var(--r); }

/* Open and close both animate. allow-discrete lets the dialog stay painted for
   the length of the exit transition, so the close no longer snaps -- and it
   needs no JS, which is why the click handler in main.js is untouched. */
dialog.lightbox, dialog.lightbox::backdrop {
  opacity: 0;
  transition: opacity .28s var(--ease), display .28s allow-discrete, overlay .28s allow-discrete;
}
dialog.lightbox[open], dialog.lightbox[open]::backdrop { opacity: 1; }
dialog.lightbox img { transition: transform .35s var(--ease); transform: scale(.96); }
dialog.lightbox[open] img { transform: none; }
@starting-style {
  dialog.lightbox[open], dialog.lightbox[open]::backdrop { opacity: 0; }
  dialog.lightbox[open] img { transform: scale(.96); }
}

/* ---------- testimonials ----------
   These were cream cards with a --sand outline: pixel for pixel the same
   treatment as the five service cards on the home page. Supporting evidence
   was carrying identical visual weight to the primary navigation, which is
   the main reason nothing on the page announced itself as important.

   The box is gone entirely. The quotes now sit directly on a tinted band with
   a rule and a hanging mark -- which also fixes the other failure here: the
   reviews range from five words to sixty, and an outlined box around a
   five-word quote was three-quarters empty and read as a broken render. With
   no box there is nothing to leave empty, so the ragged lengths become normal
   editorial texture. Whitespace instead of another border. */
.quotes {
  display: grid;
  gap: clamp(2rem, 4vw, 3.25rem);
  grid-template-columns: 1fr;
  align-items: start;
}
/* Same rule as the steps: three or one, never a two-plus-orphan row. */
@media (min-width: 820px) { .quotes { grid-template-columns: repeat(3, 1fr); } }
.quote { position: relative; padding-top: 1.5rem; }
.quote::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--line-strong);
}
/* The same anchor segment the feature clusters use, in amber rather than
   terracotta so the two secondary patterns stay distinguishable without
   inventing a third language. Amber fails text contrast, which is exactly why
   it is safe here: it is decoration and carries no meaning. Not a decorative
   quote glyph -- the markup already carries real typographic quotes, and two
   of them is one too many. */
.quote::after {
  content: "";
  position: absolute; top: 0; left: 0; width: 2.25rem; height: 2px;
  background: var(--amber); transform: translateY(-.5px);
}
.quote blockquote {
  margin: 0 0 1rem;
  font-family: var(--display); font-size: clamp(1.05rem, 1.4vw, 1.2rem);
  line-height: 1.45;
}
.quote cite {
  display: block; font-style: normal; font-size: .82rem;
  color: var(--ink-soft); letter-spacing: .04em;
}
[data-placeholder] { outline: 2px dashed var(--terracotta); outline-offset: 6px; }

/* ---------- menu list ---------- */
/* The list-style is removed on the items but the UL kept the browser's
   default 40px inline padding, so the whole menu sat indented from the
   heading and lede directly above it. Nothing else in the page
   is inset like that, so it read as a misalignment rather than a hierarchy. */
.menu { display: grid; gap: 0; max-width: 760px; margin: 0; padding: 0; }
.menu li { list-style: none; padding: 1.25rem 0; border-bottom: 1px solid var(--line); }
.menu li:first-child { padding-top: 0; }
/* The 404 index reuses .menu for its link list, so the links are un-decorated
   here rather than inline on each one; the row already reads as a target. */
.menu a { text-decoration: none; }
.menu a:hover { text-decoration: underline; text-underline-offset: .18em; }
.menu h3, .menu__h { margin: 0 0 .25rem; font-size: clamp(1.2rem, 1.8vw, 1.5rem);
  font-family: var(--display); font-weight: 600; letter-spacing: -.01em; line-height: 1.04; }
.menu p { margin: 0; font-size: .93rem; color: var(--ink-soft); }
.tag {
  display: inline-block; vertical-align: middle; margin-left: .55rem;
  padding: .22em .6em; border-radius: var(--r-xs);
  background: var(--terracotta); color: var(--cream);
  font: 500 .6rem/1 var(--body); letter-spacing: .12em; text-transform: uppercase;
}

/* Intro copy inside a dark section. Was an inline rgba on every estimator
   and enquiry block; it is the dark-ground counterpart of .lede. */
.lede--light { color: rgba(247, 243, 236, .82); }

/* ---------- utility pages (404, thanks) ----------
   One centred message, held short of the full measure. Both pages hand-rolled
   this inline with slightly different numbers; there is no reason for them to
   differ. */
.page-msg { min-height: 66svh; display: flex; align-items: center; }
.page-msg__body { max-width: 56ch; }
.page-msg__cta { margin-top: 2rem; margin-bottom: 0; }
.h1--sm { font-size: clamp(2.2rem, 5vw, 3.6rem); }

/* ---------- feature panel ----------
   For a single piece of content that has to stand alone and be noticed --
   currently the "margaritas in a can" announcement, which was naked centred
   text between two large blocks and simply vanished. This is the one place a
   strong standalone container is the right answer, so it gets the full
   treatment: raised surface, real padding, a dashed edge because the thing
   itself is not finished yet. Deliberately rare; if it appears twice on a
   page it has stopped meaning anything. */
.panel {
  max-width: 62ch; margin-inline: auto;
  padding: clamp(2rem, 5vw, 3.5rem);
  background: var(--paper);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--e1);
  text-align: center;
}
.panel > :last-child { margin-bottom: 0; }
.panel__h { font-size: clamp(1.7rem, 3.2vw, 2.4rem); }

/* A quiet caveat or aside under a block. No box, no rule: it is an aside,
   and the whitespace above it is separation enough. */
.note { margin-top: 1.75rem; color: var(--ink-soft); font-size: .95rem; }

/* Small uppercase trust line under a section. Restrained on purpose: it is
   reassurance, not a claim to sell on, so it gets no box and no colour. */
.trust {
  /* It is a rule across the section, not a paragraph, so it opts out of the
     62ch measure every other <p> inherits -- otherwise the line stops short of
     the content edge and reads as a truncation. */
  max-width: none;
  margin-top: var(--flow);
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  font-size: .8rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---------- FAQ ---------- */
.faq { max-width: 760px; }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer; list-style: none; padding: 1.3rem 2.5rem 1.3rem 0;
  font-family: var(--display); font-size: 1.1rem; position: relative;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; position: absolute; right: .2rem; top: 50%; translate: 0 -50%;
  font-size: 1.4rem; color: var(--terracotta);
  transition: rotate .25s var(--ease), scale .25s var(--ease);
}
.faq details[open] summary::after { rotate: 45deg; }
.faq details p { padding-bottom: 1.2rem; margin: 0; color: var(--ink-soft); }

/* The whole row is the target, so the whole row should react. */
.faq summary > span { display: inline-block; transition: translate .25s var(--ease); }
.faq summary:hover > span { translate: 4px 0; }
.faq summary:hover::after { scale: 1.15; }

/* The answer slides instead of snapping. ::details-content is Chromium-only
   today; everywhere else this is inert and <details> opens exactly as it does
   now, which is the whole point of doing it in CSS rather than JS. */
@media (prefers-reduced-motion: no-preference) {
  .faq { interpolate-size: allow-keywords; }
  .faq details::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size .3s var(--ease), content-visibility .3s allow-discrete;
  }
  .faq details[open]::details-content { block-size: auto; }
}

/* ---------- band + footer ---------- */
/* The strongest full-width feature section on every page, and it was a flat
   fill sitting directly on top of the flat ink footer: two heavy slabs meeting
   at a hard line. A warm off-centre glow gives it a light source, and the base
   darkens so it hands off into the footer instead of butting against it. */
.band {
  position: relative;
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(255,167,85,.45) 0%, transparent 62%),
    linear-gradient(var(--terracotta) 55%, var(--terracotta-d) 100%);
  color: var(--cream); text-align: center;
}
.band > .wrap { position: relative; }
.band h2 { color: var(--cream); }
.band p { margin-inline: auto; color: rgba(247,243,236,.92); }

.ftr { background: var(--ink); color: rgba(247,243,236,.78); font-size: .9rem; }
.ftr a { color: rgba(247,243,236,.78); }
.ftr__brand { color: var(--cream); margin-right: 0; }
.ftr__brand:hover { color: #fff; }
.ftr__grid { display: grid; gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.ftr__h {
  font: 500 .75rem/1 var(--body); letter-spacing: .16em; text-transform: uppercase;
  color: var(--amber); margin: 0 0 1rem;
}
.ftr ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.ftr__base {
  margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line-light);
  display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; justify-content: space-between;
  font-size: .82rem; color: rgba(247,243,236,.55);
}

/* ---------- video block ---------- */
.vid { position: relative; border-radius: var(--r); overflow: hidden; background: var(--ink); box-shadow: var(--e2); }
.vid video { width: 100%; aspect-ratio: 5/8; object-fit: cover; }
.vid--wide video { aspect-ratio: 16/10; }

/* ---------- scroll reveal ----------
   Nothing is hidden by default. The observer ADDS an animation when a section
   enters view; if it never runs, the content is simply there, un-animated.
   Two earlier versions got this wrong: one set opacity:0 and waited for JS
   (every section below the fold stayed blank when the script failed), and a
   CSS view() timeline stayed hidden whenever the page could not scroll.
   Decoration must never be load-bearing for legibility. */
@media (prefers-reduced-motion: no-preference) {
  .reveal.is-in { animation: reveal-in .75s var(--ease) both; }
  .reveal.is-in > * { animation: rise .5s var(--ease) backwards; }
  /* never stagger past ~8 children; the tail starts to feel laggy */
  .reveal.is-in > :nth-child(1) { animation-delay: 0ms; }
  .reveal.is-in > :nth-child(2) { animation-delay: 80ms; }
  .reveal.is-in > :nth-child(3) { animation-delay: 160ms; }
  .reveal.is-in > :nth-child(4) { animation-delay: 240ms; }
  .reveal.is-in > :nth-child(5) { animation-delay: 320ms; }
  .reveal.is-in > :nth-child(6) { animation-delay: 400ms; }
  .reveal.is-in > :nth-child(7) { animation-delay: 480ms; }
  .reveal.is-in > :nth-child(n+8) { animation-delay: 560ms; }
}
/* 42px rather than 24, over .75s rather than .55: the old travel was small
   enough that the block looked like it had simply appeared. Same gesture,
   enough distance to register as one. */
@keyframes reveal-in {
  from { opacity: 0; transform: translateY(42px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- mobile action bar ----------
   Replaces a hamburger menu. Two thumb-reachable actions beat a nav drawer
   on a page whose only job is to produce a call or an enquiry. */
.actionbar { display: none; }
@media (max-width: 900px) {
  .actionbar {
    position: fixed; inset: auto 0 0; z-index: 70;
    display: grid; grid-template-columns: 1fr 1fr;
    background: var(--cream);
    border-top: 1px solid var(--line);
    box-shadow: 0 -8px 24px -12px rgba(58,36,25,.28);
    padding: .55rem var(--gutter) calc(.55rem + env(safe-area-inset-bottom));
    gap: .55rem;
  }
  .actionbar .btn { justify-content: center; padding: .85rem 1rem; font-size: .9rem; }
  body { padding-bottom: 4.5rem; }
}

:focus-visible { outline: 2px solid var(--terracotta); outline-offset: 3px; }

.skip {
  position: absolute; left: -9999px; z-index: 99;
  background: var(--cream); color: var(--ink); padding: .8rem 1.2rem;
}
.skip:focus { left: var(--gutter); top: .6rem; }

/* ---------- page-to-page transitions ----------
   Six pages share a header, a footer and a palette, and currently hard-cut
   between each other. This crossfades them and holds the header in place, so
   moving between services feels like one site rather than six documents.
   Chromium-only; everywhere else navigation behaves exactly as it does now. */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .28s; }
.hdr { view-transition-name: hdr; }

/* Motion is off entirely below 900px. A phone viewport is short and the scroll
   is fast, so every reveal fires at once and the page reads as flicker rather
   than polish -- which is exactly what people reported. This also stops the
   ken-burns scale and the page-transition crossfade, both of which keep a
   composited layer alive on the device least able to afford it.

   The hero VIDEO is untouched: it is an element that plays, not a CSS
   animation, and it is the mobile hero by design. */
@media (prefers-reduced-motion: reduce), (max-width: 900px) {
  @view-transition { navigation: none; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
}
