/* tokens.css and fonts.css used to be @import-ed from here. They are <link>ed
   from every page's <head> instead, because an @import is only DISCOVERED once
   this file has been fetched and parsed -- so the browser could not start on
   either of them until styles.css had landed, and the critical path to first
   paint was three stylesheets deep in a straight line:

       HTML -> styles.css -> tokens.css -> fonts.css

   tokens.css holds every custom property on the site, so nothing could compute
   its final colour, radius or spacing until the LAST hop finished. As <link>s
   all three are found by the preload scanner in the initial HTML and fetched in
   parallel, which flattens that chain to one round trip.

   Cascade order does not matter here: tokens.css only defines custom properties
   on :root and nothing else redefines them, so the values resolve the same
   whichever sheet is parsed first. The <link> order still puts tokens first for
   readability. */

/* ============================================================
   1. BASE
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }
/* The <html> background is the CANVAS background: it is what a phone paints in
   the rubber-band gutter when you drag past the top or the bottom of the page.
   It was inheriting body's --plaster, so a hard flick at the end of any page
   opened a cream slab under the ink footer and the fixed call bar floated up
   with it (iOS drags position:fixed along during the bounce). Every page on the
   site opens on ink (.hero / .phead) and closes on ink (.foot), so ink is the
   correct gutter colour at BOTH ends. body keeps --plaster and paints only its
   own box, which is exactly the content.

   overscroll-behavior-y: none then removes the bounce itself where it is
   supported (Safari 16+, Chrome). The one thing it costs is pull-to-refresh on
   Android, which is not a gesture anyone needs on a five-page brochure site. */
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background: var(--ink);
  overscroll-behavior-y: none;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--plaster);
  color: var(--text);
  font-family: var(--f-body);
  font-size: var(--t-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 800;
  line-height: .98;
  letter-spacing: -.025em;
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: var(--t-hero); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); font-weight: 700; line-height: 1.15; letter-spacing: -.015em; }
p  { margin: 0 0 var(--s2); max-width: var(--maxw-text); text-wrap: pretty; }
a  { color: inherit; }
img, svg, video { display: block; max-width: 100%; height: auto; }
ul, ol { margin: 0; padding: 0; list-style: none; }

::selection { background: var(--gold-2); color: var(--ink); }

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

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink); color: var(--plaster); padding: var(--s2) var(--s3);
}
.skip:focus { left: 0; }

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.wrap--narrow { max-width: 920px; }

.eyebrow {
  font-family: var(--f-display);
  font-weight: 500;
  font-size: var(--t-eyebrow);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--gold-3);
  margin: 0 0 var(--s2);
  display: flex; align-items: center; gap: var(--s2);
}
.eyebrow::before {
  content: ''; width: 28px; height: 1px; background: currentColor; flex: none;
}
/* Centred, a single leading rule reads as a stray dash floating beside the label.
   Mirror it so the label sits inside a pair of rules instead. */
/* `justify-content` centres the flex LINE inside the box, but the box itself was
   still a full-width block starting at the left gutter -- which is why the label
   looked off-centre against the heading below it. It needs to shrink to its
   content and centre as a block. */
.eyebrow--center {
  justify-content: center; margin-bottom: var(--s1);
  width: fit-content; margin-inline: auto;
}
.eyebrow--center::after {
  content: ''; width: 28px; height: 1px; background: currentColor; flex: none;
}
/* GRADIENT ON DARK, SOLID ON LIGHT -- see section 1b. The eyebrow inherits its
   colour into the ::before/::after rules via currentColor, so on dark we cannot
   simply background-clip the <p> (that would erase the rules too). The text gets
   its own span-less treatment: clip the gradient to the text only, and repaint
   the hairlines with a solid gold. */
.on-dark .eyebrow, .sec--dark .eyebrow, .foot h2 {
  background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.on-dark .eyebrow::before, .sec--dark .eyebrow::before,
.on-dark .eyebrow::after,  .sec--dark .eyebrow::after { background: var(--gold-2); }
@media (forced-colors: active) {
  .on-dark .eyebrow, .sec--dark .eyebrow, .foot h2 { background: none; color: CanvasText; }
}

.lead { font-size: var(--t-lead); line-height: 1.55; color: var(--text-soft); }
.sec--dark .lead { color: var(--text-invert-soft); }

/* ---- curved section overlaps -------------------------------------------
   Every band after the hero pulls itself up over the one before it with a
   negative top margin and a large top radius, so section changes read as
   smooth curved overlaps instead of flat cutoffs. Sections paint in DOM
   order, so each one naturally covers the bottom edge of its predecessor.
   Same idea as the portfolio's SectionShell.

   Each band MUST declare its own background -- without one the curve reveals
   whatever is behind the page rather than the previous section. That is why
   plain `.sec` now sets `--plaster` explicitly; it used to inherit from body. */
.sec, .award, .trust, .proof, .marquee, .foot, .phead + .sec {
  position: relative;
  border-top-left-radius:  var(--radius-section);
  border-top-right-radius: var(--radius-section);
}
.sec + .sec, .award, .trust, .proof, .marquee, .foot,
.sec--dark, .sec--tint, .trust + .sec, .phead + .sec {
  margin-top: calc(var(--section-lift) * -1);
}
/* anchors must not land under the fixed nav */
.sec, .award, .trust, .proof { scroll-margin-top: calc(var(--nav-h) + var(--s2)); }

/* Top and bottom padding are the derived --section-pt / --section-pb, never a
   raw --section-y. The bottom one carries the lift the NEXT band is about to
   take out of it, which is the only reason the rhythm reads as even. */
.sec { padding-block: var(--section-pt) var(--section-pb); background: var(--plaster); }
.sec--dark { background: var(--ink); color: var(--text-invert); }
.sec--tint { background: var(--plaster-2); }

/* The hero is full-bleed and pinned; it is the one band that is never lifted
   and never rounded. The section AFTER it does the curving, over the top. */
.hero { border-radius: 0; margin-top: 0; }

/* grain */
.grain {
  position: fixed; inset: 0; z-index: 900; pointer-events: none; opacity: .34;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.45'/%3E%3C/svg%3E");
}

/* scroll progress hairline */
.progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 100%;
  transform: scaleX(0); transform-origin: 0 50%;
  background: var(--gold-2); z-index: 950; pointer-events: none;
}

/* ============================================================
   1b. GOLD SYSTEM
   Ported from the Cleverstack portfolio. Two hard rules:

   1. GRADIENT ON DARK, SOLID ON LIGHT. .gold-text only ever goes on ink;
      on plaster use solid var(--gold-3). A gradient clipped to text on a
      light ground goes muddy and drops below contrast.
   2. NEVER ANIMATE box-shadow. A glow that pulses lives on its own
      absolutely-positioned layer animated on OPACITY, which the compositor
      can handle. Animating box-shadow repaints every frame, forever --
      the portfolio's CSS records this as a real source of jank.
   ============================================================ */
.gold-text {
  background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
/* Windows High Contrast drops backgrounds, and this class paints its glyphs
   THROUGH one -- so `color: transparent` would leave the text invisible. */
@media (forced-colors: active) {
  .gold-text { background: none; color: CanvasText; }
}

/* --- media corners -------------------------------------------------------
   Every photograph on the site is rounded to --radius-card, so the images and
   the curved section overlaps read as one radius language. Two deliberate
   exceptions: the hero (its plate and cut-out layers share one coordinate
   space and must stay full-bleed edge to edge) and the award medallion (an
   alpha cut-out -- there is no rectangle there to round). ---------------- */
.proof__media,
.ba__frame,
.gal__item,
.video__frame,
.svc-detail__media,
.about-grid__media img,
.contact-grid .contact-aside picture img,
.lb img { border-radius: var(--radius-card); }
/* Safari has never reliably clipped an iframe to an ancestor's radius, so the
   frame's radius is restated on the iframe itself rather than trusting the
   parent's overflow:hidden. This is what rounds the Vimeo player. */
.video__frame iframe,
.video__poster { border-radius: inherit; }
.drop__thumb { border-radius: calc(var(--radius-card) * .4); }

/* Hairline gold gradient border, via the padding-box/border-box trick. */
.gold-ring {
  position: relative;
  background:
    linear-gradient(var(--ring-fill, var(--ink)), var(--ring-fill, var(--ink))) padding-box,
    linear-gradient(120deg, rgba(245,231,193,.9), rgba(212,175,55,.6) 50%, rgba(245,231,193,.9)) border-box;
  border: 1px solid transparent;
}
/* On light surfaces pale champagne reads as nothing, so the ring goes deep and
   picks up a soft drop shadow to lift it off the plaster. */
.gold-ring-dark {
  background:
    linear-gradient(var(--ring-fill, var(--plaster)), var(--ring-fill, var(--plaster))) padding-box,
    linear-gradient(120deg, rgba(212,175,55,.95), rgba(122,96,26,.85) 50%, rgba(212,175,55,.95)) border-box;
  border: 1.5px solid transparent;
  box-shadow: 0 4px 18px -4px rgba(122,96,26,.35);
}

/* The pulsing glow. Decorative and pointer-events:none so it can never swallow
   a click on the control underneath it. */
.gold-halo {
  pointer-events: none;
  box-shadow: 0 0 10px 1px rgba(212,175,55,.4), 0 0 26px 4px rgba(212,175,55,.25);
  animation: halo-pulse 2.8s ease-in-out infinite;
  will-change: opacity;
}
@keyframes halo-pulse {
  0%, 100% { opacity: .4; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .gold-halo { animation: none; opacity: .55; }
}

/* ============================================================
   2. BUTTONS
   ============================================================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .6em;
  font-family: var(--f-display); font-weight: 700; font-size: .95rem;
  letter-spacing: .01em;
  padding: 1.05em 1.9em;
  border: 1px solid transparent; border-radius: 999px;
  text-decoration: none; cursor: pointer;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out), transform .25s var(--ease-out);
  will-change: transform;
}
/* --- brass/primary: gold gradient fill, with a pulsing halo on dark ------- */
.btn--brass {
  background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  color: var(--ink);
  position: relative; isolation: isolate;
}
.btn--brass:hover { transform: translateY(-2px); }
.btn--brass:active { transform: translateY(0); }

/* --- ghost: gold ring outline that FILLS FROM THE BOTTOM on hover and drains
   again on leave. The fill is a child layer scaled on Y, so only transform
   animates. ------------------------------------------------------------- */
.btn--ghost {
  border-color: var(--rule); color: var(--text);
  position: relative; overflow: hidden; isolation: isolate;
}
.btn--ghost::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: var(--ink);
  transform: scaleY(0); transform-origin: 50% 100%;
  transition: transform .45s var(--ease-snap);
}
.btn--ghost:hover { border-color: var(--ink); color: var(--plaster); }
.btn--ghost:hover::before { transform: scaleY(1); }

/* On ink the ghost becomes the portfolio's gold-ring pill: gradient hairline
   border, gold gradient filling up from the bottom, text flipping to ink. */
.on-dark .btn--ghost, .sec--dark .btn--ghost, .foot .btn--ghost {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--ink), var(--ink)) padding-box,
    linear-gradient(120deg, rgba(245,231,193,.9), rgba(212,175,55,.6) 50%, rgba(245,231,193,.9)) border-box;
  color: var(--gold-1);
}
.on-dark .btn--ghost::before, .sec--dark .btn--ghost::before, .foot .btn--ghost::before {
  background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
}
.on-dark .btn--ghost:hover, .sec--dark .btn--ghost:hover, .foot .btn--ghost:hover {
  color: var(--ink);
}

/* The halo is a SIBLING layer, not a box-shadow transition -- see the note in
   section 1b. Markup: <a class="btn btn--brass"><span class="btn__halo"></span>…
   It is added by JS so a no-JS render is a plain button. */
.btn__halo {
  position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  pointer-events: none;
  box-shadow: 0 0 10px 1px rgba(212,175,55,.4), 0 0 26px 4px rgba(212,175,55,.25);
  animation: halo-pulse 2.8s ease-in-out infinite;
  will-change: opacity;
}
@media (prefers-reduced-motion: reduce) {
  .btn__halo { animation: none; opacity: .55; }
  .btn--brass:hover { transform: none; }
  .btn--ghost::before { transition: none; }
}

.btn__ico { width: 1em; height: 1em; }

/* --- button rows ---------------------------------------------------------
   This had NO base rule, which caused two visible bugs at once. The buttons
   are inline-flex, so a row of them was held apart by nothing but one
   collapsed text space -- they looked glued together. And several rows are
   marked up as <p style="justify-content:center">, which does nothing at all
   on a non-flex box; worse, the base `p` rule caps them at --maxw-text, so a
   "centred" row was really centred inside a 68ch column sitting on the left.
   Making the row a real flex container fixes both. --------------------- */
.btn-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2);
  max-width: none; margin-bottom: 0;
}
.btn-row > .btn { flex: 0 0 auto; }
/* Below 480px a two-button row of full-width pills reads better stacked than
   squeezed, and 44px touch targets must not end up shoulder to shoulder. */
@media (max-width: 479px) {
  .btn-row { flex-direction: column; align-items: stretch; gap: var(--s2); }
  .btn-row > .btn { width: 100%; }
}

/* ============================================================
   3. NAV — hidden over the hero, slides in when the hero releases
   ============================================================ */
/* Dark glass on every page, matching the portfolio. Was light plaster and went
   dark only over the hero; Palmer wanted the darker treatment throughout. */
.nav {
  position: fixed; inset-inline: 0; top: 0; z-index: 800;
  height: var(--nav-h);
  display: flex; align-items: center;
  background: rgba(10,10,12,.72);
  backdrop-filter: saturate(1.4) blur(16px);
  -webkit-backdrop-filter: saturate(1.4) blur(16px);
  border-bottom: 1px solid rgba(212,175,55,.16);
  color: var(--plaster);
  transform: translateY(-100%);
}
.nav a, .nav .brand { color: var(--plaster); }
.nav__links a { color: rgba(245,242,237,.66); transition: color .3s var(--ease-out); }
.nav__links a:hover, .nav__links a[aria-current="page"] { color: var(--gold-1); }
.nav__tel { color: var(--plaster); }
.nav__tel:hover { color: var(--gold-1); }
.nav.is-visible { transform: translateY(0); }
.nav__inner {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: var(--s3);
}
.brand {
  font-family: var(--f-display); font-weight: 800; font-size: 1.32rem;
  letter-spacing: -.02em; text-decoration: none; line-height: 1;
  display: flex; align-items: baseline; gap: .5rem; margin-right: auto;
}
/* The whole wordmark carries the hero TV's ramp, dot included, as ONE gradient
   across the word rather than a plain word plus a gold full stop. The nav is
   permanently on ink (rgba(10,10,12,.72)), so the dark-surface ramp is correct.
   .brand__dot must give up its own background and colour, otherwise it clips a
   second, independent gradient and the full stop steps out of the ramp. */
.brand__mk {
  letter-spacing: -.04em;
  background-image: var(--grad-gold-lit);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.brand__dot { background: none; color: inherit; }
@media (forced-colors: active) {
  .brand__mk { background: none; color: CanvasText; }
}
/* --text-invert-soft, NOT --text-soft. The nav sits on ink on every page --
   transparent over the dark hero on the homepage, solid ink via .nav--static
   everywhere else -- so the light-ground token was being painted on a dark
   ground: #5A5A5F on #0B0B0D measures 2.86:1 at 10.88px, well under the 4.5:1
   this size needs. The dark-ground token measures ~9:1. Same rule as the gold
   scale in tokens.css: pick the token that matches the GROUND, and re-measure
   rather than eyeball it. */
.brand__sub {
  font-family: var(--f-body); font-weight: 500; font-size: .68rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--text-invert-soft);
}
.nav__links { display: flex; gap: var(--s3); align-items: center; }
.nav__links a {
  font-family: var(--f-display); font-weight: 500; font-size: .93rem;
  text-decoration: none; padding: .4rem 0; position: relative;
}
/* "Tubelight": the underline is a gold bar with a soft bloom above it, the same
   idea as the portfolio's active tab lamp reduced to a single hairline. */
.nav__links a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 0; height: 2px;
  border-radius: 2px 2px 0 0;
  background: linear-gradient(90deg, var(--gold-2), var(--gold-1), var(--gold-2));
  box-shadow: 0 -4px 12px rgba(212,175,55,.55), 0 -1px 4px rgba(245,231,193,.7);
  transition: right .35s var(--ease-out);
}
.nav__links a:hover::after, .nav__links a[aria-current="page"]::after { right: 0; }
.nav__tel {
  font-family: var(--f-display); font-weight: 700; text-decoration: none;
  white-space: nowrap;
}
.nav__cta {
  padding: .7em 1.3em; font-size: .88rem;
  position: relative; overflow: hidden; isolation: isolate;
  border: 1px solid transparent;
  background:
    linear-gradient(#111009, #111009) padding-box,
    linear-gradient(120deg, rgba(245,231,193,.9), rgba(212,175,55,.6) 50%, rgba(245,231,193,.9)) border-box;
  color: var(--gold-1) !important;
}
.nav__cta::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  transform: scaleY(0); transform-origin: 50% 100%;
  transition: transform .35s var(--ease-snap);
}
.nav__cta:hover { color: var(--ink) !important; }
.nav__cta:hover::before { transform: scaleY(1); }
@media (prefers-reduced-motion: reduce) { .nav__cta::before { transition: none; } }


/* mobile menu uses <details> so it works with JS off */
.menu { display: none; }

@media (max-width: 900px) {
  .nav__links, .nav__tel, .brand__sub { display: none; }
  .menu { display: block; margin-left: auto; }
  .menu > summary {
    list-style: none; cursor: pointer; padding: .6rem; margin: -.6rem;
    display: flex; align-items: center; gap: .5rem;
    font-family: var(--f-display); font-weight: 700; font-size: .9rem;
  }
  .menu > summary::-webkit-details-marker { display: none; }
  .menu__bars { width: 22px; height: 12px; position: relative; }
  .menu__bars::before, .menu__bars::after {
    content: ''; position: absolute; left: 0; right: 0; height: 1.5px; background: currentColor;
    transition: transform .3s var(--ease-out);
  }
  .menu__bars::before { top: 0; } .menu__bars::after { bottom: 0; }
  .menu[open] .menu__bars::before { transform: translateY(5px) rotate(45deg); }
  .menu[open] .menu__bars::after  { transform: translateY(-5px) rotate(-45deg); }
  /* .nav has a transform and a backdrop-filter, both of which make it the
     containing block for fixed children — so position the panel against it. */
  .menu__panel {
    position: absolute; top: 100%; left: 0; right: 0;
    height: calc(100svh - var(--nav-h)); background: var(--ink);
    padding: var(--s4) var(--gutter) var(--s6);
    display: flex; flex-direction: column; gap: var(--s1);
    overflow-y: auto;
  }
  /* Gradient lettering on ink -- the sanctioned direction (gradient on dark,
     solid on light). The border-bottom is unaffected: background-clip:text
     clips the BACKGROUND to the glyphs, it does not clip borders. */
  .menu__panel a {
    font-family: var(--f-display); font-weight: 700;
    font-size: clamp(1.6rem, 7vw, 2.2rem); text-decoration: none;
    padding: .45em 0; border-bottom: 1px solid var(--rule-invert);
    background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
  }
  /* Hover must NOT set a solid `color` -- that would paint over the clipped
     gradient and flip the link from gradient to flat mid-interaction. Shift the
     whole gradient's brightness instead. */
  .menu__panel a:hover { filter: brightness(1.18); }
  .menu__panel .btn { margin-top: var(--s3); }
  /* `.menu__panel a` (0,1,1) outranks `.btn--brass` (0,1,0), so the call pill
     inherited the panel's link colour and rendered near-WHITE text on the gold
     gradient. This wins at (0,2,0) and restores black-on-gold. It also has to
     undo the clip, or the pill's own gradient fill would be clipped to its text. */
  .menu__panel .btn--brass {
    color: var(--ink);
    -webkit-background-clip: border-box;
            background-clip: border-box;
  }
  .menu__panel .btn--brass:hover { filter: none; }
  @media (forced-colors: active) {
    .menu__panel a { background: none; color: CanvasText; }
  }
}

/* Narrow phones: the sticky call bar carries the CTA, so the nav keeps only
   the wordmark and the menu. */
@media (max-width: 520px) {
  .nav__cta { display: none; }
  .brand { font-size: 1.2rem; }
}

/* ============================================================
   4. HERO — full-bleed camera plate
   One plate holds the master photograph AND every cut-out in a
   single coordinate space, so the layers can never drift off the
   image. The plate is the ONLY transformed element: it carries the
   camera dolly, and every layer inherits it for free.

   Two masters, switched on ASPECT RATIO (not width): 16:9 for
   landscape viewports, 3:4 for portrait. Cover behaviour is a
   function of aspect ratio alone; copy layout is a function of
   width alone. Keeping those axes separate is what stops iPad
   portrait and landscape phones from becoming special cases.

   Layer positions are generated from the measured quads — run
   brand/sync_hero_css.py, never hand-edit the block below.
   ============================================================ */

/* HERO:VARS:BEGIN — generated by brand/sync_hero_css.py, do not hand-edit */
.hero {
  --ar-w: 16; --ar-h: 9;
  --cam-x: 50%; --cam-y: 39.925%; --cam-start: 1.40;
  --gl-x: 11.994%; --gl-y: 1.986%; --gl-w: 77.989%; --gl-h: 73.991%;
  --br-x: 40.579%; --br-y: 31.803%; --br-w: 19.227%; --br-h: 17.969%;
  --tv-x: 34.151%; --tv-y: 23.307%; --tv-w: 31.734%; --tv-h: 33.236%;
  --sb-x: 38.253%; --sb-y: 63.574%; --sb-w: 27.925%; --sb-h: 3.809%;
  --pr-x: 23.494%; --pr-y: 50.977%; --pr-w: 54.001%; --pr-h: 15.820%;
}
@media (max-aspect-ratio: 9/10) {
  .hero {
    --ar-w: 3; --ar-h: 4;
    --cam-x: 50%; --cam-y: 44.675%; --cam-start: 1.22;
    --gl-x: 1.981%; --gl-y: 9.981%; --gl-w: 96.010%; --gl-h: 62.000%;
    --br-x: 34.459%; --br-y: 40.134%; --br-w: 35.575%; --br-h: 15.317%;
    --tv-x: 23.940%; --tv-y: 33.145%; --tv-w: 52.232%; --tv-h: 23.059%;
    --sb-x: 40.904%; --sb-y: 63.967%; --sb-w: 25.781%; --sb-h: 2.637%;
    --pr-x: 6.975%; --pr-y: 54.488%; --pr-w: 87.500%; --pr-h: 12.701%;
  }
}
/* HERO:VARS:END */

/* The plate is sized against the LARGE viewport so that when the iOS URL bar
   collapses and the visual viewport grows, the plate is already big enough to
   cover it — no letterbox, no reflow. */
:root { --hero-vh: 100vh; }
@supports (height: 100lvh) { :root { --hero-vh: 100lvh; } }

.hero {
  position: relative;
  isolation: isolate;
  height: 100svh;              /* layout height = SMALL viewport, so copy always fits */
  overflow: hidden;
  background: var(--ink);
  color: var(--text-invert);
  display: grid;
  align-items: center;
}

/* --- the plate: one coordinate space for the photograph and every cut-out --- */
/* A centring frame that is NEVER transformed. Grid centring rather than a
   translate matters: it leaves the plate's transform entirely to GSAP, so the
   camera scale can't compound with a centring translate. */
.hero__stage {
  position: absolute; inset: 0; z-index: 0;
  display: grid; place-items: center;
  overflow: hidden;
}
.hero__plate {
  position: relative;
  /* Cover, without object-fit. Width resolves to max(viewport, height x ratio),
     and aspect-ratio then makes the height >= the viewport in both branches. */
  width: max(100%, calc(var(--hero-vh) * var(--ar-w) / var(--ar-h)));
  aspect-ratio: var(--ar-w) / var(--ar-h);
  height: auto;
  transform-origin: var(--cam-x) var(--cam-y);
  backface-visibility: hidden;
  /* no transform at rest — the resting state IS the final frame */
}
/* Only with JS do we start zoomed in, so a no-JS render is never mid-animation. */
.js .hero__plate { transform: scale(var(--cam-start)); }

/* The box already carries the master's ratio, so the image fills it 1:1.
   `fill` (not `cover`) means a one-pixel export error degrades to a sub-pixel
   stretch instead of a crop — registration survives a rounding mistake. */
.hero__base {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: fill; z-index: 1;
}

/* cut-outs, positioned in % of the plate */
.hero__layer { position: absolute; height: auto; }
/* Stacking order matters more than it looks. Nothing here physically overlaps
   — the books sit left of the soundbar, the vases right of it, and none of them
   reach the TV. But a difference matte always carries faint drift, so whichever
   layer sits on top paints its ghosting onto the ones below. Putting the props
   UNDER the TV and the soundbar hides that ghosting behind solid objects, which
   is why no part of the props matte has to be cut away. */
.hero__glow     { left: var(--gl-x); top: var(--gl-y); width: var(--gl-w); z-index: 2; }
.hero__bracket  { left: var(--br-x); top: var(--br-y); width: var(--br-w); z-index: 3; }
.hero__props    { left: var(--pr-x); top: var(--pr-y); width: var(--pr-w); z-index: 5; }
.hero__tv       { left: var(--tv-x); top: var(--tv-y); width: var(--tv-w); z-index: 6; }
.hero__soundbar { left: var(--sb-x); top: var(--sb-y); width: var(--sb-w); z-index: 7; }

/* structural x-ray + the cable, drawn in the master's own pixel space */
.hero__gfx {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 4; overflow: visible; pointer-events: none;
}
.hero__gfx--top { z-index: 8; }
/* one x-ray per master; only the active orientation's pair is in play */
.hero__gfx--p { display: none; }
@media (max-aspect-ratio: 9/10) {
  .hero__gfx--l { display: none; }
  .hero__gfx--p { display: block; }
}

/* --- the scrim: a SIBLING of the plate, so it never moves with the camera.
   As the dolly pulls back and different marble slides under the headline, the
   guaranteed contrast floor stays exactly where it is. --- */
/* --- the wordmark on the TV screen ---------------------------------------
   Sits INSIDE .hero__plate, boxed on the TV layer's own generated geometry, so
   the camera transform carries it and it stays registered to the panel at every
   scroll position and on both masters. Nothing here is animated by CSS: GSAP
   owns its opacity/scale on the same timeline as every other beat.

   Width is 50% of the TV box (--tv-w) rather than a fixed size, so it scales
   with the panel on every viewport instead of needing a breakpoint per device.
   font-size is driven off that same width in cqw units via a container query,
   which is what keeps the lettering the same proportion of the screen on a
   390px phone and a 5K display. */
.hero__logo {
  position: absolute;
  left: var(--tv-x); top: var(--tv-y);
  width: var(--tv-w); height: var(--tv-h);
  /* Two lines, so `align-content` has to be stated. Left at its `normal`
     default the two auto rows STRETCH to fill the panel height and the pair
     pulls apart to the top and bottom edges; `center` collapses the tracks to
     their content and centres the lockup as one block. `justify-items` keeps
     each line horizontally centred on the other. */
  display: grid;
  align-content: center; justify-items: center;
  /* No row-gap: at line-height 1 each line's box already carries 20.5% of its
     own font-size as unused descender space below the caps, which puts 4.1cqw
     of clear air between the wordmark's baseline and the sub line's cap tops.
     Adding a gap on top of that pulls the lockup apart. */
  row-gap: 0;
  container-type: inline-size;
  pointer-events: none;
  opacity: 0;              /* GSAP reveals it; 0 here so a no-JS render is clean */
  /* ABOVE the TV cut-out (z-index 6) -- the wordmark is on the screen, not
     behind the panel. Left at `auto` it painted underneath and was invisible
     even at full opacity, which looks exactly like a broken animation. 9 clears
     the soundbar (7) and the x-ray overlay (8) too. */
  z-index: 9;
}
/* One line of the lockup: the fill and its streak stacked on a single
   baseline. `max-content` matters -- the shine's background-size is a
   percentage of ITS OWN box, so a line that stretched to the panel width
   would sweep a streak sized for the panel, not for the glyphs. */
.hero__logo-line {
  display: grid;
  width: max-content; max-width: 100%;
}
.hero__logo-word,
.hero__logo-shine {
  grid-area: 1 / 1;         /* stacked, not offset -- one baseline, no drift */
  font-family: var(--f-display); font-weight: 800;
  /* Measured, not guessed: 15cqw rendered the word at 40.9% of the panel, so
     18.3cqw puts it at ~50%. Re-measure if the wordmark or face changes. */
  font-size: 18.3cqw;
  letter-spacing: -.04em; line-height: 1;
  white-space: nowrap;
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
/* White into gold and back, the same 100deg ramp the rest of the site uses --
   just starting from plaster instead of gold-1, which is what makes it read as
   lit glass rather than a flat gold sticker. */
.hero__logo-word {
  background-image: var(--grad-gold-lit);
  filter: drop-shadow(0 2px 18px rgba(0, 0, 0, .55));
}
.hero__logo-dot { color: transparent; }

/* --- the second line: TV MOUNTING KELOWNA --------------------------------
   Same box, same gold ramp, same reveal beat -- it is a child of #heroLogo, so
   GSAP's one opacity tween on the container brings both lines up together. Do
   not give this its own beat: "at the exact same time" is a structural
   guarantee here, not a timing coincidence to keep in sync by hand.

   Sized to land at roughly the same width as TVMK. above it, so the lockup
   reads as one block with two flush edges rather than two stacked words. Like
   the wordmark's 18.3cqw that is MEASURED against the rendered panel, not
   derived -- re-measure if the face or the copy changes. */
.hero__logo-word--sub,
.hero__logo-shine--sub {
  font-weight: 700;
  /* Not a guess: measured off the shipped faces. TVMK. sets 2.746em wide in
     Cabinet Grotesk 800 at -.04em tracking, so 18.3cqw renders it at 50.2% of
     the panel. TV MOUNTING KELOWNA sets 16.91em in the 700 face at .3em, so
     2.97cqw put it within half a percent -- 2.995cqw is that corrected against
     the real render, because the metrics sum ignores the kern pairs in TV and
     KELOWNA. Both lines now measure 49.9% of the panel: the lockup has two
     flush edges and reads as one block. */
  font-size: 2.995cqw;
  letter-spacing: .3em;
  text-transform: uppercase;
  /* letter-spacing also pads the LAST glyph, which would push the line half a
     tracking unit left of centre. Pull that trailing space back off. */
  margin-right: -.3em;
}
/* Optical centring. The grid centres the two BOXES, but neither line uses its
   descender space -- these are caps and a full stop. That leaves 2.29cqw of
   dead box above the wordmark's caps against 0.61cqw below the sub line's
   baseline, so the ink as a whole sits 0.84cqw low. This margin is the 1.68cqw
   imbalance; centring a block that is 1.68 taller than the ink lifts the ink
   by half of it, onto the true centre of the panel. */
.hero__logo-line--sub { margin-bottom: 1.68cqw; }

.hero__logo-word--sub {
  /* the wordmark's 18px shadow is sized for 18.3cqw glyphs; at a fifth of that
     size it reads as a smudge rather than a lift */
  filter: drop-shadow(0 1px 6px rgba(0, 0, 0, .6));
}

/* The clean-sweep streak, the same idea as .award__shine but clipped to the
   glyphs instead of a medallion silhouette: a narrow white band on an
   oversized background, moved across by GSAP (background-position, one short
   one-off tween -- not a loop, and never a box-shadow). */
.hero__logo-shine {
  background-image: linear-gradient(100deg,
      rgba(255,255,255,0) 42%, rgba(255,255,255,.92) 50%, rgba(255,255,255,0) 58%);
  background-size: 300% 100%;
  background-position: 200% 0;
  opacity: 0;
}
/* The two lines are near enough the same width that one shared tween crosses
   both at the same rate; the sub line only wants a softer streak, because a
   .92 white band over 3cqw glyphs blows them out. */
.hero__logo-shine--sub {
  background-image: linear-gradient(100deg,
      rgba(255,255,255,0) 40%, rgba(255,255,255,.75) 50%, rgba(255,255,255,0) 60%);
}
/* Windows High Contrast drops backgrounds, and both layers paint their glyphs
   THROUGH one -- so transparent text would vanish entirely. */
@media (forced-colors: active) {
  .hero__logo-word { background: none; color: CanvasText; }
  .hero__logo-shine { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__logo { opacity: 1; }      /* shown, never animated */
  .hero__logo-shine { display: none; }
}

.hero__scrim {
  position: absolute; inset: 0; z-index: 9; pointer-events: none;
  background:
    linear-gradient(100deg,
      rgba(8,8,10,.92)  0%,
      rgba(8,8,10,.80) 26%,
      rgba(8,8,10,.36) 52%,
      rgba(8,8,10,0)   76%),
    linear-gradient(to bottom, rgba(8,8,10,.55) 0%, rgba(8,8,10,0) 18%),
    linear-gradient(to top,    rgba(8,8,10,.55) 0%, rgba(8,8,10,0) 22%);
}

/* --- copy --- */
.hero__inner {
  position: relative; z-index: 10;
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: var(--s6) var(--gutter) var(--s5);
}
.hero__copy { max-width: 46ch; }
.hero h1 { color: var(--plaster); margin-bottom: var(--s3);
           text-shadow: 0 1px 22px rgba(0,0,0,.55); }
.hero h1 em { font-style: normal; color: var(--gold-1); display: block; }
.hero__lead { color: var(--text-invert-soft); font-size: var(--t-lead);
              margin-bottom: var(--s3); max-width: 40ch;
              text-shadow: 0 1px 22px rgba(0,0,0,.55); }
.hero .btn-row { margin-bottom: var(--s4); }
.hero__meta {
  display: flex; flex-wrap: wrap; gap: var(--s1) var(--s3);
  font-size: var(--t-small); color: var(--text-invert-soft);
  border-top: 1px solid var(--rule-invert); padding-top: var(--s3);
  max-width: 46ch;
}
.hero__meta strong { color: var(--plaster); font-weight: 600; }

/* Scroll cue. Persists for the whole pinned hero (JS dims it once the headline
   goes), so it has to stay legible over BOTH the near-black marble and the bright
   gold veining -- hence the text-shadow rather than a flat colour. */
.hero__cue {
  position: absolute; left: 50%; bottom: 1.4rem; transform: translateX(-50%);
  z-index: 10; display: flex; flex-direction: column; align-items: center; gap: .35rem;
  font-family: var(--f-display); font-weight: 700;
  font-size: .68rem; letter-spacing: .3em; text-transform: uppercase;
  color: rgba(245,242,237,.86);
  text-shadow: 0 1px 10px rgba(0,0,0,.85), 0 0 26px rgba(0,0,0,.6);
  text-decoration: none;
}
.hero__cue:hover { color: var(--gold-1); }
/* Progress fill under the cue label. Desktop never shows it -- the desktop hero
   was signed off as is and its cue is not covered by anything. */
.hero__cue-bar {
  display: none; width: 104px; height: 2px; border-radius: 2px;
  background: rgba(245,242,237,.22); overflow: hidden;
}
.hero__cue-bar i {
  display: block; width: 100%; height: 100%; transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, var(--gold-2), var(--gold-1));
}
.hero__cue svg {
  width: 18px; height: 18px; display: block;
  filter: drop-shadow(0 1px 8px rgba(0,0,0,.9));
  animation: cue-bob 1.8s ease-in-out infinite;
}
@keyframes cue-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) { .hero__cue svg { animation: none; } }

/* The grain overlay is fixed, full-viewport and mix-blend-mode — exactly the
   kind of layer that stutters over a scrubbing full-screen image. Keep it off
   the hero entirely, and off phones. */
.hero-live .grain { opacity: 0; }
@media (max-width: 767px) { .grain { display: none; } }

/* The nav now reveals DURING the pin, over the photograph. A full-width
   backdrop-filter over an actively scrubbing image is one of the most
   expensive things you can ask a mid-range phone to do — go opaque instead.
   `.hero-live` is set on <html> by the hero ScrollTrigger's onToggle. */
/* The nav is dark everywhere now, so this override is no longer about colour --
   it exists ONLY to drop the backdrop-filter while the hero is scrubbing. A
   full-width blur over a 4K image being scrubbed is one of the most expensive
   things you can ask a mid-range phone to do. Do not delete it just because the
   two states now look almost identical. */
.hero-live .nav {
  background: rgba(10,10,12,.96);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}

/* ---- short viewports: the hero is pinned, so nothing below the fold during
   the pin is reachable. These keep the whole copy block inside 100svh. ---- */
@media (min-width: 768px) and (max-height: 900px) {
  .hero__inner { padding-block: var(--s5) var(--s4); }
  .hero h1 { margin-bottom: var(--s2); }
  .hero__lead { font-size: 1.02rem; margin-bottom: var(--s3); }
  .hero .btn-row { margin-bottom: var(--s3); }
  .hero__meta { padding-top: var(--s2); }
}
@media (min-width: 768px) and (max-height: 740px) {
  .hero__inner { padding-block: var(--s4) var(--s3); }
  .hero h1 { font-size: clamp(1.9rem, 3.1vw, 2.5rem); }
  .hero__meta { display: none; }
  .hero .btn { padding: .9em 1.5em; font-size: .9rem; }
}

/* ---- tablet ---- */
@media (max-width: 1023px) {
  .hero__copy { max-width: 38ch; }
}

/* ---- portrait viewports: copy sits at the bottom, scrim flips to match ---- */
@media (max-aspect-ratio: 9/10) {
  .hero { align-items: end; }
  /* Stops are measured from the BOTTOM. On a 390x844 phone the headline copy
     occupies 0-48% and the bracket/TV sit at 42-68%, so this gradient was doing
     two jobs at once: holding contrast under the text AND dimming the hardware
     by up to 62% black. A matte-black bracket on near-black marble under 62%
     dimming is simply invisible, which is why the animation read as "nothing
     happened" on a phone.
     Everything from 0-42% is UNCHANGED, so headline contrast is untouched
     (re-measured, not assumed). Only the ramp above the copy is steepened, which
     roughly halves the dimming over the TV: ~0.42 -> ~0.19 at 55%. */
  .hero__scrim {
    background:
      linear-gradient(to top,
        rgba(8,8,10,.95)  0%,
        rgba(8,8,10,.90) 24%,
        rgba(8,8,10,.62) 42%,
        rgba(8,8,10,.30) 50%,
        rgba(8,8,10,.08) 60%,
        rgba(8,8,10,0)   72%),
      linear-gradient(to bottom, rgba(8,8,10,.55) 0%, rgba(8,8,10,0) 16%);
  }
}
@media (max-width: 767px) {
  /* The bottom padding has to clear the scroll cue, which is now 48px tall
     (label + chevron + progress bar) sitting 18px off the bottom -- a 66px
     no-go zone. --s6 alone left the button stack 2px INTO it at 414x896.
     Note the axis: the copy's height varies with WIDTH, not height, because it
     is the headline and lead re-wrapping. So this is unconditional on phones
     rather than gated behind a max-height. */
  .hero__inner { padding: var(--s4) var(--gutter) calc(var(--s6) + var(--s2)); }
  .hero__copy { max-width: none; }
  .hero h1 { font-size: clamp(2rem, 8.6vw, 2.6rem); margin-bottom: var(--s2); }
  .hero__lead { font-size: 1rem; margin-bottom: var(--s3); }
  .hero .btn-row { gap: var(--s2); margin-bottom: 0; }
  .hero .btn { flex: 1 1 auto; padding: .95em 1.2em; font-size: .88rem; }
  .hero__meta { display: none; }
  /* the cue stays on phones -- it is the whole point of the pinned hero */
  /* The cue was 55x40px at 9.9px type and the sticky call bar covered it from
     45% of the pin onward -- so the one affordance telling a phone user to keep
     scrolling was both unreadable and hidden. It is legible now, sits clear of
     the call bar, and carries a progress fill so the FIRST pixel of scroll
     produces visible feedback. That is the real fix for "the page looks broken":
     the hero always responds, even before the bracket has faded in. */
  .hero__cue { bottom: 1.15rem; font-size: .68rem; letter-spacing: .2em; gap: .3rem; }
  .hero__cue .hero__cue-bar { display: block; }

}

/* reduced motion: no pin, no dolly — the finished frame, everything visible */
@media (prefers-reduced-motion: reduce) {
  .js .hero__plate { transform: none; }
  .hero__layer, .hero__gfx { opacity: 1 !important; transform: none !important; }
  .hero__gfx { opacity: 0 !important; }   /* x-ray is a motion beat, not a resting state */
  .hero__copy { opacity: 1 !important; visibility: visible !important; }
}

/* ============================================================
   5. REVEALS
   ============================================================ */
.js .rv { opacity: 0; transform: translateY(14px); }
.is-ready .rv { will-change: opacity, transform; }
@media (prefers-reduced-motion: reduce) { .rv { opacity: 1 !important; transform: none !important; } }

/* ============================================================
   6. PROOF BAND — Wil hanging off a mount he installed
   ============================================================ */
.proof { position: relative; background: var(--ink); color: var(--text-invert); overflow: hidden; }
/* The grid, not the copy column, owns the section padding now. It used to sit
   on .proof__body alone, which left the photograph bleeding into all four
   edges of the band -- so its bottom sat hard against the section below it,
   and there was no rectangle inset from the edge to round. */
.proof__grid {
  /* The media column was 1.05fr when the frame was landscape 4/3.4. At 4/5 that
     share renders ~660x825 against a ~450px text column and the photograph eats
     the band. .88fr holds it near 575x719, which reads as a pair. */
  display: grid; grid-template-columns: .88fr 1.12fr; align-items: center;
  gap: var(--s5) var(--s6);
  max-width: var(--maxw); margin-inline: auto;
  padding: var(--section-pt) var(--gutter) var(--section-pb);
}
/* 4/5 because the three source frames ARE 4:5 -- manifest.CROPS cuts them to it --
   so object-fit has nothing left to crop and object-position has nothing to bias.
   Change one and you must change the other, or the framing starts drifting. */
.proof__media { position: relative; aspect-ratio: 4 / 5; overflow: hidden; }
.proof__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
.proof__media picture { display: block; width: 100%; height: 100%; }

/* ---- the proof slideshow -------------------------------------------------
   Three frames of one job on a 6s loop. .proof__media keeps .rv + .rv-wipe, so
   the scroll-in uncover is unchanged; this all happens inside that frame. */
.proof__show  { position: absolute; inset: 0; }
.proof__show:focus-visible { outline: 2px solid var(--gold-2); outline-offset: 3px; }
.proof__slide { position: absolute; inset: 0; display: block; opacity: 0; }
/* The resting frame -- what shows with the animation off (reduced motion, or an
   engine that never runs it). It is the weight test, not the bare wall: with no
   motion the band still has to show the photograph its own headline claims. */
.proof__slide:nth-child(2) { opacity: 1; z-index: 3; }

/* 6s cycle per frame: 1.55s hold, 0.45s dissolve.
   The one thing that matters here: the ARRIVING frame is already fully opaque,
   underneath, and only the departing frame fades. Fading two frames through 0.5
   at once would let ~25% of the ink background through mid-dissolve and the band
   would visibly dip dark three times a cycle. That is what the z-index cycle
   (top 3 -> bottom 1 -> on-deck 2) is for; it is not decoration. */
@keyframes proof-cross {
  0%     { opacity: 1; z-index: 3; }
  25.83% { opacity: 1; z-index: 3; }   /* 1.55s -- held on top */
  33.33% { opacity: 0; z-index: 3; }   /* 2.00s -- handed off */
  33.34% { opacity: 0; z-index: 1; }   /* drop to the back of the stack */
  66.66% { opacity: 0; z-index: 1; }
  66.67% { opacity: 1; z-index: 2; }   /* opaque again, safely under the live frame */
  100%   { opacity: 1; z-index: 2; }
}
.proof__slide { animation: proof-cross 6s linear infinite; }
/* 0 / -4 / -2, NOT 0 / -2 / -4. The delays are what put the visible windows in
   DOM order (before 0-2s, hanging 2-4s, finished 4-6s); swapping the last two
   plays the story backwards. Watch it, do not read it. */
.proof__slide:nth-child(1) { animation-delay:  0s; }
.proof__slide:nth-child(2) { animation-delay: -4s; }
.proof__slide:nth-child(3) { animation-delay: -2s; }

/* Pause, on the same contract as .rmq and .wall (wireRollingControls in main.js).
   A 6s infinite loop needs a pause mechanism; tabindex on .proof__show is the
   keyboard half of it. */
@media (hover: hover) { .proof__show:hover .proof__slide { animation-play-state: paused; } }
.proof__show.roll--held .proof__slide,
.proof__show.roll--keyboard-focus .proof__slide { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .proof__slide { animation: none; } }
.proof__body { padding: 0; }
.proof__body h2 { color: var(--plaster); margin-bottom: var(--s3); }
.proof__quote {
  font-family: var(--f-display); font-weight: 500;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem); line-height: 1.45;
  color: var(--plaster); border-left: 2px solid var(--gold-2);
  padding-left: var(--s3); margin: var(--s4) 0 0; max-width: 40ch;
}
@media (max-width: 900px) {
  .proof__grid { grid-template-columns: 1fr; gap: var(--s4); }
  /* Single column here, so the frame would be 100vw -- and at 4/5 that is 1055px
     tall on a landscape phone (844x390). Cap it by viewport height instead, which
     covers landscape phones and short laptop windows in one rule. */
  .proof__media { aspect-ratio: 4 / 5; width: min(100%, calc(70svh * 4 / 5)); margin-inline: auto; }
  .proof__body { padding: 0; }
}

/* ============================================================
   7. TRUST BAR
   ============================================================ */
/* The bar's tiles carry their own padding, so it does not take the full
   --section-pt/pb. It still owes the lift back at the bottom, because the
   section after it is now pulled up over this edge like every other junction. */
.trust { background: var(--plaster-2); border-block: 1px solid var(--rule);
         padding-bottom: var(--section-lift); }
/* FIVE tiles, which is an awkward number to wrap. Dividers come from a 1px grid
   gap letting the rule-coloured background show through -- the same trick
   .svc-grid uses -- because the old `border-left` + `:first-child` reset only
   works for a single unwrapped row and draws in the wrong places otherwise.
   Widths are chosen so no row is ever left with an orphan tile. */
.trust__grid {
  display: grid; gap: 1px; background: var(--rule);
  grid-template-columns: repeat(2, 1fr);
  max-width: var(--maxw); margin-inline: auto;
}
.trust__item {
  background: var(--plaster-2);
  padding: var(--s4) var(--s2); text-align: center;
  transition: background .45s var(--ease-out);
}
/* phone: 2 + 2 + 1, the last tile carrying the full width */
.trust__item:nth-child(5) { grid-column: span 2; }
.trust__item:hover { background: var(--plaster); }

/* tablet / laptop: a SIX column track, because 6 divides by both 3 and 2 --
   three tiles of 2 then two tiles of 3, so both rows come out flush */
@media (min-width: 700px) {
  .trust__grid { grid-template-columns: repeat(6, 1fr); }
  .trust__item { padding: var(--s5) var(--s3); }
  .trust__item:nth-child(-n+3) { grid-column: span 2; }
  .trust__item:nth-child(n+4)  { grid-column: span 3; }
}
/* desktop: all five across. Type and padding step down a notch so "Insured"
   and the award label do not wrap awkwardly inside a ~260px column. */
@media (min-width: 1160px) {
  .trust__grid { grid-template-columns: repeat(5, 1fr); }
  .trust__item:nth-child(-n+3),
  .trust__item:nth-child(n+4) { grid-column: auto; }
  .trust__item { padding: var(--s5) var(--s2); }
  .trust__n { font-size: clamp(1.9rem, 2.6vw, 2.5rem); }
}
.trust__n {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(2rem, 3.6vw, 3rem); line-height: 1;
  letter-spacing: -.03em; color: var(--ink); display: block;
  /* the digits are being counted up -- proportional figures make the whole row
     jitter sideways on every frame */
  font-variant-numeric: tabular-nums;
  transition: transform .45s var(--ease-snap);
}
.trust__item:hover .trust__n { transform: translateY(-2px); }
.trust__n .u { color: var(--gold-3); }
/* "Free" is the one non-numeric tile, so it gets the reveal the counters cannot have:
   letters wipe up out of a mask, then a brass rule draws under the word. Both the
   mask and the rule are inert until JS adds .is-split / animates them, so a no-JS
   render is just the word. */
.trust__n .u { position: relative; display: inline-block; }
.trust__n .u .split-line { display: inline-block; overflow: hidden; vertical-align: bottom; }
/* Absolutely positioned so it does NOT take part in the flow: as a block element
   it added ~8px under the two word tiles and their labels sat lower than the
   three number tiles, which read as a misaligned row. */
.trust__rule {
  position: absolute; left: 0; right: 0; bottom: -.28rem; height: 2px;
  background: linear-gradient(90deg, var(--gold-3), var(--gold-2));
  transform: scaleX(0); transform-origin: 0 50%;
}
/* "Insured" and "Free" share the letter wipe and the brass rule; they are only
   separated in time. An earlier version gave Insured a drawn tick as well, but it
   was the one tile with an extra element in the flow, so its label sat lower than
   the other four and the whole row looked misaligned. */
.trust__l {
  display: block; margin-top: .6rem; font-size: .82rem; line-height: 1.4;
  color: var(--text-soft); letter-spacing: .02em;
}

/* ============================================================
   7b. AWARD BAND — the Consumer Choice medallion
   Light, not dark, for two reasons worth writing down because
   "award = dark background" is the reflex: the hero and the proof
   band on either side are both ink, so a dark band here would make
   three dark sections in a row; and the medallion's deep red goes
   muddy against #0E0E10 while it stays rich against plaster. Gold
   and red on warm plaster reads as a wax seal on letterhead.
   ============================================================ */
.award { background: var(--plaster-2); border-bottom: 1px solid var(--rule);
         padding-block: var(--section-pt) var(--section-pb); }
.award__grid {
  display: grid; gap: var(--s3) var(--s5);
  grid-template-columns: auto 1fr; align-items: center;
}
/* Wide screens get a third column so the band carries the full page width
   instead of stranding the copy on the left -- seal, then the claim, then the
   explanation of why the claim is worth anything. */
@media (min-width: 1080px) {
  .award__grid { grid-template-columns: auto minmax(0, 1fr) minmax(0, .8fr);
                 column-gap: var(--s6); align-items: center; }
  .award__note { border-left-width: 2px; align-self: center; }
}
.award__seal { position: relative; width: clamp(112px, 16vw, 190px); flex: none; }
/* Never upscaled: the source cut-out is 370px wide, so 190px keeps it crisp at
   2x DPR — and a seal beside a headline wants to be this size anyway. */
.award__seal img { width: 100%; height: auto; display: block;
                   filter: drop-shadow(0 10px 24px rgba(14,14,16,.22)); }
.award__body h2 { margin-bottom: var(--s2); }
.award__body .lead { max-width: 52ch; margin-bottom: var(--s2); }
.award__note {
  font-size: var(--t-small); color: var(--text-soft); max-width: 52ch;
  border-left: 2px solid var(--gold-3); padding-left: var(--s2); margin: 0;
}
.award__note strong { color: var(--ink); font-weight: 600; }

/* Shine sweep. The band is masked BY the medallion itself, so the highlight
   only ever appears on the metal — a plain overflow:hidden box would show a
   rectangle crossing the transparent corners. Guarded by @supports so an engine
   without mask support simply gets no shine rather than a white slab. */
.award__shine { display: none; }
@supports ((-webkit-mask-image: url(#m)) or (mask-image: url(#m))) {
  .award__shine {
    display: block; position: absolute; inset: 0; overflow: hidden;
    pointer-events: none;
    -webkit-mask: url("/assets/img/cca-medallion-370.webp?v=794d7481") center/contain no-repeat;
            mask: url("/assets/img/cca-medallion-370.webp?v=794d7481") center/contain no-repeat;
  }
}
.award__shine i {
  position: absolute; top: -25%; bottom: -25%; left: 0; width: 42%;
  background: linear-gradient(105deg, rgba(255,255,255,0) 0%,
              rgba(255,255,255,.72) 50%, rgba(255,255,255,0) 100%);
  transform: translateX(-160%);
}
/* below the 3-column breakpoint the note simply follows the body in the second
   column; on a phone everything stacks AND centres */
@media (max-width: 1079px) {
  .award__note { grid-column: 2; margin-top: var(--s1); }
}
@media (max-width: 720px) {
  .award__grid { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .award__note { grid-column: 1; }
  .award__seal { margin-inline: auto; }
  .award__body .lead { margin-inline: auto; }
  .award__body .eyebrow { justify-content: center; }
  /* the note keeps its gold rule, so it stays left-READ but centres as a block */
  .award__note { text-align: left; margin-inline: auto; max-width: 40ch; }
}
@media (prefers-reduced-motion: reduce) { .award__shine { display: none; } }

/* ============================================================
   8. SECTION HEADERS
   ============================================================ */
.sec__head { max-width: 62ch; margin-bottom: var(--s5); }
.sec__head h2 { margin-bottom: var(--s2); }

/* --- split head -------------------------------------------------------------
   Mobile-first on purpose: below 1080px this modifier does nothing at all, so
   phones and tablets keep the plain stacked head. Above it, the eyebrow and the
   headline hold the left column while the supporting sentence sits bottom-aligned
   in the right one, and a hairline carries the block the full width of the page.
   Heads with no .lead degrade to a single column and keep only the rule. */
@media (min-width: 1080px) {
  .sec__head--split {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    column-gap: var(--s6);
    align-items: end;
    padding-bottom: var(--s4);
    border-bottom: 1px solid var(--rule);
  }
  .sec__head--split .eyebrow { grid-column: 1; grid-row: 1; }
  .sec__head--split h2       { grid-column: 1; grid-row: 2; margin-bottom: 0; }
  .sec__head--split .lead,
  .sec__head--split .sec__aside {
    grid-column: 2; grid-row: 1 / span 2;
    align-self: end; margin-bottom: 0; max-width: 46ch;
  }
  .sec__head--split .sec__aside { max-width: none; }
}
.sec--dark .sec__head--split { border-bottom-color: var(--rule-invert); }

/* ---- the About / Meet-the-owner split ----
   This used to be an INLINE style on the element with no CSS rule at all, so no
   media query could ever collapse it -- inline wins -- and on a phone the copy
   was trapped in a narrow left column beside a postage-stamp photo. */
.about-grid {
  display: grid; gap: var(--s5);
  grid-template-columns: 1.1fr .9fr; align-items: start;
}
.about-grid__media img { width: 100%; height: auto; }
@media (max-width: 860px) {
  .about-grid { grid-template-columns: 1fr; gap: var(--s4); }
  /* photo first, then the copy at the full width of the column */
  .about-grid__media { order: -1; }
  .about-grid__media img { max-height: 62vh; object-fit: cover; }
}

/* ============================================================
   9. BEFORE / AFTER SLIDERS
   ============================================================ */
.ba-grid { display: grid; gap: var(--s4); grid-template-columns: repeat(2, 1fr); }
@media (max-width: 820px) { .ba-grid { grid-template-columns: 1fr; } }

.ba { position: relative; }
.ba__frame {
  position: relative; overflow: hidden; aspect-ratio: 4 / 5;
  background: var(--ink-2); cursor: ew-resize; touch-action: pan-y;
}
/* Photographs are uncovered rather than faded in -- it matches what the section
   is about. Gated behind .js so a no-JS render is never clipped to nothing. */
.js .rv-wipe { clip-path: inset(0 0 100% 0); }
@media (prefers-reduced-motion: reduce) { .rv-wipe { clip-path: none !important; } }
.ba__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.ba__after { z-index: 1; }
/* The before layer is full size and revealed with clip-path, so both images
   always render at identical scale no matter how wide the frame is. */
.ba__beforeWrap {
  position: absolute; inset: 0; z-index: 2;
  clip-path: inset(0 50% 0 0);
}
.ba__beforeWrap::after {
  content: ''; position: absolute; top: 0; bottom: 0; right: 0; width: 1px;
  background: rgba(245,242,237,.7);
}
.ba__handle {
  position: absolute; top: 0; bottom: 0; left: 50%; z-index: 3;
  width: 44px; transform: translateX(-50%);
  display: grid; place-items: center; pointer-events: none;
}
.ba__knob {
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--plaster); color: var(--ink);
  display: grid; place-items: center;
  box-shadow: 0 4px 18px rgba(0,0,0,.35);
  font-size: .8rem; font-family: var(--f-display); font-weight: 800;
  transition: transform .35s var(--ease-snap), background .35s var(--ease-out);
}
.ba__frame:hover .ba__knob { transform: scale(1.12); background: var(--gold-2); }
@media (prefers-reduced-motion: reduce) { .ba__frame:hover .ba__knob { transform: none; } }
.ba__tag {
  position: absolute; z-index: 4; top: var(--s2);
  font-family: var(--f-display); font-weight: 700; font-size: .68rem;
  letter-spacing: .16em; text-transform: uppercase;
  padding: .35em .7em; background: rgba(14,14,16,.72); color: var(--plaster);
  backdrop-filter: blur(6px);
}
.ba__tag--b { left: var(--s2); }
.ba__tag--a { right: var(--s2); background: var(--gold-2); color: var(--ink); }
.ba__cap { margin-top: var(--s2); font-size: var(--t-small); color: var(--text-soft);
            max-width: 46ch; }
.ba input[type="range"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; margin: 0; cursor: ew-resize; z-index: 5;
}

/* ============================================================
   10. SERVICES
   ============================================================ */
.svc-grid { display: grid; gap: 1px; background: var(--rule);
            grid-template-columns: repeat(3, 1fr); border: 1px solid var(--rule); }
.svc {
  background: var(--plaster); padding: var(--s4);
  display: flex; flex-direction: column; gap: var(--s2);
  text-decoration: none; transition: background .4s var(--ease-out);
  position: relative;
}
.svc:hover { background: var(--plaster-2); }
/* a brass edge that draws in from the left, so the whole grid does not shift */
.svc::after {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--gold-3); transform: scaleY(0); transform-origin: 50% 0;
  transition: transform .4s var(--ease-snap);
}
.svc:hover::after { transform: scaleY(1); }
.svc__ico { width: 30px; height: 30px; color: var(--gold-3);
            transition: transform .4s var(--ease-snap); }
.svc:hover .svc__ico { transform: translateY(-3px); }
.svc h3 { font-size: 1.12rem; }
.svc p { font-size: var(--t-small); color: var(--text-soft); margin: 0; }
.svc__more { margin-top: auto; font-family: var(--f-display); font-weight: 700;
             font-size: .82rem; color: var(--gold-3);
             display: inline-flex; align-items: center; gap: .4em; }
.svc__more::after { content: '\2192'; transition: transform .4s var(--ease-snap); }
.svc:hover .svc__more::after { transform: translateX(5px); }
@media (prefers-reduced-motion: reduce) {
  .svc:hover .svc__ico, .svc:hover .svc__more::after { transform: none; }
}
@media (max-width: 940px) { .svc-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .svc-grid { grid-template-columns: 1fr; } }

/* ============================================================
   11. VIDEO
   ============================================================ */
.video__frame {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  background: var(--ink-2); border: 1px solid var(--rule-invert);
  max-width: 1060px; margin-inline: auto;
}
.video__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video__poster {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: brightness(.62) saturate(.9);
}
/* The disc is centred on the FRAME, not on a stack containing the label -- it
   used to be one grid column with the label under it, so the pair was centred
   and the disc alone sat visibly high. The label is now out of flow. */
.video__play {
  position: absolute; inset: 0; z-index: 2;
  border: 0; background: none; cursor: pointer; color: var(--plaster);
  font-family: var(--f-display); font-weight: 700;
  display: grid; place-items: center;
}
.video__disc {
  position: relative;
  width: clamp(64px, 8vw, 92px); aspect-ratio: 1; border-radius: 50%;
  background: linear-gradient(140deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  color: var(--ink);
  display: grid; place-items: center;
  transition: transform .45s var(--ease-snap);
}
/* Pulsing glow on its own layer, animated on OPACITY only -- see section 1b. */
.video__disc::after {
  content: ''; position: absolute; inset: -6px; border-radius: 50%; z-index: -1;
  box-shadow: 0 0 18px 4px rgba(212,175,55,.55), 0 0 48px 12px rgba(212,175,55,.3);
  animation: halo-pulse 2.6s ease-in-out infinite;
  will-change: opacity;
}
.video__play:hover .video__disc,
.video__play:focus-visible .video__disc { transform: scale(1.12); }
.video__play:hover .video__disc::after { animation-duration: 1.3s; }
.video__play:active .video__disc { transform: scale(1.04); }

.video__label {
  position: absolute; left: 50%; top: calc(50% + clamp(58px, 6vw, 78px));
  transform: translateX(-50%);
  font-size: .82rem; letter-spacing: .16em; text-transform: uppercase;
  white-space: nowrap;
  animation: label-breathe 3.4s ease-in-out infinite;
}
@keyframes label-breathe {
  0%, 100% { opacity: .72; letter-spacing: .16em; }
  50%      { opacity: 1;   letter-spacing: .22em; }
}
.video__play:hover .video__label { animation-play-state: paused; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .video__disc::after { animation: none; opacity: .6; }
  .video__label { animation: none; opacity: 1; }
  .video__play:hover .video__disc { transform: none; }
}

/* ============================================================
   12. PROCESS
   ============================================================ */
.step-grid { display: grid; gap: var(--s4); grid-template-columns: repeat(4, 1fr); }
.step { border-top: 1px solid var(--rule); padding-top: var(--s3); }
.sec--dark .step { border-color: var(--rule-invert); }
.step__n {
  font-family: var(--f-display); font-weight: 800; font-size: .8rem;
  letter-spacing: .14em; color: var(--gold-3); display: block; margin-bottom: var(--s2);
}
.sec--dark .step__n { color: var(--gold-1); }
.step h3 { font-size: 1.05rem; margin-bottom: .5rem; }
.step p { font-size: var(--t-small); color: var(--text-soft); margin: 0; }
.sec--dark .step p { color: var(--text-invert-soft); }
@media (max-width: 900px) { .step-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .step-grid { grid-template-columns: 1fr; } }

/* Keep the whole closing block on one centred measure. The step grid inside it
   is a full-width 4-column track by default, which pulled the section's visual
   weight to the left even though the heading above it was centred. */
.cta-block { max-width: 1040px; margin-inline: auto; }
.cta-block .step-grid { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 760px) { .cta-block .step-grid { grid-template-columns: 1fr; } }

/* the expectation-setting line under the homepage CTA steps */
.cta__expect {
  max-width: 58ch; margin: var(--s4) auto 0; font-size: var(--t-small);
  color: var(--text-invert-soft); text-align: center;
}

/* ============================================================
   13. REVIEWS
   ============================================================ */
.rev {
  background: var(--plaster); border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  padding: var(--s4); display: flex; flex-direction: column; gap: var(--s2);
  transition: border-color .4s var(--ease-out), transform .4s var(--ease-snap);
}
.rev:hover { border-color: var(--gold-2); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .rev:hover { transform: none; } }
.sec--dark .rev { background: var(--ink-2); border-color: var(--rule-invert); }
/* Core gold reads well on ink. On light surfaces the stars are decorative
   (aria-hidden, with a text rating beside them) but still want more weight. */
/* Stars carry the site's gold GRADIENT rather than a flat fill. Note which
   gradient: the champagne .gold-text ramp (gold-1 -> gold-2 -> gold-1) is for
   DARK grounds only, and gold-1 (#F5E7C1) on plaster (#F5F2ED) is very nearly
   invisible -- these cards are all on plaster. So the stars use the same
   light-surface pair the trust rule already uses, gold-2 highlight into gold-3
   deep, which reads as metallic gold and stays legible. The gradient is defined
   as inline SVG (see STARS in build/build_reviews.py) because background-clip
   cannot paint an SVG path; its stops read the CSS tokens, so the colours still
   come from one place. `color` remains the fallback wherever no def is present. */
.stars { display: flex; gap: 2px; color: var(--gold-3); }
.sec--dark .stars, .on-dark .stars { color: var(--gold-2); }
.stars svg { width: 15px; height: 15px; }
.grad-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.rev p { margin: 0; font-size: var(--t-small); }
.rev__who { margin-top: auto; font-family: var(--f-display); font-weight: 700; font-size: .88rem; }
.rev__when { font-family: var(--f-body); font-weight: 400; color: var(--text-soft); font-size: .8rem; }

/* Attribution row: monogram + name + month, on every review card there is.
   The `:not(.rev--feature)` these three used to carry is gone with the class --
   it existed to spare a single homepage pull-quote that no longer exists, since
   the homepage reviews became the .rmq marquee and use the same card as the
   wall. Every .rev on the site now wants the monogram row.
   The monogram is initials, never the reviewer's Google profile photo: those URLs
   rotate on Google's CDN (so they break) and they are photographs of real people,
   which are not ours to re-host. Google itself falls back to exactly this. */
.rev .rev__who { display: flex; align-items: center; gap: .6rem; }
.rev .rev__id  { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.rev .rev__when { margin-top: .1rem; }
/* --plaster-2, not --plaster-3. The monogram is 12.8px bold, which is NOT large
   text under WCAG, so it needs 4.5:1 like any other small type. Measured:
   --gold-3 on --plaster-3 is 4.09:1 (fails); on --plaster-2 it is 4.76:1
   (passes) and the circle is still a clear step darker than the --plaster card
   behind it. Same instruction as everywhere else the gold scale is used --
   re-measure if you change either colour, do not eyeball it.
   These initials are aria-hidden, but they are still VISIBLE, so contrast is
   about low-vision sighted readers, not screen readers. */
.rev__avatar {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--plaster-2); color: var(--gold-3);
  font-family: var(--f-display); font-weight: 800; font-size: .8rem;
  letter-spacing: .01em; user-select: none;
}
.sec--dark .rev__avatar, .on-dark .rev__avatar {
  background: var(--rule-invert); color: var(--gold-2);
}



/* ============================================================
   13b. REVIEW WALL  (/clients-review)
   Three columns scrolling vertically at different speeds. Rebuilt from a
   React/framer-motion reference in plain CSS, because the page must be
   complete for crawlers that never execute a line of JavaScript.

   PROGRESSIVE ENHANCEMENT, and the order matters:
     - Base (no JS, or reduced motion): plain columns at natural height, no
       mask, no clipping, nothing moving. Every review readable by scrolling
       the page normally. This is what a crawler and a reduced-motion visitor
       get, and it is complete.
     - .wall--live is added by main.js ONLY after it has cloned each list and
       only when motion is allowed. The clone is what makes translateY(-50%)
       wrap seamlessly. Capping the height and masking the edges before the
       clone exists would hide reviews behind an animation that never runs,
       so both are gated on that class -- never move them out of it.
   ============================================================ */
/* ONE breakpoint, at 900px, and nothing is ever hidden at any width.
   An earlier pass had three columns collapsing to one below 900px by setting
   `display:none` on the other two. That silently hid 14 of the 22 reviews from
   every phone visitor -- and `display:none` is hidden from assistive tech and
   discounted by search engines, so it took the content out of exactly the two
   places this page exists to serve. Below 900px the three columns simply stack
   and every review is readable in full; the marquee is a desktop flourish, not
   the only way to reach the content. Do not reintroduce a hiding rule here. */
.wall { display: grid; grid-template-columns: 1fr; gap: var(--s3); }
@media (min-width: 900px) { .wall { grid-template-columns: repeat(3, 1fr); } }

.wall__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s3);
}
/* Cards size to their own content -- no equal-height stretching, so a two-line
   review does not inherit the dead space of a twelve-line one. */
.wall .rev { gap: var(--s2); padding: var(--s3) var(--s4); }
.wall .rev blockquote { margin: 0; display: flex; flex-direction: column; gap: .55em; }
.wall .rev p { font-size: var(--t-small); line-height: 1.55; }
.wall .rev__who { margin-top: var(--s1); }

/* The 50% fallback is load-bearing, not decoration. --wall-distance is measured
   and set by main.js, and only `if (distance > 0)` — so any moment the lists are
   unmeasurable (a rAF before the first measure lands, a display:none ancestor,
   a zero-height column) leaves it unset. Without a fallback the var is invalid
   at computed-value time, which takes the whole `transform` declaration with it:
   the `to` keyframe then holds nothing and the wall silently sits still, with no
   error anywhere to point at it. 50% is the pre-measurement approximation the
   wall used before — half a gap short at the seam, but moving. */
@keyframes wall-scroll { to { transform: translateY(calc(-1 * var(--wall-distance, 50%))); } }

/* Capping and masking live behind BOTH .wall--live (main.js has cloned the
   lists) and the 900px breakpoint (the columns are actually side by side).
   Either one alone would clip reviews out of reach. */
@media (min-width: 900px) {
  .wall--live {
    max-height: min(78vh, 820px);
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
            mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  }
  .wall--live .wall__list {
    animation: wall-scroll var(--dur, 60s) linear infinite;
    will-change: transform;
  }
  /* Touch devices pause only while the finger is down (see
     wireRollingControls()). A touch browser's sticky :hover or focus state
     must never leave a wall stopped after the finger lifts. */
  .wall--live.roll--held .wall__list,
  .wall--live.roll--keyboard-focus .wall__list { animation-play-state: paused; }
  @media (hover: hover) {
    .wall--live:hover .wall__list { animation-play-state: paused; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .wall--live { max-height: none; overflow: visible;
                -webkit-mask-image: none; mask-image: none; }
  .wall--live .wall__list { animation: none; }
}

/* ---- gallery variant --------------------------------------------------
   Same marquee mechanics, different cargo: photos rather than review cards, so
   no card chrome and a tighter gap. Below 900px, where the marquee is off and
   the three columns stack, each column becomes a CSS-columns masonry so a phone
   still gets a dense grid rather than 36 full-width photos in a single file --
   that is the density the old .gal layout had, and losing it would have been a
   regression dressed up as a redesign. */
.wall--gal .wall__list { gap: var(--s2); }
.wall--gal .gal__item { margin-bottom: 0; }
@media (max-width: 899.98px) {
  .wall--gal .wall__list { display: block; columns: 2; column-gap: var(--s2); }
  .wall--gal .wall__list > li { break-inside: avoid; margin-bottom: var(--s2); }
}
@media (max-width: 520px) {
  .wall--gal .wall__list { columns: 1; }
}

.wall__cta {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  justify-content: center; margin: var(--s4) 0 0;
}


/* ============================================================
   13c. REVIEW MARQUEE  (homepage #reviews band)
   Horizontal sibling of the vertical wall on /clients-review. Same rules
   apply and for the same reasons -- read 13b before changing either.

   The homepage shows only reviews that fit a card WITHOUT being cut off:
   build_home_reviews.py selects short single-paragraph ones rather than
   truncating long ones. A trimmed testimonial with an ellipsis is a quiet
   misquote of a real customer, and the full text is one click away.
   ============================================================ */
.rmq { position: relative; }
.rmq__track {
  display: flex; gap: var(--s3);
  list-style: none; margin: 0; padding: 0;
}
/* Cards are a fixed width so the loop distance is predictable, and a fixed
   height so the row has one baseline instead of a ragged edge. */
.rmq .rev {
  flex: 0 0 clamp(15rem, 78vw, 20.5rem);
  border-radius: var(--radius-card);
  padding: var(--s3) var(--s4);
  gap: var(--s2);
}
.rmq .rev blockquote { margin: 0; }
.rmq .rev p { font-size: var(--t-small); line-height: 1.55; }
.rmq .rev__who { margin-top: auto; }

/* Base = no JS, or reduced motion: a normal horizontal scroller. Every card is
   still reachable, by swipe or by keyboard, with nothing moving on its own. */
.rmq__viewport {
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  padding-block: var(--s1);
}
.rmq__viewport > .rmq__track > .rev { scroll-snap-align: start; }

/* Same fallback, same reason as wall-scroll above — see the note there. */
@keyframes rmq-scroll { to { transform: translateX(calc(-1 * var(--rmq-distance, 50%))); } }

/* .rmq--live is added by main.js only after it has cloned the track. */
.rmq--live .rmq__viewport {
  overflow: hidden;
  scroll-snap-type: none;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.rmq--live .rmq__track {
  width: max-content;
  animation: rmq-scroll var(--dur, 90s) linear infinite;
  will-change: transform;
}
/* See the matching review-wall rule above: hover is deliberately limited to
   hardware that actually hovers, while touch/pen pauses only during a hold. */
.rmq--live.roll--held .rmq__track,
.rmq--live.roll--keyboard-focus .rmq__track { animation-play-state: paused; }
@media (hover: hover) {
  .rmq--live:hover .rmq__track { animation-play-state: paused; }
}

@media (prefers-reduced-motion: reduce) {
  .rmq--live .rmq__viewport { overflow-x: auto; scroll-snap-type: x proximity;
                              -webkit-mask-image: none; mask-image: none; }
  .rmq--live .rmq__track { animation: none; width: auto; }
}

.rating-hero { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.rating-hero__n { font-family: var(--f-display); font-weight: 800;
                  font-size: clamp(2.6rem, 5vw, 3.8rem); line-height: 1; letter-spacing: -.03em; }

/* ============================================================
   14. FAQ  (details/summary — readable with JS off)
   ============================================================ */
/* Two columns from 1024px: the heading stays put while the questions scroll past
   it, and the accordion gets the whole remaining width instead of being capped at
   900px inside a 1320px section. Below 1024px it is a plain stacked block and the
   sticky is dropped, because pinning anything on a phone eats the viewport. */
.faq-layout { display: grid; gap: var(--s4); }
.faq-layout .sec__head { margin-bottom: 0; }
@media (min-width: 1024px) {
  .faq-layout {
    grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
    column-gap: var(--s6);
    align-items: start;
  }
  .faq-layout .sec__head {
    position: sticky; top: calc(var(--nav-h) + var(--s4));
    max-width: none;
  }
}
.faq__aside { margin-top: var(--s3); font-size: var(--t-small); color: var(--text-soft); }
.faq__aside a { font-family: var(--f-display); font-weight: 700; color: var(--gold-3);
                text-decoration: none; white-space: nowrap; }
.faq__aside a:hover { text-decoration: underline; }

.faq { border-top: 1px solid var(--rule); max-width: 900px; }
.faq-layout .faq { max-width: none; }
.faq details { border-bottom: 1px solid var(--rule); }
.faq summary {
  list-style: none; cursor: pointer; padding: var(--s3) 2.5rem var(--s3) 0;
  position: relative; font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1rem, 1.4vw, 1.15rem); line-height: 1.35;
  transition: color .3s var(--ease-out), padding-left .35s var(--ease-snap);
}
.faq summary:hover { color: var(--gold-3); padding-left: 6px; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ''; position: absolute; right: .5rem; top: 50%; width: 13px; height: 13px;
  margin-top: -6px;
  background:
    linear-gradient(var(--gold-3), var(--gold-3)) center/100% 1.5px no-repeat,
    linear-gradient(var(--gold-3), var(--gold-3)) center/1.5px 100% no-repeat;
  transition: transform .35s var(--ease-out);
}
.faq summary:hover::after { transform: rotate(90deg); }
.faq details[open] summary::after { transform: rotate(90deg); background-size: 100% 1.5px, 0 0; }
.faq details[open] summary:hover::after { transform: rotate(90deg); }
.faq__a { padding: 0 2.5rem var(--s3) 0; }
.faq__a p { font-size: var(--t-small); color: var(--text-soft); max-width: 74ch; }
.faq__a p:last-child { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) {
  .faq summary:hover { padding-left: 0; }
  .faq summary:hover::after { transform: none; }
}

/* ============================================================
   15. GALLERY
   ============================================================ */
/* The old `.gal` CSS-columns masonry is gone -- the gallery is now the same
   three-column scrolling wall as /clients-review (see 13b), so the tile keeps its
   own styling and the layout comes from .wall / .wall--gal. */
.gal__item {
  break-inside: avoid; position: relative;
  display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in;
  overflow: hidden;   /* so the hover scale is a crop, not an overflow */
}
.gal__item img { width: 100%; height: auto;
                 transition: transform .6s var(--ease-snap); }
.gal__item:hover img { transform: scale(1.045); }
.gal__item::after {
  content: ''; position: absolute; inset: 0; background: var(--ink); opacity: 0;
  transition: opacity .4s var(--ease-out); pointer-events: none;
}
.gal__item:hover::after { opacity: .12; }
@media (prefers-reduced-motion: reduce) { .gal__item:hover img { transform: none; } }

.lb[hidden] { display: none; }
.lb {
  position: fixed; inset: 0; z-index: 990; background: rgba(10,10,12,.94);
  display: grid; place-items: center; padding: var(--gutter);
}
.lb img { max-width: 100%; max-height: 84vh; width: auto; }
.lb__cap {
  position: absolute; left: 0; right: 0; bottom: var(--s3); text-align: center;
  color: var(--text-invert-soft); font-size: var(--t-small); padding-inline: var(--gutter);
}
.lb__btn {
  position: absolute; background: none; border: 1px solid var(--rule-invert);
  color: var(--plaster); width: 46px; height: 46px; border-radius: 50%;
  cursor: pointer; display: grid; place-items: center; font-size: 1.1rem;
}
.lb__close { top: var(--s3); right: var(--s3); }
.lb__prev { left: var(--s3); top: 50%; transform: translateY(-50%); }
.lb__next { right: var(--s3); top: 50%; transform: translateY(-50%); }

/* ============================================================
   16. MARQUEE — service areas
   ============================================================ */
/* The marquee is a thin band with a lifted section on BOTH sides of it: it
   pulls up over the band above, and the reviews band pulls up over it. At
   --s3 of padding the lift below was taller than the band itself, so the dark
   section simply covered the names. It pays both lifts back and keeps --s4 of
   clear air on each side of the text. */
/* The band's own top border used to draw the section's curved corners as a
   visible hairline arc -- the only place on the site where a rule bent. The
   radius is dropped here and the rule is redrawn as a straight ::before inset
   to the container, so it matches the hairlines above it (.sec__head--split,
   .step) instead of curling into the gutters. The bottom border went with it:
   the reviews band overlaps this edge, so it was never visible anyway. */
.marquee { overflow: hidden; border: 0;
           border-top-left-radius: 0; border-top-right-radius: 0;
           padding-block: calc(var(--s4) + var(--section-lift));
           background: var(--plaster); }
.marquee::before {
  content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: min(100% - var(--gutter) * 2, var(--maxw) - var(--gutter) * 2);
  height: 1px; background: var(--rule);
}
.sec--dark .marquee { background: var(--ink); }
.sec--dark .marquee::before { background: var(--rule-invert); }
/* No flex `gap` here on purpose. The track is cloned to a whole number of
   segments and wrapped by exactly one segment width; a gap between the segments
   would make the wrap point land half a gap short and show a seam. Spacing is
   carried by the span's own padding instead, so every repeat is identical. */
.marquee__track { display: flex; width: max-content; will-change: transform; }
.marquee__track span {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(1.1rem, 2.2vw, 1.7rem); letter-spacing: -.02em;
  color: var(--text-soft); display: flex; align-items: center; gap: var(--s4);
  white-space: nowrap; padding-right: var(--s4);
}
.marquee__track span::after { content: ''; width: 6px; height: 6px; border-radius: 50%;
                              background: var(--gold-2); flex: none; }

/* ============================================================
   17. FOOTER
   ============================================================ */
.foot { background: var(--ink); color: var(--text-invert); padding-block: var(--s6) var(--s4); }
.foot__grid {
  display: grid; gap: var(--s5); grid-template-columns: 1.4fr 1fr 1fr 1.1fr;
  padding-bottom: var(--s5); border-bottom: 1px solid var(--rule-invert);
}
.foot h2 {
  font-size: .74rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--gold-1); font-weight: 700; margin-bottom: var(--s2);
}
.foot a { color: var(--text-invert-soft); text-decoration: none; }
.foot a:hover { color: var(--plaster); }
.foot li { margin-bottom: .45rem; font-size: var(--t-small); }
.foot__brand { font-family: var(--f-display); font-weight: 800; font-size: 1.6rem;
               letter-spacing: -.03em; margin-bottom: var(--s2); }
/* No !important on the colour: `background-clip: text` needs `color: transparent`
   and an !important here would beat it. Dropping it is safe -- contact.html
   reuses this class on a LIGHT card and pins it with an inline
   `color: var(--ink) !important`, which outranks any stylesheet either way. */
.foot__tel { font-family: var(--f-display); font-weight: 800;
             font-size: 1.5rem; letter-spacing: -.02em; color: var(--plaster);
             text-decoration: none; display: inline-block; margin-bottom: .3rem; }
.foot__social { display: flex; gap: var(--s2); margin-top: var(--s3); }
.foot__social a {
  width: 42px; height: 42px; border: 1px solid var(--rule-invert); border-radius: 50%;
  display: grid; place-items: center; transition: border-color .3s, background .3s;
}
.foot__social a:hover { border-color: var(--gold-2); background: rgba(212,175,55,.14); }
.foot__social svg { width: 18px; height: 18px; }
/* Scoped to .foot ON PURPOSE. `.foot__tel` is reused on contact.html inside a
   plaster card; unscoped, that phone number would be handed a white-into-gold
   ramp on a near-white ground and all but disappear. Never loosen this to a
   bare `.foot__tel`. */
.foot .foot__brand,
.foot .foot__tel {
  background-image: var(--grad-gold-lit);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.foot .foot__tel:hover, .foot .foot__tel:focus-visible { color: transparent; }
@media (forced-colors: active) {
  .foot .foot__brand, .foot .foot__tel { background: none; color: CanvasText; }
}

.foot__legal {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); justify-content: space-between;
  padding-top: var(--s3); font-size: .8rem; color: var(--text-invert-soft);
}
/* --- the Cleverstack credit ----------------------------------------------
   Deliberately NOT gold at rest. Gold is this site's DECORATIVE language --
   headings, stars, brand marks, section rules -- so a gold word here would read
   as ornament, which is exactly the thing that stops it looking clickable. The
   affordance comes from contrast and an underline instead, and gold is held
   back for the hover so the interaction has somewhere to travel to.

   The underline is a ::after hairline rather than text-decoration so it can be
   animated, and it is worth noting `.foot a` sets `text-decoration: none` on
   every other footer link -- nothing else down there is underlined, which is
   what makes this one unambiguous rather than lost in noise. */
.foot__by { color: var(--text-invert-soft); }
.foot__by a {
  position: relative;
  /* inline-FLEX, not inline. The reset at the top of this file sets
     `img, svg, video { display: block }`, so an inline anchor containing the
     arrow fragments into three boxes: the arrow drops to its own line and the
     absolutely-positioned underlines below anchor to the first fragment and
     scatter. A single flex box cannot fragment, which fixes both at once. */
  display: inline-flex; align-items: center;
  color: var(--plaster);            /* near-white against the grey = "interactive" */
  text-decoration: none;
  padding-bottom: 1px;
  transition: color .3s var(--ease-out);
  white-space: nowrap;              /* never split "Cleverstack" from its arrow */
}
/* resting hairline, always visible */
.foot__by a::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: currentColor; opacity: .45;
}
/* the gold wipe, same idiom as the nav's "tubelight" underline */
.foot__by a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: var(--gold-2);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .4s var(--ease-snap);
}
.foot__by a svg {
  width: .72em; height: .72em; margin-left: .28em; flex: 0 0 auto;
  transition: transform .3s var(--ease-snap);
}
.foot__by a:hover, .foot__by a:focus-visible { color: var(--gold-1); }
.foot__by a:hover::after, .foot__by a:focus-visible::after { transform: scaleX(1); }
.foot__by a:hover svg, .foot__by a:focus-visible svg { transform: translate(2px, -2px); }
/* Keyboard users must get the same signal as the mouse, or the link is invisible
   to them; the browser outline is kept as well rather than being suppressed. */
.foot__by a:focus-visible { outline: 2px solid var(--gold-2); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .foot__by a, .foot__by a::after, .foot__by a svg { transition: none; }
  .foot__by a:hover svg, .foot__by a:focus-visible svg { transform: none; }
}
@media (forced-colors: active) {
  .foot__by a { color: LinkText; }
  .foot__by a::after { background: LinkText; }
}

.foot__hours { display: grid; grid-template-columns: auto auto; gap: .3rem var(--s3);
               font-size: var(--t-small); }
.foot__hours dt { color: var(--text-invert-soft); }
.foot__hours dd { margin: 0; color: var(--plaster); }
@media (max-width: 900px) { .foot__grid { grid-template-columns: repeat(2, 1fr); } }

/* PHONES: keep TWO columns rather than collapsing to one.
   Stacked full-width, the two link lists were 350px-wide blocks holding ~110px
   of text -- 187px + 219px of height with the right half of each one empty, and
   that empty half is most of why the mobile footer ran to 1219px (1.44 screens).
   Side by side they cost one column's height instead of two.

   The towns stay one per line on purpose. Flowing them inline only pays off in
   a full-width column; inside a half-width one each name is atomic and wraps to
   its own line anyway, so it would buy nothing and split the column ragged. */
@media (max-width: 540px) {
  .foot__grid { grid-template-columns: repeat(2, 1fr); gap: var(--s4) var(--s3); }
  .foot__col--brand, .foot__col--touch { grid-column: 1 / -1; }
}

/* sticky mobile call bar */
.callbar { display: none; }
@media (max-width: 767px) {
  .callbar {
    position: fixed; inset: auto 0 0 0; z-index: 850;
    display: grid; grid-template-columns: 1fr 1fr;
    background: var(--ink); border-top: 1px solid var(--rule-invert);
    transform: translateY(100%); transition: transform .4s var(--ease-out);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .callbar.is-visible { transform: translateY(0); }
  /* Typing on a phone opens the keyboard, which shrinks the VISUAL viewport
     while the bar stays pinned to the LAYOUT viewport -- so on the contact form
     it stopped looking like a bottom bar and started looking like a black slab
     parked across the middle of the page. It is also redundant right then: the
     visitor is already in the middle of making contact. So it drops out of the
     way for as long as a field is focused and slides back when they are done.
     Set on <html> because the focused field and the bar are unrelated subtrees.
     Range inputs are excluded in JS -- those are the before/after sliders. */
  .js-typing .callbar.is-visible { transform: translateY(100%); }
  /* The bar is fixed, so it floats over whatever the page ends with -- and the
     footer's last line ("Consumer Choice Award 2024") was sitting under it.
     The padding goes on .foot, NOT on body: body is --plaster, so padding there
     would paint a light strip below the ink footer. On .foot the ink simply
     extends behind the bar, which is what is wanted. */
  .foot { padding-bottom: calc(var(--s4) + var(--callbar-h) + env(safe-area-inset-bottom)); }
  .callbar a {
    padding: 1rem; text-align: center; text-decoration: none;
    font-family: var(--f-display); font-weight: 700; font-size: .92rem;
    color: var(--plaster);
  }
  /* Both halves now carry the site's gold, in the two sanctioned forms: gradient
     LETTERING on the ink half, gradient FILL on the solid half. The fill used to
     be flat --gold-2, which was the only place on the site where the gold was a
     single colour rather than the .btn--brass gradient. */
  .callbar a:first-child {
    background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
  }
  .callbar a + a {
    background: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
    color: var(--ink);
  }
  body { padding-bottom: 0; }
  /* Windows High Contrast drops backgrounds, and the first half paints its
     glyphs THROUGH one -- without this it renders as invisible text. */
  @media (forced-colors: active) {
    .callbar a:first-child { background: none; color: CanvasText; }
  }
}

/* ============================================================
   17b. 404 SIGNPOST
   ============================================================ */
/* The list of real pages on 404.html. Each row is a whole-block link rather
   than a link inside a paragraph: someone who has just hit a dead end should
   not have to aim at a few underlined words, least of all on a phone. */
.lost { display: grid; gap: 0; margin-top: var(--s4); border-top: 1px solid var(--rule); }
.lost__item {
  display: grid; gap: .25rem;
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
  text-decoration: none; color: inherit;
  transition: padding-inline-start .3s var(--ease-out), background .3s var(--ease-out);
}
.lost__t {
  font-family: var(--f-display); font-weight: 700; font-size: 1.12rem;
  letter-spacing: -.01em;
}
.lost__d { color: var(--text-soft); font-size: var(--t-small); line-height: 1.55; max-width: 60ch; }
/* Hover nudges the row in from the rule, which reads as "this is the way out".
   Guarded so a touch browser's sticky :hover does not leave a row indented
   after a tap -- the same trap the gallery controls documented. */
@media (hover: hover) {
  .lost__item:hover { padding-inline-start: var(--s3); background: var(--plaster-2); }
  .lost__item:hover .lost__t { color: var(--gold-3); }
}
.lost__item:focus-visible {
  outline: 2px solid var(--gold-3); outline-offset: 3px;
  padding-inline-start: var(--s3); background: var(--plaster-2);
}

/* ============================================================
   18. FORMS
   ============================================================ */
.form { display: grid; gap: var(--s3); max-width: 720px; }
.form__row { display: grid; gap: var(--s3); grid-template-columns: 1fr 1fr; }
@media (max-width: 620px) { .form__row { grid-template-columns: 1fr; } }

.field { position: relative; display: block; }
.field > span {
  display: block; font-family: var(--f-display); font-weight: 500;
  font-size: .82rem; letter-spacing: .02em; margin-bottom: .45rem;
}
.field .req { color: var(--gold-3); }
.field input, .field select, .field textarea {
  width: 100%; font-family: var(--f-body); font-size: 1rem; color: var(--text);
  background: var(--plaster); border: 1px solid var(--rule); border-radius: 2px;
  padding: .85rem 1rem; transition: border-color .3s var(--ease-out);
}
.field textarea { min-height: 110px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--gold-3); outline: none;
}
.field__hint { display: block; margin-top: .35rem; font-size: .8rem; color: var(--text-soft); }

/* photo upload */
.drop {
  border: 1px dashed var(--rule); border-radius: 2px; background: var(--plaster);
  padding: var(--s4) var(--s3); text-align: center; cursor: pointer;
  transition: border-color .3s var(--ease-out), background .3s var(--ease-out);
  display: block;
}
.drop:hover, .drop.is-over { border-color: var(--gold-3); background: var(--plaster-2); }
.drop input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.drop__ico { width: 26px; height: 26px; color: var(--gold-3); margin: 0 auto .6rem; }
.drop__t { font-family: var(--f-display); font-weight: 700; font-size: .95rem; }
.drop__s { font-size: .82rem; color: var(--text-soft); margin-top: .25rem; }
.drop__preview { display: none; align-items: center; gap: var(--s2); text-align: left; }
.drop.has-file .drop__prompt { display: none; }
.drop.has-file .drop__preview { display: flex; }
.drop__thumb { width: 74px; height: 74px; object-fit: cover; flex: none; }
.drop__meta { font-size: .85rem; min-width: 0; }
.drop__name { font-family: var(--f-display); font-weight: 700; word-break: break-all; }
.drop__remove {
  margin-left: auto; flex: none; border: 1px solid var(--rule); background: none;
  border-radius: 999px; padding: .4rem .9rem; cursor: pointer;
  font-family: var(--f-display); font-weight: 700; font-size: .78rem;
}
.drop__remove:hover { border-color: var(--ink); }

.form__note { font-size: .82rem; color: var(--text-soft); }
.toast {
  margin-top: var(--s2); padding: var(--s3); border-left: 2px solid var(--gold-3);
  background: var(--plaster-2); font-size: var(--t-small);
}
.toast[hidden] { display: none; }
.toast--err { border-color: #B4402E; }

/* honeypot */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* contact split */
.contact-grid { display: grid; gap: var(--s6); grid-template-columns: 1.25fr .75fr; align-items: start; }
@media (max-width: 900px) { .contact-grid { grid-template-columns: 1fr; gap: var(--s5); } }
.contact-card { border: 1px solid var(--rule); padding: var(--s4);
                transition: border-color .4s var(--ease-out); }
.contact-card:hover { border-color: var(--gold-3); }
.contact-card h3 { font-size: 1.05rem; margin-bottom: var(--s2); }
.contact-card dl { display: grid; grid-template-columns: auto 1fr; gap: .4rem var(--s3);
                   font-size: var(--t-small); margin: 0; }
.contact-card dt { color: var(--text-soft); }
.contact-card dd { margin: 0; }

/* page header (inner pages) */
.phead { background: var(--ink); color: var(--text-invert);
         padding-block: calc(var(--nav-h) + var(--s6)) calc(var(--s6) + var(--section-lift)); }
/* Inner-page titles carry the gold gradient. .phead is on ink, so this is the
   CHAMPAGNE ramp (gold-1 -> gold-2 -> gold-1), the same one .gold-text and the
   brand dot use on dark -- not the light-surface pair the review stars use.
   The homepage h1 is deliberately left plaster: it sits over a photograph with
   a scrim behind it, where gradient-clipped text loses the contrast the
   text-shadow is there to protect. */
.phead h1 {
  margin-bottom: var(--s2);
  color: var(--plaster);
  background-image: linear-gradient(100deg, var(--gold-1) 0%, var(--gold-2) 55%, var(--gold-1) 100%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
/* Windows High Contrast drops backgrounds, and this paints its glyphs THROUGH
   one -- transparent text would leave the page title invisible. */
@media (forced-colors: active) {
  .phead h1 { background: none; color: CanvasText; }
}
.phead p { color: var(--text-invert-soft); font-size: var(--t-lead); }

/* Inner-page headers use the SAME split the homepage sections use: crumbs and
   the h1 on the left, the standfirst on the right, both sitting on one baseline
   above a hairline. Matching .sec__head--split exactly -- same 1080px
   breakpoint, same 1.05fr/.95fr split, same align-items:end -- so the two never
   drift apart. Below 1080px it collapses to the stacked order it always was.
   The rule is --rule-invert because .phead is on ink, not plaster. */
@media (min-width: 1080px) {
  .phead .wrap {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    column-gap: var(--s6);
    align-items: end;
    padding-bottom: var(--s4);
    border-bottom: 1px solid var(--rule-invert);
  }
  .phead .crumbs      { grid-column: 1; grid-row: 1; }
  .phead h1           { grid-column: 1; grid-row: 2; margin-bottom: 0; }
  /* :not(.crumbs) matters -- the breadcrumb is also a <p> in this block. */
  .phead p:not(.crumbs) {
    grid-column: 2; grid-row: 1 / span 2;
    align-self: end; margin-bottom: 0; max-width: 46ch;
  }
}
.crumbs { font-size: .8rem; color: var(--text-invert-soft); margin-bottom: var(--s3); }
.crumbs a { text-decoration: none; }
.crumbs a:hover { color: var(--plaster); }

/* inner pages show the nav immediately */
.nav--static { transform: translateY(0); }

/* service blocks */
.svc-detail { display: grid; gap: var(--s5); grid-template-columns: 1fr 1fr;
              align-items: center; padding-block: var(--section-y);
              border-top: 1px solid var(--rule); }
.svc-detail:first-of-type { border-top: 0; }
.svc-detail--flip .svc-detail__media { order: -1; }
@media (max-width: 860px) {
  .svc-detail { grid-template-columns: 1fr; gap: var(--s4); }
  .svc-detail--flip .svc-detail__media { order: 0; }
}
.svc-detail ul { margin-top: var(--s2); }
.svc-detail li { position: relative; padding-left: 1.4rem; margin-bottom: .5rem;
                 font-size: var(--t-small); color: var(--text-soft); }
.svc-detail li::before { content: ''; position: absolute; left: 0; top: .62em;
                         width: 7px; height: 1.5px; background: var(--gold-3); }
.svc-detail__media { overflow: hidden; }
.svc-detail__media img { transition: transform .7s var(--ease-snap); }
.svc-detail__media:hover img { transform: scale(1.03); }
@media (prefers-reduced-motion: reduce) { .svc-detail__media:hover img { transform: none; } }

/* filter pills */
/* .filters (the gallery category chips) was removed on request -- the gallery is
   scrolled, not queried. The rules went with the markup rather than being left
   here to rot; `data-tag` is still on every tile if a filter is ever wanted back. */
