/* ==========================================================================
   Evergreen Dream Home — Motion layer + Portfolio reel
   --------------------------------------------------------------------------
   Everything in here is gated behind `html.motion`, a class that
   assets/js/motion.js adds from the <head> — and only when GSAP has actually
   loaded and the visitor has not asked for reduced motion. If the CDN is
   blocked, the script 404s or the browser is set to "reduce", the class is
   never written and the site falls back to the plain CSS/IntersectionObserver
   behaviour in style.css + site.js. Nothing below may ever be load-bearing
   for layout or legibility on its own.

   Sections
     1.  Curtain (first-paint veil)
     2.  Reveal start states  — must stay in sync with HIDE in motion.js
     3.  Split-text helpers
     4.  Film hero — per-word chapter cascade
     5.  Parallax / drift hooks
     6.  Marquee handoff, steps rail, misc overrides
     7.  Portfolio reel  (works with or without motion)
   ========================================================================== */

/* ==========================================================================
   1. Preloader  (home page only)
   --------------------------------------------------------------------------
   Two layers, because one of them has to exist before any HTML has been
   parsed:

     • The sheet — body::before. A pseudo-element needs no mark-up in any
       page, so it is already painting ink over the viewport on the very first
       frame, long before a script could append anything.

     • The stage — .pl, injected by motion.js the moment <body> is parsed.
       It carries the badge, the wordmark, the meter and the four exit panels.
       Because it paints the same ink as the sheet it is already under, the
       hand-over is invisible; the visitor sees the mark rise into a black
       screen that was there from frame one.

   Both are scoped to html.m-home, which motion.js writes only on index.html —
   so the intro plays on every arrival at the home page (reloads included) and
   never on an interior page, where it would just be a toll booth.

   The delayed keyframes are failsafes: if the script dies before it can run
   the exit, they clear anyway, so the page can never be left behind a black
   screen. Everything is gated on html.motion, so with no GSAP (or reduced
   motion) there is no preloader at all.
   ========================================================================== */
html.motion.m-home body::before {
  content: ""; position: fixed; inset: 0; z-index: 520;
  background:
    radial-gradient(120% 80% at 50% 42%, rgba(213, 168, 63, .11), transparent 62%),
    var(--ink);
  animation: mCurtainFail 0s linear 6s forwards;
}
/* content:none removes the pseudo-element outright once it is off-stage */
html.motion.m-off body::before { content: none; }
@keyframes mCurtainFail { to { transform: translateY(-101%); visibility: hidden; } }

/* ---------- interior pages ----------
   With no sheet in front of them, the banner copy can paint for a frame
   before site.js has booted and the opening timeline has set its start
   states. Holding it back for that frame avoids the flicker; the delayed
   keyframe releases it regardless, so it can never be stuck hidden. */
html.motion:not(.m-home) .banner .shell {
  visibility: hidden;
  animation: mBannerFail 0s linear 4s forwards;
}
@keyframes mBannerFail { to { visibility: visible; } }

/* ==========================================================================
   The stage — an architect's drawing sheet
   --------------------------------------------------------------------------
   The old stage was a framed plate: a mat, four corner ticks and the lockup.
   It was handsome but it was decoration; it could have fronted any business.

   This one says what the company does before a word is read. The visitor
   arrives on a drafting sheet on a lit table — grid, rulers, a title block in
   the corner — and watches a house elevation draw itself in gold, line by
   line, in the order it would actually be drawn: ground, walls, roof, then
   the openings and the dimensions. The drawing then sinks back to a ghost and
   the brand settles on top of it. It leaves by the sheet being lifted away.

   Layer order inside .pl (which isolates, so none of it escapes):
     0  .pl__grid    drafting squares
     1  .pl__glow    the warm pool of the light table
     2  .pl__ruler   the two edge rulers
     3  .pl__draw    the elevation
     4  .pl__brand   badge + wordmark, over the finished drawing
     5  .pl__tb      the title block
     6  .pl__edge    the lit seam on the lifting edge
   ========================================================================== */
.pl {
  position: fixed; inset: 0; z-index: 530;
  background: var(--ink);
  overflow: hidden; isolation: isolate;
  will-change: transform;
  /* a stalled exit must never keep the page hostage */
  animation: plFail 0s linear 8s forwards;
}
@keyframes plFail { to { opacity: 0; visibility: hidden; pointer-events: none; } }

/* ---------- the sheet itself ---------- */
/* Two grids at different pitches — fine squares with a heavier line every
   fifth — which is what makes it read as drafting paper rather than graph. */
.pl__grid {
  position: absolute; inset: 0; opacity: 0;
  background-image:
    linear-gradient(rgba(213, 168, 63, .055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(213, 168, 63, .055) 1px, transparent 1px),
    linear-gradient(rgba(213, 168, 63, .105) 1px, transparent 1px),
    linear-gradient(90deg, rgba(213, 168, 63, .105) 1px, transparent 1px);
  background-size: 30px 30px, 30px 30px, 150px 150px, 150px 150px;
}
.pl__glow {
  position: absolute; left: 50%; top: 47%; transform: translate(-50%, -50%);
  width: min(1000px, 145vw); aspect-ratio: 1; opacity: 0; pointer-events: none;
  background: radial-gradient(closest-side, rgba(213, 168, 63, .17), transparent 70%);
}

/* ---------- rulers ----------
   The mask is what turns a repeating gradient into tick marks: the stripes
   run the full depth, and the mask lets only the last few pixels show, so
   they read as ticks rising off the edge instead of as bars. */
.pl__ruler { position: absolute; opacity: 0; }
.pl__ruler--t {
  top: 0; left: 0; right: 0; height: 26px;
  border-bottom: 1px solid rgba(213, 168, 63, .2);
  background: repeating-linear-gradient(90deg, rgba(213, 168, 63, .34) 0 1px, transparent 1px 24px);
  -webkit-mask-image: linear-gradient(to top, #000 0 10px, transparent 10px);
          mask-image: linear-gradient(to top, #000 0 10px, transparent 10px);
}
.pl__ruler--l {
  left: 0; top: 0; bottom: 0; width: 26px;
  border-right: 1px solid rgba(213, 168, 63, .2);
  background: repeating-linear-gradient(180deg, rgba(213, 168, 63, .34) 0 1px, transparent 1px 24px);
  -webkit-mask-image: linear-gradient(to left, #000 0 10px, transparent 10px);
          mask-image: linear-gradient(to left, #000 0 10px, transparent 10px);
}

/* ---------- the elevation ---------- */
/* One centred column: the elevation, then the brand beneath it. Layering the
   two put the wordmark straight across the face of the house, which read as a
   collision rather than as one thing handing over to the other. */
.pl__stack {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(680px, 86vw);
  display: grid; justify-items: center;
}
.pl__draw { width: 100%; height: auto; overflow: visible; }
/* non-scaling-stroke keeps every line a true hairline at any viewport width,
   which is the whole character of a drafted line. */
.pl__draw [data-draw] {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.pl__draw .is-dim  { stroke: rgba(213, 168, 63, .5);  stroke-width: .9; }
.pl__draw .is-fine { stroke: rgba(246, 230, 180, .62); }
.pl__draw text {
  fill: rgba(213, 168, 63, .55);
  font-family: var(--f-body, sans-serif);
  font-size: 9px; letter-spacing: .22em; text-transform: uppercase;
  opacity: 0;
}

/* ---------- the brand, settling on the finished drawing ---------- */
/* The block is gated as a whole rather than relying on each word's own mask.
   A masked line only stays hidden while its translate is larger than the box
   it sits in, and the descender padding on .pl__word made that margin thin
   enough for the second line to peek through before its moment. */
.pl__brand {
  display: grid; justify-items: center; text-align: center;
  width: 100%; margin-top: clamp(16px, 3vw, 32px);
  opacity: 0;
}
/* The full crest, the same one the hero reveals — house, crown, wordmark and
   strapline all in the artwork. So the two .pl__word lines are gone again:
   with the name already lettered into the mark, spelling it out underneath
   printed it twice. The tag line below is the only type left here.

   Capped at 300px because the source is 599px tall; past that it is being
   enlarged, and a soft mark reads worse than a smaller sharp one. */
.pl__mark {
  height: clamp(160px, 24vw, 300px); width: auto;
  opacity: 0; margin-bottom: 10px;
}
.pl__word { display: block; overflow: hidden; padding-bottom: .16em; margin-bottom: -.16em; }
.pl__word + .pl__word { margin-top: 12px; }
.pl__word > span {
  display: inline-block; will-change: transform;
  font-family: var(--f-display); font-weight: 700; line-height: 1;
  background: var(--gold-sheen);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.pl__word--1 > span { font-size: clamp(1.5rem, 4.4vw, 2.7rem); letter-spacing: .1em; text-transform: uppercase; }
.pl__word--2 > span { font-size: clamp(.78rem, 1.5vw, 1rem); letter-spacing: .42em; text-transform: uppercase; }
.pl__tag {
  margin-top: 18px; opacity: 0;
  font-size: .58rem; font-weight: 500; letter-spacing: .3em;
  text-transform: uppercase; color: rgba(255, 255, 255, .34);
}

/* ---------- the title block ----------
   Bottom-right, the way a real sheet is stamped. It doubles as the loading
   readout, so the progress indicator is part of the fiction rather than a
   spinner bolted onto it. */
.pl__tb {
  position: absolute;
  right: clamp(16px, 3vw, 46px); bottom: clamp(16px, 3vw, 46px);
  min-width: min(300px, 62vw); opacity: 0;
  border: 1px solid rgba(213, 168, 63, .26);
  background: rgba(8, 9, 8, .55);
  backdrop-filter: blur(2px);
}
.pl__tb .r { display: flex; align-items: center; gap: 12px; padding: 8px 13px; }
.pl__tb .r + .r { border-top: 1px solid rgba(213, 168, 63, .16); }
.pl__tb .k {
  flex: none; min-width: 62px;
  font-size: .52rem; letter-spacing: .24em; text-transform: uppercase;
  color: rgba(255, 255, 255, .3);
}
.pl__tb .v {
  flex: 1; text-align: right;
  font-size: .68rem; letter-spacing: .16em; text-transform: uppercase;
  color: rgba(246, 230, 180, .82);
}
.pl__tb .v--big {
  font-family: var(--f-display); font-size: .92rem; letter-spacing: .1em;
  color: var(--gold); font-variant-numeric: tabular-nums;
}
/* the fill sits under the block as a single hairline */
.pl__bar { height: 1px; background: rgba(255, 255, 255, .1); overflow: hidden; }
.pl__bar i {
  display: block; height: 100%; width: 100%;
  transform: scaleX(var(--pl, 0)); transform-origin: 0 50%;
  background: var(--gold-sheen);
}

/* ---------- the lifting edge ---------- */
.pl__edge {
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px; opacity: 0;
  background: linear-gradient(90deg, transparent, rgba(246, 230, 180, .95) 50%, transparent);
  transition: opacity .3s var(--ease);
}
.pl.is-lifting .pl__edge { opacity: 1; }

@media (max-width: 620px) {
  .pl__stack { width: 90vw; }
  .pl__tb { left: clamp(16px, 3vw, 46px); min-width: 0; }
  .pl__ruler--l { display: none; }
}
@media (max-height: 560px) {
  .pl__mark { height: 78px; margin-bottom: 6px; }
  .pl__tag { margin-top: 12px; }
}

/* ---------- 2. Reveal start states --------------------------------------
   The selector list below is duplicated as HIDE in motion.js and the two
   MUST match: motion.js animates every element it matches back to visible
   (bespoke timeline first, generic sweep for the rest), and that sweep is
   the only guarantee that nothing stays hidden. Add to one, add to the other.

   Excluded by ancestor: carousels, the drawer, the lightbox and the hero,
   all of which either clip their children or animate them themselves. */
html.motion :is(
  .head, .split > *, .card, .icard, .pillar, .step, .stat, .gal__item,
  .acc, .callout, .tablewrap, .mapwrap, .empty, .form-panel, .infoblock,
  .frame, .quotes, .filters, .stepbar, .deflist, .ftr__main > *,
  .plate, .reel__head, .reel__intro, .reel__foot, [data-reveal]
):not(:is(.hero, .hero__stage, .drawer-host, .lb, .quotes, .nav, .topbar, .fset, [hidden]) *):not([hidden]) {
  opacity: 0;
  visibility: hidden;
}
/* The stylesheet's own transition would double up with the GSAP tween. */
html.motion [data-reveal] { transition: none; }

/* GSAP owns the transform on these while a pointer is over them, so the
   stylesheet has to stop transitioning transform or the two fight. */
html.motion .card, html.motion .icard, html.motion .plate__media {
  transition-property: box-shadow, border-color;
}

/* ---------- 3. Split-text helpers --------------------------------------- */
/* One word: a clipping box with the glyphs riding inside it. The padding
   pair buys room for descenders that overflow:hidden would otherwise cut. */
.m-w {
  display: inline-block; overflow: hidden; vertical-align: top;
  padding-bottom: .16em; margin-bottom: -.16em;
}
.m-w > i { display: inline-block; font-style: inherit; will-change: transform; }
/* Character split, used on eyebrows only — no clipping, they just fade up. */
.m-c { display: inline-block; will-change: transform, opacity; }

/* ---------- 4. Film hero — per-word chapter cascade ----------------------
   The film hero cross-fades four chapters of copy as you scrub the shot.
   Driving the word rise from the .is-on class (rather than a timeline) means
   the cascade replays on every chapter change, forwards or backwards,
   without motion.js having to know when site.js switched them.

   Scoped to .is-live: without it site.js never sets .is-on at all (data
   saver, no canvas), and the words would be left sitting under their mask. */
/* Fast, to match the pop the chapter itself now arrives on. At 1s the words
   were still climbing out of their masks after the block had finished
   seating, so the headline assembled itself in two separate movements
   instead of one. .46s with a tighter stagger lands them together. */
html.motion .hero--film.is-live [data-hero-copy] .m-w > i {
  transform: translateY(115%);
  transition: transform .46s var(--pop) var(--d, 0s);
}
html.motion .hero--film.is-live [data-hero-copy].is-on .m-w > i { transform: none; }

/* style.css brings the hero furniture in with keyframes that hold their end
   state (`both`), which would keep overriding the inline transform GSAP
   writes. The timeline in motion.js does this work instead. */
html.motion .hero .btn-row,
html.motion .hero__pills,
html.motion .hero:not(.hero--film) [data-hero-copy] > * { animation: none; }

/* ---------- 5. Parallax / drift hooks -----------------------------------
   site.js writes --par from its own rAF loop when motion is off; when motion
   is on, motion.js writes the same variable from a scrubbed ScrollTrigger.
   Either way the stylesheet decides what the element does with it, so no
   layout can shift. `clip-path: inset(0)` is what lets the framed photos be
   over-scaled for the drift without spilling out of their box. */
html.motion .frame img,
html.motion .split__media img {
  clip-path: inset(0);
  transform: scale(var(--fz, 1));
  will-change: transform, clip-path;
}
html.motion .split__media { --fz: 1.02; }

/* Gallery tiles drift on scroll and zoom on hover. Both ride custom
   properties on one transform, so the scrubbed drift cannot wipe out the
   hover crop (or the other way round). */
html.motion .gal__item img { transform: translate3d(0, var(--gy, 0px), 0) scale(var(--gz, 1)); }
html.motion .gal__item:hover img { --gz: 1.07; }

/* ---------- 6. Handoffs and small overrides ----------------------------- */
/* The marquee becomes a GSAP ticker loop so it can react to scroll velocity. */
html.motion .marquee__track { animation: none; }
html.motion .marquee { --skew: 0deg; }
html.motion .marquee__set span { transform: skewX(var(--skew)); }

/* The process timeline's hairline becomes a real element so it can be drawn
   in as you arrive — a pseudo-element cannot be tweened. */
html.motion .step::after { display: none; }
.steps__rail {
  position: absolute; top: 16px; left: 0; right: 0; height: 1px;
  background: var(--line); overflow: hidden; z-index: -1;
}
.section--ink .steps__rail, .section--forest .steps__rail { background: rgba(255, 255, 255, .12); }
.steps__rail b {
  display: block; height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--gold-dk), var(--gold) 60%, var(--gold-lt));
  transform: scaleX(0); transform-origin: left;
}
@media (max-width: 1000px) { .steps__rail { display: none; } }

/* Reading progress: same hairline, driven by a scrubbed trigger instead. */
html.motion .progress i { transition: none; }

/* ==========================================================================
   7. Portfolio reel
   --------------------------------------------------------------------------
   The featured-projects block. It has two lives:

     • Plain (no GSAP, or narrow viewport) — a vertical stack of full-width
       "plates". Perfectly readable, no script needed.
     • Pinned (html.motion, >= 1000px) — the stage sticks to the viewport and
       the plates travel sideways as you scroll, like a reel of contact
       sheets. motion.js drives the x, the counter and the progress rail.

   Everything below the `html.motion` media query is the plain version, so a
   failure to load GSAP degrades to a normal, good-looking section.
   ========================================================================== */
.reel {
  position: relative; isolation: isolate; overflow: hidden;
  padding-block: var(--sec);
  color: rgba(255, 255, 255, .74);
  background:
    radial-gradient(90% 60% at 88% 0%, rgba(213, 168, 63, .13), transparent 60%),
    radial-gradient(70% 50% at 0% 100%, rgba(213, 168, 63, .07), transparent 62%),
    linear-gradient(180deg, var(--forest-2) 0%, var(--ink) 62%);
}
/* A gold hairline that fades at both ends, the same signature as the header,
   so the band reads as a deliberate deepening rather than a repeat of the
   dark section above it. */
.reel::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; z-index: 2;
  background: linear-gradient(90deg, transparent, rgba(213, 168, 63, .55) 26%, rgba(246, 230, 180, .8) 50%, rgba(213, 168, 63, .55) 74%, transparent);
}
.reel h2, .reel h3 { color: #fff; }

/* Oversized wordmark behind the plates. Decorative only — aria-hidden in the
   markup — and it drifts sideways on scroll when motion is on. */
.reel__ghost {
  position: absolute; left: 50%; top: 50%; z-index: -1;
  transform: translate3d(calc(-50% + var(--gx, 0px)), -50%, 0);
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(6rem, 21vw, 20rem); line-height: 1;
  letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
  color: transparent; -webkit-text-stroke: 1px rgba(213, 168, 63, .12);
  pointer-events: none; user-select: none;
}
@media (max-width: 760px) { .reel__ghost { display: none; } }

.reel__stage { position: relative; }
.reel__top { display: grid; gap: 1.1rem; margin-bottom: clamp(34px, 4vw, 56px); }
.reel__head { max-width: 30ch; }
.reel__intro { max-width: 46ch; font-size: clamp(1rem, 1.2vw, 1.08rem); }

/* Stacked, the viewport is just a shell-width column; pinned, the track takes
   over the gutters so a plate can sit dead centre of the screen. */
.reel__viewport {
  position: relative; width: 100%; max-width: var(--shell);
  margin-inline: auto; padding-inline: var(--gut);
}
.reel__track { display: grid; gap: clamp(28px, 4vw, 48px); }

/* ---------- plate ---------- */
.plate {
  position: relative;
  display: grid; gap: clamp(22px, 3vw, 46px);
  grid-template-columns: minmax(0, 1.04fr) minmax(0, 1fr);
  align-items: center;
  padding: clamp(18px, 2vw, 26px);
  background: rgba(255, 255, 255, .022);
  border: 1px solid rgba(255, 255, 255, .07);
  transition: border-color var(--t), background var(--t);
}
.plate:hover { border-color: rgba(213, 168, 63, .34); background: rgba(213, 168, 63, .035); }
@media (max-width: 820px) { .plate { grid-template-columns: 1fr; } }

.plate__media {
  position: relative; overflow: hidden;
  aspect-ratio: 4 / 3; background: #000;
  transform: translateZ(0);
}
/* The photograph is wrapped in a link; making it a block means the image can
   take its height from the plate rather than from a line box. */
.plate__media a { display: block; width: 100%; height: 100%; }
.plate__media img {
  width: 100%; height: 100%; object-fit: cover;
  /* --pz is the drift written by motion.js, --pc the hover crop. Both ride
     one transform so neither can overwrite the other. */
  transform: translate3d(0, var(--pz, 0px), 0) scale(var(--pc, 1.08));
  transition: transform .9s var(--ease);
}
.plate:hover .plate__media img { --pc: 1.15; }
/* inner hairline, so the photograph is framed rather than bled */
.plate__media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border: 1px solid rgba(255, 255, 255, .1);
  box-shadow: inset 0 -90px 90px -60px rgba(8, 9, 8, .8);
}
.plate__tag {
  position: absolute; top: 0; left: 0; z-index: 2;
  font-size: .64rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase;
  padding: .62em 1.1em; background: var(--ink); color: var(--gold);
}
.plate__tag--gold { background: var(--gold-sheen); color: var(--ink); }

.plate__body { position: relative; padding-right: clamp(0px, 3vw, 42px); }
/* Outlined index numeral, sitting over the top-right dead space. */
.plate__no {
  position: absolute; top: -.34em; right: 0;
  font-family: var(--f-display); font-weight: 700; line-height: 1;
  font-size: clamp(3.2rem, 6vw, 5.6rem);
  color: transparent; -webkit-text-stroke: 1px rgba(213, 168, 63, .38);
  pointer-events: none; user-select: none;
}
.plate__kind {
  display: inline-flex; align-items: center; gap: .7em;
  font-size: .72rem; font-weight: 500; letter-spacing: .28em; text-transform: uppercase;
  color: var(--gold);
}
.plate__kind::before { content: ""; width: 26px; height: 1px; background: var(--gold); flex: none; }
.plate h3 {
  font-size: clamp(1.6rem, 2.6vw, 2.35rem); margin-top: 1rem;
  max-width: 22ch;
}
.plate__loc {
  display: flex; align-items: center; gap: .5em; margin-top: .8rem;
  font-size: .82rem; letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255, 255, 255, .56);
}
.plate__loc svg { width: 14px; height: 14px; color: var(--gold); flex: none; }
.plate__blurb { margin-top: 1.15rem; max-width: 46ch; }

.plate__specs {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  margin-top: 1.6rem; border-top: 1px solid rgba(255, 255, 255, .1);
}
.plate__specs > div { padding: 1rem 1.2rem 0 0; }
.plate__specs > div + div { padding-left: 1.2rem; border-left: 1px solid rgba(255, 255, 255, .1); }
.plate__specs dt {
  font-size: .66rem; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  color: rgba(255, 255, 255, .45);
}
.plate__specs dd {
  margin: .45rem 0 0; font-family: var(--f-serif); font-size: 1.16rem;
  font-weight: 600; color: #fff;
}
.plate__specs dd small {
  font-family: var(--f-sans); font-size: .58em; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: rgba(255, 255, 255, .5);
  margin-left: .3em;
}
@media (max-width: 480px) {
  .plate__specs { grid-auto-flow: row; grid-auto-columns: auto; }
  .plate__specs > div + div { padding-left: 0; border-left: 0; border-top: 1px solid rgba(255, 255, 255, .1); }
}
.plate__price {
  margin-top: 1.3rem; font-family: var(--f-serif); font-size: 1.25rem;
  font-weight: 600; color: var(--gold-lt);
}
.plate__cta { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px; margin-top: 1.8rem; }

/* the closing plate — an invitation rather than a project */
.plate--end {
  grid-template-columns: 1fr; place-items: center; text-align: center;
  padding: clamp(38px, 6vw, 76px);
  border-color: rgba(213, 168, 63, .3);
  background:
    radial-gradient(90% 90% at 50% 0%, rgba(213, 168, 63, .09), transparent 65%),
    rgba(255, 255, 255, .02);
}
.plate--end h3 { font-size: clamp(1.7rem, 3vw, 2.6rem); max-width: 20ch; }
.plate--end p { max-width: 44ch; margin-top: 1.1rem; }
.plate--end .btn-row { justify-content: center; }

/* ---------- footer rail (only meaningful while pinned) ---------- */
.reel__foot { display: none; }

/* ---------- pinned, horizontal variant ---------- */
@media (min-width: 1000px) {
  html.motion .reel { padding-block: 0; }
  html.motion .reel__stage {
    height: 100svh; min-height: 660px;
    display: flex; flex-direction: column;
    padding-block: clamp(26px, 4vh, 54px);
    overflow: hidden;
  }
  html.motion .reel__top {
    flex: none; margin-bottom: clamp(16px, 2.4vh, 30px);
    grid-template-columns: 1fr auto; align-items: end; gap: 12px 40px;
    width: 100%; max-width: var(--shell); margin-inline: auto;
    padding-inline: var(--gut);
  }
  html.motion .reel__head { margin: 0; }
  html.motion .reel__intro { text-align: right; }

  html.motion .reel__viewport {
    flex: 1; min-height: 0; display: flex; align-items: center;
    max-width: none; padding-inline: 0;
  }
  html.motion .reel__track {
    display: flex; align-items: center; width: max-content;
    gap: clamp(26px, 3vw, 54px);
    /* first plate lines up with the shell, last one can reach the middle */
    padding-inline: max(var(--gut), calc((100vw - var(--shell)) / 2 + var(--gut)));
    will-change: transform;
  }
  html.motion .plate {
    flex: none; width: min(1020px, 82vw);
    /* One row that fills the fixed height, so the photograph runs the full
       depth of the plate and the copy centres against it. */
    height: min(560px, 62vh); grid-template-rows: minmax(0, 1fr);
  }
  html.motion .plate__media { aspect-ratio: auto; height: 100%; }
  html.motion .plate h3 { font-size: clamp(1.5rem, 2.1vw, 2.1rem); margin-top: .8rem; }
  html.motion .plate__blurb { margin-top: .9rem; }
  html.motion .plate__specs { margin-top: 1.2rem; }
  html.motion .plate__cta { margin-top: 1.3rem; }
  html.motion .plate--end { width: min(720px, 62vw); }

  html.motion .reel__foot {
    flex: none; display: flex; align-items: center; gap: clamp(18px, 3vw, 40px);
    width: 100%; max-width: var(--shell); margin-inline: auto;
    padding-inline: var(--gut); margin-top: clamp(16px, 2.4vh, 30px);
  }
  html.motion .reel__count {
    font-size: .82rem; letter-spacing: .22em; color: rgba(255, 255, 255, .5);
    font-variant-numeric: tabular-nums; display: inline-flex; align-items: center; gap: .7em;
    flex: none;
  }
  html.motion .reel__count b { color: var(--gold); font-weight: 600; font-size: 1rem; }
  html.motion .reel__count i { width: 22px; height: 1px; background: rgba(255, 255, 255, .28); }
  html.motion .reel__bar { flex: 1; height: 1px; background: rgba(255, 255, 255, .14); overflow: hidden; }
  html.motion .reel__bar i {
    display: block; height: 100%; width: 100%;
    background: linear-gradient(90deg, var(--gold-dk), var(--gold) 55%, var(--gold-lt));
    transform: scaleX(var(--rp, 0)); transform-origin: left;
  }
  html.motion .reel__nav { display: flex; gap: 8px; flex: none; }
  html.motion .reel__nav button {
    width: 44px; height: 44px; display: grid; place-items: center; color: #fff;
    border: 1px solid rgba(255, 255, 255, .26); border-radius: 50%;
    transition: background var(--t), border-color var(--t), color var(--t), opacity var(--t);
  }
  html.motion .reel__nav button:hover { background: var(--gold); border-color: var(--gold); color: var(--ink); }
  html.motion .reel__nav button[disabled] { opacity: .3; pointer-events: none; }
  html.motion .reel__nav svg { width: 15px; height: 15px; }
}
/* Short desktop viewports: give the copy back some room. */
@media (min-width: 1000px) and (max-height: 780px) {
  html.motion .plate__blurb { display: none; }
  html.motion .plate { height: min(500px, 68vh); }
}

/* ==========================================================================
   Atmosphere & depth
   --------------------------------------------------------------------------
   The page already animates a great deal. What it did not have was DEPTH —
   the sense of a lit, physical surface rather than flat panels that move.
   Four quiet layers, none of which add more movement to compete with the
   reveals already running:

     grain   a fixed film-grain sheet, so the ink reads as leather and the
             cream as paper instead of as pure #000/#fff fills
     glow    a soft gold pool that follows the pointer. Blended with `screen`,
             so it lifts the dark bands and all but disappears over the light
             ones — no per-section detection needed
     seam    a hairline that draws itself across the top edge of a dark band
             as that band arrives
     tilt    cards lean fractionally toward the cursor, on a real perspective

   All four are inert without JS and are only injected when the motion engine
   is active, so a reduced-motion visitor never sees any of it.
   ========================================================================== */

.atm {
  position: fixed; inset: 0;
  pointer-events: none;          /* never intercepts a click or a hover */
  z-index: 2;
}

/* Fine fractal noise, rendered once as an inline SVG so it costs no request.
   Kept far below the threshold where it reads as dirt — at this opacity it is
   felt rather than seen, which is the whole point. */
.atm--grain {
  opacity: .03;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

.atm--glow {
  opacity: 0;
  mix-blend-mode: screen;
  transition: opacity .6s var(--ease, cubic-bezier(.22,1,.36,1));
  /* 240px, down from 460. At the old radius the pool was ~920px across — wider
     than most of the content it sat on, so it stopped reading as a light being
     carried over the page and started reading as an overall haze that dulled
     whatever was underneath. The falloff is pulled in with it (58% instead of
     70%) so the edge actually resolves; the centre is a touch stronger to keep
     the same presence from a smaller pool. */
  background: radial-gradient(240px circle at var(--gx, 50%) var(--gy, 38%),
              rgba(213, 168, 63, .17),
              rgba(213, 168, 63, .05) 36%,
              transparent 58%);
}
html.motion.m-off .atm--glow,
body.no-glow .atm--glow { opacity: 0 !important; }

/* ---------- the drawn seam ---------- */
.seam {
  position: absolute; left: 0; right: 0; top: 0; height: 1px;
  transform: scaleX(0); transform-origin: 50% 50%;
  background: linear-gradient(90deg,
              transparent,
              rgba(213, 168, 63, .55) 24%,
              rgba(246, 230, 180, .8) 50%,
              rgba(213, 168, 63, .55) 76%,
              transparent);
  pointer-events: none;
  z-index: 4;
}

/* ---------- tilt ----------
   The perspective lives on the PARENT. Putting it on the card itself would
   make the vanishing point travel with the card and flatten the effect. */
/* Scoped to a pointer that can actually hover, because that is exactly when
   tilt() runs. Without the guard the `transform: none` below would still
   apply on a touch device, where tilt() bails out early — and the card would
   lose its lift with nothing put back in its place. */
@media (hover: hover) and (pointer: fine) {
  html.motion .svcs,
  html.motion .grid--tilt { perspective: 1200px; }
  html.motion .svc { transform-style: preserve-3d; will-change: transform; }
  /* The lift used to come from .svc:hover in sections.css. Once the tilt
     writes an inline transform that rule can no longer reach it, so the lift
     is folded into the JS instead — see tilt() in motion.js. */
  html.motion .svc:hover { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .atm, .seam { display: none; }
}
@media (max-width: 900px) {
  /* No pointer to follow, and grain over a phone screen is wasted paint. */
  .atm--glow { display: none; }
  .atm--grain { opacity: .022; }
}
