/* ============================================================
   Tatsam Atelier — design tokens
   ============================================================ */
:root{
  --maroon-950: #220503;
  --maroon-900: #360806;
  --maroon-800: #4a0d0a;
  --maroon-700: #5e130f;
  --maroon-600: #7a1d17;
  --maroon-ink: #260603; /* footer address heading, rule and emblem (assets/logo-icon-dark.webp is filled with this same colour) */
  /* The red plaster texture behind the page title bands, reused on cards
     and panels. Both variants are anchored right of centre: the image's
     left third is a bright gold patch that washes out gold text on top of
     it. A maroon veil calms the grain so card type stays legible.
     --maroon-texture is for wide cards carrying paragraphs: scaled up so
     they crop into the quiet middle, with a heavier veil.
     --maroon-texture-sm is for pills, buttons and the mobile menu: natural
     size so a small box still catches some grain, with a lighter veil. */
  --maroon-texture:
    linear-gradient(rgba(34,5,3,0.45), rgba(34,5,3,0.45)),
    url("assets/bottom-bg.webp") 72% 45% / max(100%, 2200px) auto no-repeat
    var(--maroon-800);
  --maroon-texture-sm:
    linear-gradient(rgba(34,5,3,0.25), rgba(34,5,3,0.25)),
    url("assets/bottom-bg.webp") 68% 45% / max(100%, 1384px) auto no-repeat
    var(--maroon-800);
  --near-black: #0c0705;
  /* The tan sections' fine grain as a background layer, for ochre elements
     whose ::before is already taken (the buttons' hover sheen). Same noise
     at the same 12%, baked into the SVG instead of set on an overlay. */
  --ochre-grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.35' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.12'/></svg>");

  --gold-100: #98561c;
  --gold-200: #98561c;
  --gold-300: #98561c;
  --gold-400: #98561c;
  --gold-500: #98561c;
  --gold-600: #98561c;

  --tan-bg: #98561c;
  --tan-bg-dark: #98561c;

  --cream: #faf6ee;
  --ink: #1c1109;

  --font-display: 'Homes Royale', Georgia, serif;
  --font-sans: 'Poppins', 'Segoe UI', sans-serif;
  --body-size: 1.1rem; /* every body paragraph (not CTA lead, footer or labels) */
  --display-stroke: 0.018em; /* emulates a 500 weight on Homes Royale */

  --container: 1240px;
  --radius: 6px;

  /* ---- motion ----
     One vocabulary for the whole site. --ease-out-expo is the house curve:
     it leaves fast and settles for a long time, which is what makes the
     gold curtain on the Process images read as "drawn back" rather than
     "slid". Everything else hangs off it. */
  --ease-out-expo: cubic-bezier(.16,1,.3,1);
  --ease-in-out-quart: cubic-bezier(.76,0,.24,1);
  --ease-out-soft: cubic-bezier(.25,.46,.45,.94);
  /* Even in, even out, no long tail — for the few transitions that move a
     box's geometry rather than its transform (the Process image slot). Expo's
     near-instant departure reads as a snap when the browser has to reflow
     behind it. */
  --ease-smooth: cubic-bezier(.4,0,.2,1);
  --dur-fast: .35s;
  --dur-mid: .7s;
  --dur-slow: 1.1s;
  --dur-veil: 1.3s;
}

/* `block`, not `swap`. Homes Royale carries every heading, and `swap` would
   paint them in Georgia first, then reflow when the real face lands. The file
   is preloaded in every page's <head> and under 32KB, so it wins the race in
   practice; holding the text invisible for those few milliseconds is cheaper
   than painting it wrong. Only the 400 upright face exists, and browsers only fake bold
   from 600 up, so every Homes Royale rule asks for 500 and thickens the
   strokes itself with a hairline outline (--display-stroke) to read as a
   medium. body sets font-synthesis:none so nothing else gets faked. */
@font-face{
  font-family:'Homes Royale';
  src:url('assets/fonts/HomesRoyale.woff2') format('woff2'),
      url('assets/fonts/HomesRoyale.woff') format('woff');
  font-weight:400;
  font-style:normal;
  font-display:block;
}

*,*::before,*::after{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }
body{
  margin:0;
  background:var(--maroon-900);
  color:var(--cream);
  font-family:var(--font-sans);
  font-weight:300;
  line-height:1.6;
  font-synthesis:none;
  -webkit-font-smoothing:antialiased;
}
img{ max-width:100%; height:auto; display:block; }
a{ color:inherit; text-decoration:none; }
ul,ol{ list-style:none; margin:0; padding:0; }
h1,h2,h3{ margin:0; font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; }
p{ margin:0; }
.section-title{
  font-family:var(--font-display);
  font-size:clamp(2.2rem,4vw,3rem);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  text-align:center;
  color:var(--maroon-900);
}
.section-title.light{ color:var(--gold-200); }

/* subtle focus visibility for a11y */
a:focus-visible, button:focus-visible{
  outline:2px solid var(--gold-200);
  outline-offset:3px;
}
@media (prefers-reduced-motion: reduce){
  *{ animation-duration:0.001ms !important; transition-duration:0.001ms !important; scroll-behavior:auto !important; }
}

/* ============================================================
   Textured maroon backgrounds
   ============================================================ */
.texture-maroon{
  position:relative;
  background:var(--maroon-texture);
}
.texture-maroon::before{
  content:"";
  position:absolute; inset:0;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/></svg>");
  mix-blend-mode:overlay;
  pointer-events:none;
}

/* ============================================================
   Buttons
   ============================================================ */
.btn{
  display:inline-flex; align-items:center; justify-content:center;
  padding:0.85em 2em;
  border-radius:999px;
  font-family:var(--font-sans);
  font-size:0.92rem;
  font-weight:500;
  letter-spacing:0.02em;
  cursor:pointer;
  transition:transform var(--dur-fast) var(--ease-out-expo),
             box-shadow var(--dur-fast) var(--ease-out-soft),
             background var(--dur-fast) var(--ease-out-soft),
             color var(--dur-fast) var(--ease-out-soft);
  border:1px solid transparent;
  white-space:nowrap;
  position:relative;
  overflow:hidden;
  isolation:isolate;
}
.btn:hover{ transform:translateY(-2px); }
.btn:active{ transform:translateY(0); transition-duration:.08s; }
.btn-outline-gold{
  background:transparent;
  border-color:var(--gold-400);
  color:var(--gold-100);
}
.btn-outline-gold:hover{ background:var(--ochre-grain), var(--gold-400); color:var(--maroon-900); }
.btn-gold{
  background:var(--ochre-grain), #98561c;
  color:var(--maroon-900);
  font-weight:600;
  box-shadow:0 8px 20px rgba(0,0,0,0.35);
}
.btn-gold:hover{ box-shadow:0 12px 26px rgba(0,0,0,0.45); }

/* ============================================================
   Header
   ============================================================ */
.site-header{
  position:fixed; top:0; left:0; right:0; z-index:100;
  padding:12px 4vw;
  display:flex;
  /* No bar at the top of the page: the logo and links sit straight on the
     hero. The textured bar only appears once scrolled (.scrolled below). */
  background:transparent;
  transition:background .35s ease, padding .35s ease;
}
.site-header.scrolled,
/* The portfolio page opens on tan rather than a dark hero or title band, so
   a see-through bar would leave the gold logo and button unreadable: there
   the bar is present from the start. */
body:has(.pf-page) .site-header{
  /* The maroon texture, cropped for a full-width 80px strip: scaled to 3000px
     so the bar only spans the image's quiet middle. At the shared variants'
     sizes the bright gold patch on the image's left edge sat behind the
     logo and washed it out. */
  background:
    linear-gradient(rgba(34,5,3,0.3), rgba(34,5,3,0.3)),
    url("assets/bottom-bg.webp") 75% 40% / max(100%, 3000px) auto no-repeat
    var(--maroon-800);
  /* Deliberately no `backdrop-filter:blur()`. The bar is fixed and the
     content beneath it moves every frame, so the blurred backdrop can
     never be cached — it was a full-width multi-pass GPU blur on every
     scroll frame, for the whole page. The bar is opaque now anyway. */
  padding:9px 4vw;
  box-shadow:0 6px 24px rgba(0,0,0,0.35);
}
.header-inner{
  width:100%;
  max-width:1400px;
  margin:0 auto;
  display:grid;
  /* Equal side tracks so the middle one — and therefore the nav — is centred
     on the bar. With `auto 1fr auto` the wider CTA column shoved the nav ~60px
     left of centre. minmax(0,1fr) lets the sides shrink instead of overflowing
     on narrower desktops. */
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:center;
  gap:32px;
}
.logo{ display:flex; align-items:center; gap:10px; margin-left:128px; justify-self:start; }
.logo-img{ height:82px; width:auto; }

/* Sits a touch left of dead centre — optical balance against the wider
   CTA button on the right. The mobile drawer resets `right` below. */
.main-nav{
  display:flex; align-items:center; justify-content:center; gap:26px;
  position:relative; right:20px;
}
/* Only shown inside the mobile drawer, where .header-cta is hidden. */
.nav-cta{ display:none; }
.nav-scrim{ display:none; }
.nav-link{
  font-size:0.85rem; font-weight:400; color:var(--cream); opacity:0.9;
  position:relative; padding-bottom:4px;
}
.nav-link.active{ color:var(--gold-200); opacity:1; }
.nav-link::after{
  content:""; position:absolute; left:0; bottom:0; height:1px; width:0;
  background:var(--gold-300); transition:width .25s ease;
}
.nav-link:hover::after{ width:100%; }

.header-cta{
  justify-self:end;
  margin-left:20px;
  margin-right:96px;
  padding:0.72em 1.75em;
  font-size:0.82rem;
  font-weight:600;
  background:var(--ochre-grain), #98561c;
  color:var(--maroon-900);
  box-shadow:none;
  border:none;
}
.header-cta:hover{
  background:var(--ochre-grain), #98561c;
  color:var(--maroon-900);
  box-shadow:0 6px 16px rgba(0,0,0,0.25);
}
.menu-toggle{
  display:none; flex-direction:column; gap:5px;
  background:none; border:none; cursor:pointer; padding:6px;
}
.menu-toggle span{ width:24px; height:1.5px; background:var(--gold-200); }

/* side vertical tab + whatsapp */
.side-tab-stack{
  position:fixed; right:0; top:58%;
  transform:translateY(-50%);
  z-index:90;
  display:flex; flex-direction:column;
  box-shadow:-2px 0 10px rgba(0,0,0,0.25);
}
.side-tab{
  transform:rotate(180deg);
  writing-mode:vertical-rl;
  background:var(--cream);
  color:var(--maroon-800);
  font-size:0.72rem; font-weight:600; letter-spacing:0.08em;
  padding:14px 8px;
  text-decoration:none;
  display:flex; align-items:center; justify-content:center;
}
.side-tab-whatsapp{
  background:var(--gold-400);
  color:#fff;
  padding:10px 0;
  text-decoration:none;
  display:flex; align-items:center; justify-content:center;
}
.side-tab-whatsapp svg{ width:20px; height:20px; }
/* ============================================================
   Hero
   ============================================================ */
.hero{
  position:relative;
  background:var(--maroon-900);
}
/* The whole photo, never cropped: the hero takes the image's own aspect
   ratio rather than a viewport height, so nothing is cut off on any screen
   (the old full-viewport, object-fit:cover layers trimmed it and scaled the
   rest up soft). Phones crop the sides — see the max-width:600px block below. */
.hero-img{ display:block; width:100%; height:auto; }
.hero-scrim{
  position:absolute; inset:0;
  background:
    linear-gradient(180deg, rgba(10,4,2,0.55) 0%, rgba(10,4,2,0.15) 30%, rgba(10,4,2,0.35) 75%, rgba(10,4,2,0.75) 100%);
}
/* Sized and placed in percentages of the image, so it always sits on the
   wall in the same spot: centred, wholly clear of the sofa (whose crest is at
   ~54% of the image height) and of the chandelier to its right. 12% of the
   width is 21.3% of the 16:9 height, so 30.5% + 21.3% puts its foot at ~52%.
   No shadow or opacity on it anywhere: the file itself is one flat #98561c,
   and either would blend it into a second tone. */
.hero-emblem{
  position:absolute; z-index:1;
  left:50%; top:30.5%;
  width:12%; height:auto;
  transform:translateX(-50%);
}
/* Phones are the one exception to "never cropped": at 16:9 the hero was a
   ~220px strip, a quarter of the first screen, with the fixed header over a
   third of it. Here the zoom comes from a tall 4:7 box: cover scales the
   photo to the box's height, so the full height — wall to rug — is always
   kept and only the sides are trimmed, to the middle 32% (34%–66%) of the
   image. That frames the emblem with the sofa's back and cushions below it.
   (Zooming with transform:scale instead also cut off the top and bottom.)
   To zoom further or less, change the ratio's second number, then the
   emblem: nothing is trimmed vertically, so its top:30.5% always holds, and
   its 12% of the image width is 12% × 16/9 × 7/4 ≈ 37.3% of the box's width.
   The source is only 1200px wide, so it softens as the zoom goes up.
   The crop also brings the top of the photo under the logo and menu toggle,
   so the scrim is darker across the header. */
@media (max-width:600px){
  .hero{ aspect-ratio:4 / 7; overflow:hidden; }
  .hero-img{ height:100%; object-fit:cover; object-position:50% 50%; }
  .hero-emblem{ width:37.3%; }
  .hero-scrim{
    background:
      linear-gradient(180deg, rgba(10,4,2,0.85) 0%, rgba(10,4,2,0.55) 16%, rgba(10,4,2,0.15) 32%, rgba(10,4,2,0.35) 75%, rgba(10,4,2,0.75) 100%);
  }
}
/* ============================================================
   About
   ============================================================ */
.about{
  background:url("assets/about-lobby.webp") center/cover no-repeat;
  background-blend-mode:normal;
  padding:120px 6vw 100px;
}
.about-inner{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:0.9fr 1.1fr;
  gap:70px;
  align-items:center;
}
/* This block used to carry `transform:translate(220px,-240px)`, but it never
   took effect: .about-copy was in the old scroll-reveal list, and both
   `.reveal` and `.reveal.in` set their own transform, which overrode it from
   first paint onwards. Restoring it now that the reveal no longer touches
   this element pushed the heading up out of the section and gave the page a
   7px horizontal scrollbar, so it is gone for good rather than resurrected. */
.about-image{ position:relative; }
.about-image img{ width:100%; border-radius:2px; box-shadow:0 30px 60px rgba(0,0,0,0.45); }
.about-badge{
  position:absolute; right:-60px; top:90px;
  animation:spin 18s linear infinite;
  filter:drop-shadow(0 3px 8px rgba(0,0,0,0.3));
}
@keyframes spin{ to{ transform:rotate(360deg); } }
.about-copy h1{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(2.3rem,3.9vw,3.2rem);
  color:var(--gold-400);
  margin-bottom:28px;
  line-height:1.2;
}
.about-copy p{
  color:rgba(250,246,238,0.92);
  font-size:var(--body-size);
  font-weight:300;
  max-width:52ch;
}

/* ============================================================
   Spaces that inspire
   ============================================================ */
.spaces{
  background:var(--tan-bg);
  padding:100px 6vw 110px;
  text-align:center;
}
.spaces .section-title{ margin-bottom:36px; font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; color:var(--maroon-900); }
.tabs{
  display:inline-flex;
  /* background:var(--near-black); */
  border-radius:999px;
  padding:6px;
  margin-bottom:56px;
  gap:4px;
}
.tab{
  border:none; background:transparent; color:var(--gold-100);
  font-family:var(--font-sans); font-size:0.92rem; font-weight:400;
  padding:0.75em 2em; border-radius:999px; cursor:pointer;
  transition:background .25s ease, color .25s ease;
}
.tab.active{ background:var(--maroon-700); color:var(--gold-100); }

.spaces-grid{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:26px;
}
.space-card{
  position:relative;
  margin:0;
  border-radius:4px;
  overflow:hidden;
  aspect-ratio:3/4;
  box-shadow:0 20px 40px rgba(0,0,0,0.25);
}
.space-card img{ width:100%; height:100%; object-fit:cover; transition:transform .6s ease; }
.space-card:hover img{ transform:scale(1.06); }
.space-card figcaption{
  /* Overshoots the frame by a pixel on all three edges rather than sitting flush
     at bottom:0. The card's aspect-ratio height is fractional and the hover
     translate puts it on its own layer, so a flush scrim rounds to a hairline of
     bare image along the bottom. .space-card clips the overshoot. */
  position:absolute; left:-1px; right:-1px; bottom:-1px;
  padding:22px 22px 21px;
  background:linear-gradient(180deg, transparent, rgba(0,0,0,0.75));
  display:flex; flex-direction:column; align-items:flex-start;
  text-align:left;
}
.cap-title{ font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; font-size:1.4rem; color:var(--gold-100); }
.cap-sub{ font-size:0.8rem; color:rgba(255,255,255,0.85); }
/* Stretched over the card so the image and its caption are both the link. */
.space-link{ position:absolute; inset:0; z-index:2; }
.space-card:focus-within{ outline:2px solid var(--gold-200); outline-offset:4px; }

.spaces-more{
  max-width:var(--container);
  margin:34px auto 0;
  padding-top:16px;
  text-align:center;
}
.spaces-more a{
  font-size:1rem; font-weight:500; letter-spacing:0.12em; text-transform:uppercase;
  color:var(--maroon-900);
  border-bottom:1px solid rgba(0,0,0,0.45);
  padding-bottom:4px;
}
.spaces-more a:hover{ color:var(--maroon-600); border-bottom-color:var(--maroon-600); }

/* ============================================================
   Process
   ============================================================ */
.process{
  background:url("assets/our-process-bg.webp") center/cover no-repeat;
  padding:120px 6vw;
}
.process-inner{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:0.9fr 1.1fr;
  gap:60px;
}
/* Grid items default to min-width:auto, so the 486px step image would force
   the track wider than the viewport and give the whole page a sideways scroll. */
.process-inner > *{ min-width:0; }
.process-intro{
  /* Keep the section title in view while its companion timeline is read.
     Sticky is deliberately scoped to the desktop two-column composition; once
     the layout stacks, the title returns to the natural reading order below. */
  position:sticky;
  top:108px;
  align-self:start;
}
.eyebrow{
  display:block;
  font-family:var(--font-sans);
  font-weight:600;
  font-size:0.95rem;
  color:var(--gold-400);
  margin-bottom:18px;
}
.process-intro h2{
  font-size:clamp(2.2rem,3.6vw,3rem);
  color:var(--gold-100);
  line-height:1.18;
}
.timeline{ position:relative; padding-left:34px; }
.timeline::before{
  content:""; position:absolute; left:5px; top:10px; bottom:10px; width:1px;
  background:linear-gradient(#A09C9C, rgba(160,156,156,0.15));
}
.timeline-step{ position:relative; padding-bottom:52px; }
.timeline-step:last-child{ padding-bottom:0; }
.step-num{
  display:block;
  font-family:var(--font-sans); font-weight:600; font-size:0.9rem;
  color:var(--gold-500); letter-spacing:0.1em; margin-bottom:8px;
}
.step-dot{
  position:absolute; left:-34px; top:26px;
  width:11px; height:11px; border-radius:50%;
  background:#98561c;
  box-shadow:0 0 0 4px rgba(152,86,28,0.15);
}
.step-body h3{
  /* Same display face and gold as the hero headline. */
  font-family:var(--font-display);
  font-size:clamp(1.8rem,2vw,2.15rem); color:var(--gold-400); margin-bottom:10px; font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  letter-spacing:0.01em;
}
.step-body p{ color:rgba(250,246,238,0.85); font-size:var(--body-size); max-width:42ch; }
.step-image-wrap{
  position:relative;
  overflow:hidden;
  margin-top:20px;
  width:486px; height:254px;
  max-width:100%;
  border-radius:3px;
  box-shadow:0 20px 40px rgba(0,0,0,0.35);
}
/* The curtain-wipe that used to live here is now the shared [data-anim="settle"]
   reveal at the foot of this file — .step-image-wrap just opts into it. Note
   the base image is un-zoomed: with JS off the photo simply shows, where the
   old rule left it stuck at scale(1.3). */
.step-image{
  display:block;
  width:100%; height:100%;
  object-fit:cover;
}

/* On wide screens the process reads as a sequence of chapters: the card
   closest to the viewport centre expands and is the sole photograph in view.
   Inactive frames collapse so the timeline stays compact; the photo wipes
   in and settles rather than snapping, which is what the scroll loop's
   class toggle is for. */
@media (min-width:1081px){
  .step-body{
    transform-origin:left center;
    transition:transform var(--dur-mid) var(--ease-smooth),
               filter var(--dur-mid) var(--ease-out-soft);
  }
  /* Opening the slot is the one thing here that touches layout, so height is
     the only property animated on the frame itself — evenly eased, and with
     `contain` keeping the reflow inside the frame rather than letting it walk
     the rest of the timeline on every frame. The frame no longer scales or
     animates a clip-path: both forced the composited layer (image included)
     to be re-rasterised each frame, which is what read as gritty rather than
     soft. The reveal itself is now just the photograph fading in below — pure
     opacity, which the compositor runs without repainting anything. */
  .step-image-wrap{
    max-height:0;
    margin-top:0;
    contain:layout paint;
    transition:max-height var(--dur-mid) var(--ease-smooth),
               margin-top var(--dur-mid) var(--ease-smooth);
  }
  /* No zoom on the photograph: a scaled image is resampled on every frame of
     the transition, and that is what read as soft. It fades in at its natural
     size instead, so what the browser paints is pixel-for-pixel the same image
     throughout — the only thing animating is opacity, which the compositor
     handles without touching the bitmap. */
  .step-image{
    opacity:0;
    backface-visibility:hidden;
    will-change:opacity;
    transition:opacity var(--dur-mid) var(--ease-out-soft);
  }
  .timeline-step.is-process-active .step-body{
    transform:scale(1.055);
    filter:drop-shadow(0 18px 26px rgba(0,0,0,.2));
  }
  .timeline-step.is-process-active .step-image-wrap{
    max-height:254px;
    margin-top:20px;
  }
  /* The photograph trails the opening slot by 80ms so it arrives into a frame
     that is already unfolding instead of appearing with the height change. */
  .timeline-step.is-process-active .step-image{
    opacity:1;
    transition-delay:80ms;
  }
}

/* ============================================================
   Categories
   ============================================================ */
.categories{
  background:var(--tan-bg);
  padding:160px 6vw;
}
.categories .section-title{ margin-bottom:56px; }
.categories-grid{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:repeat(4,1fr);
  gap:24px;
}
.cat-card{
  margin:0; position:relative;
  border-radius:4px; overflow:hidden;
  aspect-ratio:3/4;
  box-shadow:0 16px 30px rgba(0,0,0,0.2);
}
.cat-card img{ width:100%; height:100%; object-fit:cover; transition:transform .6s ease; }
.cat-card:hover img{ transform:scale(1.06); }
.cat-card figcaption{
  position:absolute; left:18px; bottom:16px;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:2rem; letter-spacing:0.3px; color:var(--gold-100);
  text-shadow:0 2px 14px rgba(0,0,0,0.7);
}

/* Wallpaper collection — auto-scrolling carousel (pauses on hover) */
.wallpaper-carousel{
  position:relative;
  overflow:hidden;
  /* room for the card shadow and the hover lift, which overflow:hidden
     would otherwise clip against the section */
  padding:52px 0 64px;
  /* fade the edges so cards slide in/out softly */
  -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
  mask-image:linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
}
.wp-track{
  display:flex;
  width:max-content;
  animation:wp-scroll 110s linear infinite;
  will-change:transform;
}
.wallpaper-carousel:hover .wp-track,
.wallpaper-carousel:focus-within .wp-track{ animation-play-state:paused; }
@keyframes wp-scroll{
  from{ transform:translateX(0); }
  to{ transform:translateX(-50%); }
}
.wp-card{
  flex:0 0 auto;
  width:300px;
  margin:0 30px 0 0; position:relative;
  border-radius:4px; overflow:hidden;
  aspect-ratio:3/4;
  box-shadow:0 16px 30px rgba(0,0,0,0.2);
}
.wp-card img{ width:100%; height:100%; object-fit:cover; transition:transform .6s ease; }
.wp-card::after{
  /* inset:-1px, not 0 — same reason as .space-card figcaption. The marquee
     animates .wp-track by a fractional translateX every frame, so a scrim flush
     with the card's rounded clip rounds to a hairline of bare image along the
     edges. .wp-card clips the overshoot. */
  content:""; position:absolute; inset:-1px; pointer-events:none;
  background:linear-gradient(to top, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.12) 32%, rgba(0,0,0,0) 55%);
  transition:opacity .35s ease;
}
.wp-card:hover img{ transform:scale(1.06); }
.wp-card figcaption{
  position:absolute; left:0; right:0; bottom:16px;
  z-index:1; text-align:center;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:1.25rem; letter-spacing:2.5px; color:var(--gold-100);
  text-shadow:0 2px 14px rgba(0,0,0,0.8);
}
.wp-card{ cursor:zoom-in; }
.wp-card:focus-visible{ outline:2px solid var(--maroon-900); outline-offset:4px; }

/* Wallpaper lightbox — the <dialog> itself fills the viewport so a click on
   its bare area reads as a backdrop click (see script.js). */
html.lightbox-open{ overflow:hidden; }
.wp-lightbox{
  width:100%; height:100%; max-width:none; max-height:none;
  margin:0; padding:clamp(56px,8vh,90px) clamp(16px,7vw,110px);
  border:0;
  background:rgba(22,4,3,0.92);
  color:var(--cream);
  align-items:center; justify-content:center;
}
.wp-lightbox[open]{ display:flex; animation:wpl-in .3s var(--ease-out-soft); }
.wp-lightbox::backdrop{ background:transparent; }
@keyframes wpl-in{ from{ opacity:0; } to{ opacity:1; } }
.wpl-figure{
  margin:0;
  display:flex; flex-direction:column; align-items:center; gap:18px;
  max-width:100%; max-height:100%;
}
.wpl-img{
  display:block;
  max-width:100%;
  max-height:calc(100vh - 2 * clamp(56px,8vh,90px) - 60px);
  width:auto; height:auto;
  border-radius:4px;
  box-shadow:0 30px 60px rgba(0,0,0,0.5);
}
.wpl-caption{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:1.6rem; letter-spacing:3px; color:var(--gold-100);
}
.wpl-btn{
  position:absolute;
  display:flex; align-items:center; justify-content:center;
  width:48px; height:48px;
  border:1px solid rgba(152,86,28,0.6); border-radius:50%;
  background:rgba(22,4,3,0.55);
  color:var(--gold-100);
  cursor:pointer;
  transition:background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.wpl-btn:hover{ background:var(--gold-100); color:var(--maroon-900); }
.wpl-btn:focus-visible{ outline:2px solid var(--gold-100); outline-offset:3px; }
.wpl-close{ top:18px; right:18px; }
.wpl-prev{ left:clamp(10px,2vw,32px); top:50%; transform:translateY(-50%); }
.wpl-next{ right:clamp(10px,2vw,32px); top:50%; transform:translateY(-50%) scaleX(-1); }
@media (max-width:600px){
  .wpl-prev, .wpl-next{ top:auto; bottom:16px; transform:none; }
  .wpl-prev{ left:calc(50% - 60px); }
  .wpl-next{ right:calc(50% - 60px); transform:scaleX(-1); }
  .wpl-caption{ font-size:1.25rem; }
}
@media (prefers-reduced-motion: reduce){
  .wp-lightbox[open]{ animation:none; }
}

/* ============================================================
   Featured Work (incl. quote)
   ============================================================ */
.featured{
  background:url("assets/feature-work-bg.webp") center/cover no-repeat;
  padding:100px 6vw 130px;
  text-align:center;
  position:relative;
}
.featured .quote{
  padding:0 8vw 90px;
  text-align:center;
}
.featured .quote p{
  max-width:800px; margin:0 auto;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(1.1rem,2vw,1.45rem);
  color:var(--gold-300);
  line-height:1.6;
}
.featured-emblem{ display:flex; justify-content:center; margin-bottom:8px; }
.featured-emblem img{ width:180px; height:180px; object-fit:contain; }
.featured .section-title{ margin-bottom:60px; font-size:clamp(1.6rem,3vw,2.1rem); }
.featured-grid{
  max-width:920px;
  margin:0 auto;
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:28px 46px;
  text-align:left;
}
.featured-item{ position:relative; border-radius:2px; overflow:hidden; box-shadow:0 30px 50px rgba(0,0,0,0.4); }
.featured-item img{ width:100%; height:100%; object-fit:cover; }
.featured-item.large{ aspect-ratio:3/4; align-self:start; }
.featured-item.offset{ aspect-ratio:3/4; margin-top:95px; }
.featured-item:nth-child(3){ aspect-ratio:3/4; margin-top:-46px; }
.featured-item.offset-down{ aspect-ratio:3/4; }
.featured-item--embed{ display:flex; align-items:center; justify-content:center; background:#000; aspect-ratio:9/16; }
.featured-item:nth-child(3).featured-item--embed{ aspect-ratio:9/16; }
.featured-item--embed video{ width:100%; height:100%; object-fit:cover; }
.featured-item--embed iframe{ width:100% !important; }
.featured-label{
  position:absolute; left:0; right:0; bottom:-38px;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; font-size:1.25rem; color:var(--gold-100);
  text-align:center;
}

/* ============================================================
   Testimonials
   ============================================================ */
.testimonials{ background:var(--tan-bg); padding:120px 6vw; }
.testimonials-inner{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:0.85fr 1.15fr;
  gap:70px;
  align-items:center;
}
.testimonial-eyebrow{
  display:block;
  font-family:var(--font-sans);
  font-weight:600;
  font-size:0.95rem;
  letter-spacing:0.04em;
  color:var(--maroon-900);
  margin-bottom:14px;
}
.testimonial-heading{
  font-size:clamp(2rem,3.2vw,2.7rem);
  line-height:1.15;
  color:var(--maroon-900);
  margin-bottom:22px;
}
.testimonial-icon{
  display:inline-flex; align-items:center; justify-content:center;
  width:40px; height:40px;
  border-radius:8px;
  background:rgba(54,8,6,0.08);
  color:var(--maroon-800);
  margin-bottom:22px;
}
.testimonial-quote{ margin:0; }
.testimonial-quote p{
  font-family:var(--font-display);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(1.25rem,2.1vw,1.5rem);
  color:var(--maroon-900);
  line-height:1.65;
  max-width:42ch;
}
.testimonial-divider{
  display:block;
  width:60px; height:2px;
  background:var(--maroon-700);
  margin:26px 0 24px;
}
.testimonial-author{ display:flex; align-items:center; gap:14px; }
.testimonial-avatar{
  display:flex; align-items:center; justify-content:center;
  width:48px; height:48px;
  border-radius:50%;
  background:var(--maroon-800);
  color:var(--gold-200);
  font-family:var(--font-sans); font-weight:600; font-size:0.85rem;
  flex-shrink:0;
}
.testimonial-name{ font-family:var(--font-sans); font-weight:600; font-size:1.1rem; color:var(--maroon-900); }
.testimonial-role{ font-size:0.95rem; color:var(--maroon-900); }

.testimonial-gallery{
  display:grid;
  grid-template-columns:1.3fr 1fr;
  gap:22px;
}
.tg-item{ border-radius:6px; overflow:hidden; box-shadow:0 20px 40px rgba(28,17,9,0.18); }
.tg-item img{ width:100%; height:100%; object-fit:cover; }
.tg-large{ grid-column:1; grid-row:1; aspect-ratio:4/3.3; }
.tg-small-top{ grid-column:2; grid-row:1; margin-top:0; border-radius:0; }
.tg-tall{ grid-column:1; grid-row:2; aspect-ratio:4/3.6; }
.tg-col-bottom-right{ grid-column:2; grid-row:2; display:flex; flex-direction:column; justify-content:space-between; gap:22px; }
.tg-small-bottom{ border-radius:0; flex:1 1 auto; min-height:0; }
.tg-cta{ width:100%; border-radius:8px; flex-shrink:0; }
.btn-maroon{
  background:var(--maroon-texture-sm);
  color:var(--gold-100);
}
.btn-maroon:hover{ filter:brightness(1.18); }

/* ============================================================
   Clients
   ============================================================ */
.clients{ padding:100px 6vw 55px; text-align:center; overflow:hidden; }
.clients .section-title{ margin-bottom:8px; }
.clients-arc{
  position:relative;
  max-width:1160px;
  height:420px; /* replaced by layoutClientArc() above the mobile breakpoint */
  margin:0 auto;
  overflow:hidden;
}
.arc-lines{ position:absolute; inset:0; width:100%; height:100%; }
.client-list{ position:absolute; inset:0; }
.client-list li{
  position:absolute;
  width:104px; height:104px;
  border-radius:50%;
  overflow:hidden;
  background:var(--cream);
  box-shadow:0 10px 22px rgba(0,0,0,0.32);
  left:calc(50% + var(--x, 0px));
  top:var(--y, 0px);
  transform:translate(-50%,-50%);
}
/* The logo files are cream discs on a black square; cover + the circular
   mask crops the black corners away (contain would reveal them). */
.client-list li img{ width:100%; height:100%; object-fit:cover; }
.client-list li.inner{ width:96px; height:96px; }

/* ============================================================
   CTA
   ============================================================ */
.cta{
  padding:38px 6vw 45px;
  text-align:center;
}
.eyebrow.small-caps{
  font-family:var(--font-display);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; /* match every other Homes Royale heading */
  font-size:clamp(1.5rem,2.6vw,2.35rem);
  letter-spacing:0.22em;
  line-height:1.2;
  color:var(--gold-300);
  text-transform:uppercase;
  margin-bottom:20px;
}
.cta p{
  /* 31em sits between the width of "...through" (~30.2em) and "...through your"
     (~32.7em), so the sentence breaks after "through" at any font size. */
  max-width:31em; margin:0 auto;
  color:rgba(250,246,238,0.9);
  font-size:clamp(1.05rem,1.84vw,1.8rem);
}
.footer-top .btn{ padding:1.1em 2.8em; font-size:0.95rem; }

/* ============================================================
   Footer
   ============================================================ */
.site-footer{ padding:0 6vw 40px; }
.footer-top{
  max-width:var(--container);
  margin:0 auto;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:28px;
  padding:60px 0 40px;
}
.footer-emblem{ flex-shrink:0; }
.footer-address h3{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; color:var(--maroon-ink); font-size:1.4rem;
  line-height:1;
  margin-bottom:12px; position:relative; padding-bottom:18px;
}
.footer-address h3::after{
  content:""; position:absolute; left:50%; transform:translateX(-50%); bottom:0; width:40px; height:1px; background:var(--maroon-ink);
}
.footer-address p{ max-width:46ch; text-wrap:balance; color:rgba(250,246,238,0.8); font-size:0.95rem; }
.footer-rule{
  max-width:var(--container); margin:0 auto;
  height:1px; background:rgba(152,86,28,0.25);
}
.footer-bottom{
  max-width:var(--container); margin:0 auto;
  display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:16px;
  padding-top:26px;
  font-size:0.82rem; color:rgba(250,246,238,0.7);
}
.footer-bottom a{ margin-left:14px; color:rgba(250,246,238,0.7); }
.footer-bottom a:hover{ color:var(--gold-200); }

/* ============================================================
   Section backgrounds (assigning the texture)
   ============================================================ */
.about, .featured{
  background-color:var(--maroon-800);
}
.about, .process, .featured{
  position:relative;
}

/* Clients + CTA + Footer share one continuous background image
   instead of each section tiling its own texture (avoids seams). */
.bottom-band{
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
}
/* The CTA and footer sit under a maroon veil that flattens the busy texture
   (measured off the design: it cuts the texture's contrast by ~2/3) so the
   copy stays legible. It ramps in early enough to be at full strength by
   the time it reaches the foot of the client arcs (55px above this box),
   so the shade is already touching the arc line rather than starting there. */
.band-dim{ position:relative; }
.band-dim::before{
  content:"";
  position:absolute; left:0; right:0; top:-171px; bottom:0;
  background:linear-gradient(to bottom,
    rgba(74,13,10,0) 0,
    rgba(74,13,10,0.67) 116px,
    rgba(74,13,10,0.67) 100%);
  pointer-events:none;
}
.band-dim > *{ position:relative; }

/* ============================================================
   Responsive
   ============================================================ */
/* The 96px header gutters are sized for wide screens; below ~1000px they leave
   the CTA no room in its track and it rides up over the nav. */
@media (max-width:1000px){
  .logo{ margin-left:44px; }
  .header-cta{ margin-left:12px; margin-right:32px; }
  .main-nav{ gap:18px; }
}

@media (max-width:1080px){
  .about-inner, .process-inner{ grid-template-columns:1fr; }
  .process-intro{ position:static; }
  .about-image{ max-width:520px; margin:0 auto; }
  .featured-grid{ grid-template-columns:1fr 1fr; gap:24px 30px; }
  .testimonials-inner{ grid-template-columns:1fr; gap:50px; }
}

@media (max-width:860px){
  .main-nav{
    position:fixed; top:0; right:0; height:100vh; width:min(82vw,330px);
    background:var(--maroon-texture-sm);
    flex-direction:column; align-items:stretch;
    padding:96px 30px 40px; gap:22px;
    transform:translateX(100%);
    transition:transform .35s ease, visibility .35s;
    visibility:hidden;
    overflow-y:auto;
    z-index:120;
  }
  .main-nav.open{ transform:translateX(0); visibility:visible; }
  .nav-link{ font-size:1.05rem; padding-bottom:8px; }
  .nav-cta{ display:block; margin-top:14px; text-align:center; padding:0.85em 1.4em; font-size:0.9rem; }
  .nav-scrim{
    display:block;
    position:fixed; inset:0; z-index:110;
    background:rgba(0,0,0,0.55);
  }
  .nav-scrim[hidden]{ display:none; }
  body.nav-open{ overflow:hidden; }

  .menu-toggle{ display:flex; position:relative; z-index:130; }
  .menu-toggle span{ transition:transform .3s ease, opacity .2s ease; }
  .menu-toggle.open span:nth-child(1){ transform:translateY(6.5px) rotate(45deg); }
  .menu-toggle.open span:nth-child(2){ opacity:0; }
  .menu-toggle.open span:nth-child(3){ transform:translateY(-6.5px) rotate(-45deg); }

  .header-cta{ display:none; }
  .side-tab-stack{ display:none; }

  /* The 96px gutters are a desktop tweak; on mobile they shove the logo
     into the middle of the bar. */
  .site-header{ padding:8px 5vw; }
  .site-header.scrolled{ padding:6px 5vw; }
  /* The drawer nav is fixed and the CTA is hidden, so only the logo and the
     toggle are left in flow — a grid would strand the toggle mid-bar. */
  .header-inner{ display:flex; align-items:center; justify-content:space-between; }
  .logo{ margin-left:0; }
  .logo-img{ height:54px; }
  .header-cta{ margin-right:0; }
  /* 44px minimum touch target. */
  .menu-toggle{
    padding:12px; margin-right:-12px;
    min-width:44px; min-height:44px;
    align-items:center; justify-content:center;
  }

  /* Process: single column, images fill the width instead of overflowing. */
  .process{ padding:70px 6vw; }
  .timeline{ padding-left:26px; }
  .step-dot{ left:-26px; }
  .step-image-wrap{ width:100%; height:auto; aspect-ratio:486/254; }
  .step-body p{ max-width:none; }
  .timeline-step{ padding-bottom:40px; }

  /* Spinning badge hangs off the right edge at this width. */
  .about-badge{ right:auto; left:0; top:auto; bottom:-28px; width:96px; }
  .about-badge svg{ width:96px; height:96px; }

  .about{ padding-top:60px; padding-bottom:60px; }
  .spaces, .categories, .featured, .testimonials, .clients{ padding-top:60px; padding-bottom:60px; }
  .categories{ padding-top:88px; padding-bottom:88px; }

  .spaces-grid{ grid-template-columns:1fr; max-width:420px; }
  .categories-grid{ grid-template-columns:repeat(2,1fr); }
  .wallpaper-carousel{ padding:34px 0 44px; }
  .wp-card{ width:230px; margin-right:22px; }
  .featured-grid{ grid-template-columns:1fr; max-width:420px; margin:0 auto; }
  .featured-item.offset, .featured-item.offset-down, .featured-item:nth-child(3){ margin-top:0; }
  .featured-label{ position:static; margin-top:12px; }
  .testimonial-gallery{ grid-template-columns:1fr; max-width:420px; margin:0 auto; }
  .tg-large, .tg-small-top, .tg-tall, .tg-small-bottom, .tg-cta{ grid-column:1; margin-top:0; }

  .clients-arc{ height:auto; }
  .arc-lines{ display:none; }
  .client-list{ position:static; display:grid; grid-template-columns:repeat(3,1fr); gap:18px; justify-items:center; }
  .client-list li{ position:static; transform:none; width:84px; height:84px; }
  /* .inner logos are sized up for the desktop arc; in the grid that just
     makes the rows sit unevenly. */
  .client-list li.inner{ width:84px; height:84px; }

  .footer-bottom{ justify-content:center; text-align:center; }
}

@media (max-width:520px){
  .categories-grid{ grid-template-columns:repeat(2,1fr); gap:14px; }
  .wp-card{ width:200px; margin-right:18px; }
  .wp-card figcaption{ font-size:1rem; letter-spacing:1.5px; }
}

/* Reduced motion: stop the marquee, allow manual horizontal scroll */
@media (prefers-reduced-motion: reduce){
  .wp-track{ animation:none; }
  .wallpaper-carousel{ overflow-x:auto; scroll-snap-type:x proximity; }
  .wp-card{ scroll-snap-align:center; }
}

/* ============================================================
   Content scale — everything below the hero reads slightly
   smaller so sections don't feel zoomed in. Header and hero
   keep their full size.
   ============================================================ */
@media (min-width:861px){
  .about, .spaces, .process, .categories, .featured,
  .testimonials, .clients, .cta, .site-footer{
    zoom:0.88;
  }
}

/* ============================================================
   About Us page
   ============================================================ */
.ap-title-band{
  padding:170px 6vw 345px;
  text-align:center;
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
}
.ap-title-band h1{
  font-family:var(--font-display);
  font-size:clamp(2.6rem,7vw,4.8rem);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  color:var(--gold-400);
  text-shadow:0px 4px 5.5px rgba(0,0,0,0.95);
}

.ap-intro{
  background:var(--tan-bg);
  padding:1px 6vw 110px;
}
.ap-intro-image{
  max-width:534px;
  margin:-315px auto 60px;
  position:relative;
}
.ap-intro-image::after{
  content:"";
  position:absolute; right:-40px; bottom:-40px;
  width:100%; height:100%;
  border:1px solid rgba(152,86,28,0.55);
  z-index:0;
}
.ap-intro-image img{
  position:relative; z-index:1;
  width:100%; border-radius:2px;
  box-shadow:0 30px 60px rgba(0,0,0,0.45);
}
.ap-intro-text{ max-width:760px; margin:0 auto; text-align:center; }
.ap-intro-text p{
  font-size:var(--body-size); color:var(--maroon-900);
  margin-bottom:24px;
}

/* clip (not hidden) keeps from-right reveals, which start 42px to the right, from
   widening the page on phones before they play. */
.ap-vm{ background:var(--tan-bg); padding:40px 6vw 120px; overflow-x:clip; }
.ap-vm-inner{
  max-width:960px;
  margin:0 auto;
  display:grid;
  grid-template-columns:1fr 1fr;
  align-items:stretch;
}
.vm-card{
  overflow:hidden;
  padding:42px 40px 48px;
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
  box-shadow:0 24px 50px rgba(0,0,0,0.32);
  color:var(--cream);
}
.vm-vision{ border-radius:3px 0 0 3px; }
.vm-mission{
  border-radius:0 3px 3px 0;
  border-left:1px solid rgba(250,246,238,0.18);
}
.vm-card h2{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(1.9rem,3vw,2.5rem);
  color:var(--gold-300);
  margin-bottom:20px;
}
.vm-vision p{
  color:rgba(250,246,238,0.9);
  font-size:var(--body-size); line-height:1.7;
}
.vm-mission ul{ display:flex; flex-direction:column; gap:14px; }
.vm-mission li{
  position:relative; padding-left:22px;
  color:rgba(250,246,238,0.9);
  font-size:var(--body-size); line-height:1.5;
}
.vm-mission li::before{
  content:""; position:absolute; left:0; top:calc(0.75em - 3px); /* centred on the first line */
  width:6px; height:6px; border-radius:50%;
  background:var(--gold-300);
}

.ap-goals{
  padding:110px 6vw; text-align:center;
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
  overflow-x:clip; /* see .ap-vm */
}
.ap-goals .eyebrow{
  letter-spacing:0.18em; text-transform:uppercase; font-size:0.85rem;
}
.ap-goals h2{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(2.2rem,3.6vw,3rem);
  color:var(--gold-100);
  margin-bottom:56px;
}
.goal-list{
  max-width:var(--container); margin:0 auto;
  display:flex; flex-direction:column; gap:22px;
}
.goal-pill{
  display:inline-flex; align-self:flex-start;
  border:1px solid rgba(152,86,28,0.55);
  border-radius:999px;
  padding:0.9em 1.9em;
  color:var(--cream);
  font-size:0.98rem;
}
.goal-pill.right{ align-self:flex-end; }

.ap-community{
  position:relative;
  background:var(--tan-bg);
  padding:110px 6vw 100px; text-align:center;
}
/* Fine grain shared by every tan/yellow-background section. It sits behind
   the content, never over it: drawn on top, its 12% noise dulled the text
   and photos too. isolation gives each section its own stacking context, so
   z-index:-1 drops the grain under everything in it while staying above the
   section's own background. .pj-fact cells paint a solid tan of their own,
   so they carry the grain too. */
.spaces,
.categories,
.testimonials,
.ap-intro,
.ap-vm,
.ap-community,
.pf-page,
.pj-intro,
.pj-gallery-wrap,
.pj-fact{ position:relative; isolation:isolate; }
.spaces::before,
.categories::before,
.testimonials::before,
.ap-intro::before,
.ap-vm::before,
.ap-community::before,
.pf-page::before,
.pj-intro::before,
.pj-gallery-wrap::before,
.pj-fact::before{
  content:"";
  position:absolute; inset:0; z-index:-1;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.35' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  opacity:0.12;
  pointer-events:none;
}
.ap-community h2{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(2.2rem,3.6vw,3rem);
  color:var(--maroon-900);
  margin-bottom:24px;
}
.ap-community > p{
  max-width:760px; margin:0 auto 60px;
  color:var(--maroon-900); font-size:var(--body-size);
}
/* Static two-column collage — used on the home page. */
.community-grid{
  max-width:946px; margin:0 auto;
  display:grid; grid-template-columns:1fr 1fr;
  gap:24px; text-align:left;
}
.community-col{ display:flex; flex-direction:column; gap:24px; }
.c-item{
  border-radius:2px; overflow:hidden;
  background:var(--cream);
  box-shadow:0 16px 30px rgba(0,0,0,0.18);
}
.c-item img{ width:100%; height:100%; object-fit:cover; display:block; }
.c-item.c-small{ aspect-ratio:4/2.9; }
.c-item.c-square{ aspect-ratio:1/1; }
.c-item.c-tall1{ aspect-ratio:1/1; }
.c-item.c-tall2{ aspect-ratio:4/4.35; }
.c-label{
  background:var(--maroon-texture-sm);
  border-radius:60px;
  padding:26px 20px;
  text-align:center;
}
.c-label .c-label-title{
  display:block;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:1.6rem; color:var(--gold-300);
}
.c-label .c-label-sub{
  display:block; font-size:0.78rem; letter-spacing:0.14em;
  color:var(--gold-400); margin-top:4px;
}

/* --- auto-scrolling community carousel (about page) --- */
.community-carousel{
  --cc-gap:24px;
  position:relative;
  overflow:hidden;
  margin:0 -6vw;
  padding:10px 0 40px;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
}
.cc-track{
  display:flex;
  gap:var(--cc-gap);
  width:max-content;
  animation:cc-scroll 60s linear infinite;
}
.community-carousel:hover .cc-track,
.community-carousel:focus-within .cc-track{ animation-play-state:paused; }
@keyframes cc-scroll{
  from{ transform:translate3d(0,0,0); }
  to{ transform:translate3d(calc(-50% - (var(--cc-gap) / 2)),0,0); }
}
.cc-track .c-item{
  flex:0 0 auto;
  width:340px; height:250px;
}
@media (prefers-reduced-motion:reduce){
  .cc-track{ animation:none; }
  .community-carousel{ overflow-x:auto; }
}
/* Standalone caption under the carousel, rather than a cell in the collage. */
.ap-community > .c-label{
  display:inline-block;
  padding:22px 54px;
  margin-top:20px;
}

/* Wraps the about page's CTA + footer so they share one continuous backdrop. */
.ap-bottom-bg{
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
}

.ap-cta{ text-align:center; padding:110px 6vw; }
.ap-cta h2{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(2rem,4vw,3rem);
  color:var(--gold-400);
  text-shadow:0px 4px 5.5px rgba(0,0,0,0.95);
  margin-bottom:20px;
}
.ap-cta p{
  max-width:640px; margin:0 auto;
  color:rgba(250,246,238,0.9); font-size:var(--body-size);
}

@media (max-width:860px){
  .ap-title-band{ padding:110px 6vw 250px; }
  .ap-intro-image{ margin-top:-215px; }
  /* the offset frame is 40px out, wider than the 6vw side padding on phones */
  .ap-intro-image::after{ right:-14px; bottom:-14px; }
  .ap-goals{ padding:70px 6vw; }
  .ap-community{ padding:70px 6vw 60px; }
  .ap-cta{ padding:70px 6vw; }
  .ap-vm{ padding:40px 6vw 70px; }
  .ap-vm-inner{ grid-template-columns:1fr; gap:24px; max-width:460px; }
  .vm-vision{ border-radius:3px; }
  .vm-mission{ border-radius:3px; border-left:none; }
  .community-grid{ grid-template-columns:1fr; max-width:420px; margin:0 auto; }
  .community-carousel{ --cc-gap:16px; }
  .cc-track .c-item{ width:250px; height:185px; }
  .ap-community > .c-label{ padding:18px 36px; }
  .goal-pill, .goal-pill.right{ align-self:stretch; text-align:center; }
}

/* ============================================================
   Portfolio page
   ============================================================ */
.visually-hidden{
  position:absolute; width:1px; height:1px;
  margin:-1px; padding:0; border:0;
  clip:rect(0 0 0 0); clip-path:inset(50%);
  overflow:hidden; white-space:nowrap;
}

.pf-page{
  background:var(--tan-bg);
  padding:150px 6vw 90px;
}
.pf-head{
  max-width:var(--container);
  margin:0 auto 46px;
  text-align:center;
}
.pf-head .eyebrow{ color:var(--maroon-900); margin-bottom:10px; }
.pf-head h1{
  font-family:var(--font-display);
  font-size:clamp(2.4rem,6vw,4rem);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  color:var(--maroon-900);
}

.pf-list{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  gap:70px;
}

.pf-card{
  position:relative;
  box-shadow:0 26px 60px rgba(0,0,0,0.32);
}
/* The whole card is the link; the anchor is stretched over it so the images
   and the caption band are all clickable without nesting them inside an <a>. */
.pf-link{ position:absolute; inset:0; z-index:3; }
.pf-card:focus-within{ outline:2px solid var(--gold-100); outline-offset:6px; }

/* Measured off the design: a tall hero on the left at ~49% of the card, and a
   2x2 grid of detail frames filling the rest. */
.pf-media{
  display:grid;
  grid-template-columns:1.95fr 1fr 1fr;
  grid-template-rows:1fr 1fr;
  gap:6px 14px;
  aspect-ratio:1240/487;
}
.pf-media img{
  width:100%; height:100%;
  object-fit:cover;
  display:block;
  /* Grid items default to min-height:auto, so without this the rows stretch to
     each photo's intrinsic height and the card's aspect ratio is ignored. */
  min-width:0; min-height:0;
}
.pf-hero{ grid-row:1 / span 2; }
/* Short sets still have to fill the same frame, so the last thumbnails widen
   out instead of leaving holes in the grid. */
.pf-media[data-thumbs="3"] img:last-child{ grid-column:span 2; }
.pf-media[data-thumbs="2"] img:not(.pf-hero){ grid-column:2 / span 2; }
.pf-media[data-thumbs="1"] img:not(.pf-hero){ grid-column:2 / span 2; grid-row:1 / span 2; }

.pf-band{ overflow:hidden; }
.pf-band-inner{
  position:relative; z-index:1;
  padding:26px 34px 30px;
}
.pf-title{
  font-family:var(--font-display);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:clamp(1.5rem,2.6vw,2.1rem);
  color:var(--gold-200);
  margin-bottom:8px;
}
.pf-meta{
  font-size:0.76rem;
  color:rgba(250,246,238,0.72);
  margin-bottom:4px;
}
.pf-desc{
  max-width:760px;
  font-size:0.95rem; /* card copy, kept smaller than --body-size */
  color:rgba(250,246,238,0.88);
  margin:10px 0 0;
}
.pf-cta{
  display:inline-block;
  margin-top:16px;
  font-size:0.82rem; font-weight:600; letter-spacing:0.08em; text-transform:uppercase;
  color:var(--gold-300);
  border-bottom:1px solid rgba(152,86,28,0.5);
  padding-bottom:3px;
}
.pf-card:hover .pf-cta{ color:var(--gold-100); border-bottom-color:var(--gold-100); }

/* ============================================================
   Project detail page
   ============================================================ */
.pj-title-band{
  padding:170px 6vw 300px;
  text-align:center;
  background:var(--maroon-800) url("assets/bottom-bg.webp") center/cover no-repeat;
}
.pj-title-band .eyebrow{
  font-size:0.8rem; letter-spacing:0.16em; text-transform:uppercase;
  margin-bottom:14px;
}
.pj-title-band h1{
  font-family:var(--font-display);
  font-size:clamp(2.4rem,6.5vw,4.4rem);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  color:var(--gold-400);
  text-shadow:0px 4px 5.5px rgba(0,0,0,0.95);
}
.pj-place{
  margin-top:12px;
  font-size:1rem; letter-spacing:0.18em; text-transform:uppercase;
  color:rgba(250,246,238,0.75);
}

.pj-intro{
  background:var(--tan-bg);
  padding:1px 6vw 70px;
}
.pj-hero{
  max-width:1040px;
  /* Pulls up over the title band, the way the About page intro image does. */
  margin:-220px auto 54px;
  position:relative;
  text-align:center;
}
/* The deck's heroes run anywhere from 16:9 to full portrait, so the cap is on
   height rather than width — a portrait frame scaled to the column width was
   rendering three screens tall. Capping height instead keeps every hero within
   one screen without cropping any of them. */
.pj-hero img{
  max-width:100%; max-height:66vh;
  width:auto; height:auto;
  display:inline-block; vertical-align:top;
  border-radius:2px;
  box-shadow:0 30px 60px rgba(0,0,0,0.45);
}
.pj-summary{ max-width:760px; margin:0 auto 44px; text-align:center; }
.pj-summary p{ font-size:var(--body-size); color:var(--maroon-900); }

.pj-facts{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
  gap:1px;
  background:rgba(28,17,9,0.18);
  border:1px solid rgba(28,17,9,0.18);
}
.pj-fact{ background:var(--tan-bg); padding:22px 24px; }
.pj-fact dt{
  font-size:0.72rem; letter-spacing:0.16em; text-transform:uppercase;
  color:var(--maroon-900);
  margin-bottom:8px;
}
.pj-fact dd{
  margin:0;
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  font-size:1.2rem;
  color:var(--maroon-900);
}

.pj-gallery-wrap{ background:var(--tan-bg); padding:20px 6vw 110px; }
.pj-gallery-title{
  max-width:var(--container);
  margin:0 auto 30px;
  font-family:var(--font-display);
  font-size:clamp(1.8rem,3.4vw,2.6rem);
  font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor;
  color:var(--maroon-900);
  text-align:center;
}
.pj-gallery{
  max-width:var(--container);
  margin:0 auto;
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:18px;
}
.pj-gallery figure{
  margin:0; overflow:hidden;
  box-shadow:0 18px 40px rgba(0,0,0,0.28);
}
.pj-gallery img{
  width:100%; height:100%; display:block;
  object-fit:cover; aspect-ratio:4/3;
  transition:transform .6s ease;
}
.pj-gallery figure:hover img{ transform:scale(1.04); }
/* Every fifth frame runs full width so a long room-by-room set keeps a rhythm. */
.pj-wide{ grid-column:1 / -1; }
.pj-wide img{ aspect-ratio:21/9; }

.pj-nav{
  max-width:var(--container);
  margin:70px auto 0;
  display:grid;
  grid-template-columns:1fr auto 1fr;
  gap:24px;
  align-items:center;
  border-top:1px solid rgba(28,17,9,0.2);
  padding-top:30px;
}
.pj-nav a{ color:var(--maroon-900); }
.pj-nav span{
  display:block;
  font-size:0.72rem; letter-spacing:0.16em; text-transform:uppercase;
  color:var(--maroon-900);
  margin-bottom:6px;
}
.pj-nav strong{
  font-family:var(--font-display); font-weight:500; -webkit-text-stroke:var(--display-stroke) currentColor; font-size:1.15rem;
}
.pj-nav-next{ text-align:right; }
.pj-nav-all{
  font-size:0.8rem; letter-spacing:0.12em; text-transform:uppercase;
  border:1px solid rgba(28,17,9,0.35);
  padding:12px 22px;
  white-space:nowrap;
}
.pj-nav-all:hover{ background:var(--maroon-800); color:var(--gold-200); border-color:var(--maroon-800); }
.pj-nav a:hover strong{ color:var(--maroon-600); }

@media (max-width:860px){
  .pf-page{ padding:110px 5vw 60px; }
  .pf-list{ gap:44px; }
  /* The 2x2 detail grid collapses to a single band of thumbs under the hero;
     at this width the individual frames are too small to read otherwise. */
  .pf-media{
    grid-template-columns:1fr 1fr;
    grid-template-rows:auto auto;
    aspect-ratio:auto;
    gap:6px;
  }
  .pf-hero{ grid-column:1 / -1; grid-row:auto; aspect-ratio:16/10; }
  .pf-media img{ aspect-ratio:4/3; }
  .pf-media[data-thumbs="3"] img:last-child,
  .pf-media[data-thumbs="2"] img:not(.pf-hero),
  .pf-media[data-thumbs="1"] img:not(.pf-hero){ grid-column:1 / -1; grid-row:auto; }
  .pf-band-inner{ padding:20px 22px 24px; }

  .pj-title-band{ padding:110px 5vw 220px; }
  .pj-intro{ padding-bottom:50px; }
  .pj-hero{ margin-top:-180px; }
  /* Portrait phones have the height to spare and little width, so the hero can
     take more of the screen here than it does on a desktop. */
  .pj-hero img{ max-height:76vh; }
  .pj-gallery{ grid-template-columns:1fr; gap:14px; }
  .pj-wide img{ aspect-ratio:4/3; }
  .pj-nav{
    grid-template-columns:1fr;
    text-align:center;
    gap:20px;
  }
  .pj-nav-next{ text-align:center; }
  .pj-nav-all{ justify-self:center; }
}

/* ============================================================
   ============================================================
   MOTION
   ------------------------------------------------------------
   Everything below is the site's animation layer. It is applied
   entirely from script.js, which stamps `data-anim="<variant>"`
   onto elements from a selector map and adds `.in` when they
   scroll into view. Nothing here depends on the markup, which is
   what lets portfolio.html and the 15 generated project pages
   animate without build-portfolio.py ever being touched.

   Two consequences worth knowing:
     * With JS off (or before it runs) no element is ever hidden —
       the start states only exist while [data-anim] is present.
     * script.js strips the attribute once a reveal has finished,
       so these rules never linger to fight a :hover transform.
   ============================================================
   ============================================================ */

/* ------------------------------------------------------------
   Reveal variants
   ------------------------------------------------------------ */

/* Shared timing. --d is the per-element stagger written inline by JS. */
[data-anim]{
  transition-duration:var(--dur-mid);
  transition-timing-function:var(--ease-out-expo);
  transition-delay:var(--d, 0ms);
}

/* fade-up — the default. */
[data-anim="fade-up"]{ transition-property:opacity, transform; }
[data-anim="fade-up"]:not(.in){ opacity:0; transform:translateY(28px); }

/* fade — for large photographs that shouldn't travel. */
[data-anim="fade"]{ transition-property:opacity; transition-duration:var(--dur-slow); }
[data-anim="fade"]:not(.in){ opacity:0; }

/* settle — the house move for photography, and the only one the images use.
   The frame never travels and nothing is drawn across it: the wrapper fades
   up while the photograph inside relaxes out of a slight over-scale, so the
   image *arrives* rather than slides. The scale runs on the longer --dur-veil
   so it trails the fade slightly and is still moving, barely, after the
   opacity has landed — that lag is the whole effect.

   Deliberately no `overflow:hidden` here. Every grid wrapper this lands on
   (.space-card, .featured-item, .tg-item, .c-item, .pj-gallery figure,
   .step-image-wrap) already clips its own overflow, and adding it at the
   variant level would crop .about-badge, which hangs off .about-image at
   right:-60px.

   --py is the parallax offset (see .is-parallax below). Composing the two
   here rather than letting JS write `transform` directly is what stops the
   scroll loop from stamping over the settle. */
[data-anim="settle"]{
  transition-property:opacity;
  transition-duration:var(--dur-slow);
}
[data-anim="settle"]:not(.in){ opacity:0; }
[data-anim="settle"] > img,
[data-anim="settle"] > video,
[data-anim="settle"] > a > img{
  transition:transform var(--dur-veil) var(--ease-out-expo);
  transition-delay:var(--d, 0ms);
}
[data-anim="settle"]:not(.in) > img,
[data-anim="settle"]:not(.in) > video,
[data-anim="settle"]:not(.in) > a > img{
  transform:translate3d(0,var(--py, 0px),0) scale(1.06);
}

/* Parallax targets. The scroll loop only ever writes --py, so any transform
   an element already carries — a reveal, a hover — survives untouched. */
.is-parallax{ transform:translate3d(0,var(--py, 0px),0); }

/* mask-up — a display heading rising from behind its own bottom edge.
   The mask is `overflow:hidden` on the heading with the text moved into a
   .mask-inner block, NOT a clip-path on the heading itself: clip-path shrinks
   the box IntersectionObserver measures, so a heading clipped to zero height
   reported an intersection ratio of 0 and never crossed the threshold that
   would have revealed it. Clipping children leaves the observed box intact.
   The overflow (and any text-shadow it costs) goes away with the attribute
   as soon as the reveal finishes. */
[data-anim="mask-up"]{
  overflow:hidden;
  transition-property:opacity;
  transition-duration:var(--dur-slow);
}
.mask-inner{ display:block; }
[data-anim="mask-up"] .mask-inner{
  transition:transform var(--dur-slow) var(--ease-out-expo),
             opacity var(--dur-slow) var(--ease-out-soft);
  transition-delay:var(--d, 0ms);
}
[data-anim="mask-up"]:not(.in) .mask-inner{ transform:translateY(105%); opacity:0; }

/* The gold curtain-wipe that used to live here as [data-anim="veil"], and the
   travelling [data-anim="rise"] above it, are both gone — every image now uses
   the still `settle` above instead. --dur-veil survives as the timing token
   for settle's trailing scale. */

/* draw — a hairline rule unrolling from its left edge. */
[data-anim="draw"]{
  transition-property:transform, opacity;
  transition-duration:var(--dur-slow);
  transform-origin:left center;
}
[data-anim="draw"]:not(.in){ transform:scaleX(0); opacity:0; }

/* pop — for round things: client discs, the avatar, emblems. */
[data-anim="pop"]{
  transition-property:opacity, transform;
  transition-duration:var(--dur-mid);
}
[data-anim="pop"]:not(.in){ opacity:0; transform:scale(.86); }

/* from-left / from-right — paired panels arriving from opposite sides. */
[data-anim="from-left"],
[data-anim="from-right"]{
  transition-property:opacity, transform;
  transition-duration:var(--dur-slow);
}
[data-anim="from-left"]:not(.in){ opacity:0; transform:translateX(-42px); }
[data-anim="from-right"]:not(.in){ opacity:0; transform:translateX(42px); }

/* ------------------------------------------------------------
   Global chrome
   ------------------------------------------------------------ */

/* Header retracts on the way down and comes straight back on the way up, so
   the nav is never more than a flick away without sitting over the imagery. */
.site-header{
  transition:background var(--dur-fast) var(--ease-out-soft),
             padding var(--dur-fast) var(--ease-out-soft),
             transform .5s var(--ease-out-expo);
}
/* Deliberately no `will-change:transform` here: promoting the header would
   make it the containing block for the position:fixed mobile drawer and
   scrim nested inside it, and both would start positioning against the bar
   instead of the viewport. */
.site-header.retracted{ transform:translateY(-105%); }
.logo-img{ transition:height var(--dur-fast) var(--ease-out-soft); }
.site-header.scrolled .logo-img{ height:66px; }

/* Scroll progress — a 2px gold hairline pinned to the very top of the
   viewport. Created by script.js, so it simply doesn't exist without JS. */
.scroll-progress{
  position:fixed; top:0; left:0; right:0;
  height:2px;
  z-index:130;
  pointer-events:none;
  background:linear-gradient(90deg, var(--gold-600), var(--gold-300) 55%, var(--gold-100));
  transform:scaleX(0);
  transform-origin:left center;
  opacity:0;
  transition:opacity var(--dur-fast) ease;
}
.scroll-progress.visible{ opacity:.85; }

/* Gold sheen sweeping across a button on hover. */
.btn-gold::before,
.btn-maroon::before{
  content:"";
  position:absolute; top:0; bottom:0; left:-140%;
  width:60%;
  z-index:-1;
  background:linear-gradient(100deg, transparent, rgba(255,255,255,0.55), transparent);
  transform:skewX(-18deg);
  transition:left .75s var(--ease-out-soft);
}
.btn-maroon::before{ background:linear-gradient(100deg, transparent, rgba(152,86,28,0.28), transparent); }
.btn-gold:hover::before,
.btn-gold:focus-visible::before,
.btn-maroon:hover::before,
.btn-maroon:focus-visible::before{ left:140%; }
/* Fired briefly by script.js while the enquiry hands off to the mail client. */
.btn.is-sending::before{ animation:btn-sheen 1.1s var(--ease-out-soft) 2; }
@keyframes btn-sheen{
  from{ left:-140%; }
  to{ left:140%; }
}

/* Nav underline grows from the middle rather than the left — reads as
   deliberate rather than as a loading bar. */
.nav-link::after{
  left:50%;
  transform:translateX(-50%);
  transition:width var(--dur-fast) var(--ease-out-expo);
}

/* The side tab and its WhatsApp block slide in once the page has settled,
   then the disc breathes a slow ring so it stays noticeable without nagging. */
.side-tab-stack{
  animation:side-tab-in .9s var(--ease-out-expo) 1.2s both;
}
@keyframes side-tab-in{
  from{ transform:translate(100%,-50%); opacity:0; }
  to{ transform:translate(0,-50%); opacity:1; }
}
.side-tab-whatsapp{ position:relative; overflow:hidden; }
.side-tab-whatsapp::before{
  content:"";
  position:absolute; inset:0;
  background:radial-gradient(circle at 50% 50%, rgba(255,255,255,0.55), transparent 62%);
  opacity:0;
  animation:wa-pulse 6s var(--ease-out-soft) 3s infinite;
}
.side-tab-stack:hover .side-tab-whatsapp::before{ animation-play-state:paused; }
@keyframes wa-pulse{
  0%, 72%, 100%{ opacity:0; transform:scale(.4); }
  80%{ opacity:.5; transform:scale(1); }
  92%{ opacity:0; transform:scale(1.5); }
}
.side-tab-whatsapp svg{ transition:transform var(--dur-fast) var(--ease-out-expo); }
.side-tab-whatsapp:hover svg{ transform:scale(1.15); }
.side-tab{ transition:background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); }
.side-tab:hover{ background:var(--gold-200); }


/* Line masks are built by script.js; without it the headline just sits there. */
.hl-line{ display:block; overflow:hidden; padding-bottom:0.08em; }
.hl-line > span{
  display:block;
  animation:hl-rise 1.4s var(--ease-out-expo) both;
  animation-delay:var(--d, 0ms);
}
@keyframes hl-rise{
  from{ transform:translateY(105%); opacity:0; }
  to{ transform:translateY(0); opacity:1; }
}

/* ------------------------------------------------------------
   Home — about, spaces, process
   ------------------------------------------------------------ */
.about-badge{
  /* Fades and scales in first; the spin lives on the svg below so the two
     transforms don't fight over the same element (they did, silently). */
  animation:badge-in 1s var(--ease-out-expo) .5s both;
}
.about-badge svg{
  animation:spin 18s linear 1.5s infinite;
}
@keyframes badge-in{
  from{ opacity:0; transform:scale(.7); }
  to{ opacity:1; transform:scale(1); }
}

/* Tab panels cross-fade instead of snapping between hidden states. */
.spaces-grid{ transition:opacity var(--dur-fast) var(--ease-out-soft); }
.spaces-grid.swapping-out{ opacity:0; }
.tab{ position:relative; isolation:isolate; }
.tab::after{
  content:"";
  position:absolute; inset:0;
  border-radius:999px;
  background:var(--maroon-700);
  transform:scale(.7);
  opacity:0;
  z-index:-1;
  transition:transform var(--dur-fast) var(--ease-out-expo), opacity var(--dur-fast) ease;
}
.tab.active::after{ transform:scale(1); opacity:1; }
.tab.active{ background:transparent; }
.tab:not(.active):hover{ color:#fff; }

.space-card{ transition:transform var(--dur-mid) var(--ease-out-expo), box-shadow var(--dur-mid) var(--ease-out-soft); }
.space-card:hover{ transform:translateY(-6px); box-shadow:0 30px 56px rgba(0,0,0,0.34); }
/* Lift the caption text, never the figcaption itself — that box is the gradient
   scrim, so moving it uncovers a strip of image along the card's bottom edge. */
.space-card .cap-title,
.space-card .cap-sub{ transition:transform var(--dur-mid) var(--ease-out-expo); }
.space-card:hover .cap-title,
.space-card:hover .cap-sub{ transform:translateY(-4px); }

/* The timeline rule draws itself as the section scrolls past; --tl is written
   by script.js. It defaults to 1 so the line is simply there without JS. */
.timeline::before{
  transform:scaleY(var(--tl, 1));
  transform-origin:top center;
}
.step-dot{
  transition:transform var(--dur-mid) var(--ease-out-expo), box-shadow var(--dur-mid) var(--ease-out-soft);
}
.timeline-step[data-anim]:not(.in) .step-dot{ transform:scale(0); box-shadow:0 0 0 0 rgba(152,86,28,0); }
.timeline-step.in .step-dot{ box-shadow:0 0 0 6px rgba(152,86,28,0.18); }

/* ------------------------------------------------------------
   Home — wallpapers, featured, testimonials, clients
   ------------------------------------------------------------ */
.wp-card{ transition:transform var(--dur-mid) var(--ease-out-expo), box-shadow var(--dur-mid) var(--ease-out-soft); }
.wp-card:hover{ transform:translateY(-8px); box-shadow:0 26px 44px rgba(0,0,0,0.3); }
.wp-card figcaption{ transition:transform var(--dur-mid) var(--ease-out-expo); }
.wp-card:hover figcaption{ transform:translateY(-6px); }

/* Word-by-word on the pull quote — the one place the extra spans earn their
   keep, because the sentence is the section's whole argument. */
.qw{ display:inline-block; }
.qw > span{
  display:inline-block;
  transition:opacity var(--dur-slow) var(--ease-out-soft), transform var(--dur-slow) var(--ease-out-expo);
  transition-delay:var(--d, 0ms);
}
.quote p[data-anim]:not(.in) .qw > span{ opacity:0; transform:translateY(16px); }

.featured-emblem img{ animation:emblem-float 9s ease-in-out infinite; }
@keyframes emblem-float{
  0%, 100%{ transform:translateY(-5px); }
  50%{ transform:translateY(5px); }
}
.featured-item{ transition:box-shadow var(--dur-mid) var(--ease-out-soft); }
.featured-item:hover{ box-shadow:0 38px 64px rgba(0,0,0,0.5); }

/* ------------------------------------------------------------
   Featured reels
   ------------------------------------------------------------
   Everything below only applies once script.js has added .reel and taken
   the video over; until then the browser's own controls are showing and
   none of these selectors match.
   ------------------------------------------------------------ */
.reel .reel-surface,
.reel .reel-sound{
  font:inherit; color:inherit;
  border:0; padding:0; margin:0;
  background:transparent;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* The whole frame is the play/pause target; the sound button sits above it. */
.reel-surface{ position:absolute; inset:0; z-index:2; display:block; }
.reel-sound{ position:absolute; right:12px; bottom:12px; z-index:3; width:36px; height:36px; }

/* A reel that has never started, or that the browser declined to start,
   says so with a centred glyph. Once it is running the glyph is only the
   pause affordance, and only while the pointer is over the frame. */
.reel-glyph{
  position:absolute; top:50%; left:50%;
  width:64px; height:64px; margin:-32px 0 0 -32px;
  border-radius:50%;
  background:rgba(12,6,3,0.42);
  border:1px solid rgba(152,86,28,0.5);
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
  opacity:0;
  transform:scale(.82);
  transition:opacity var(--dur-fast) var(--ease-out-soft),
             transform var(--dur-fast) var(--ease-out-expo);
}
/* The triangle / the two bars, drawn from the one box. */
.reel-glyph::before{
  content:"";
  position:absolute; top:50%; left:50%;
  transform:translate(-40%,-50%);
  border-style:solid;
  border-width:9px 0 9px 15px;
  border-color:transparent transparent transparent var(--gold-100);
  transition:opacity .18s ease;
}
.reel-glyph::after{
  content:"";
  position:absolute; top:50%; left:50%;
  width:13px; height:17px;
  transform:translate(-50%,-50%);
  border-left:4px solid var(--gold-100);
  border-right:4px solid var(--gold-100);
  opacity:0;
  transition:opacity .18s ease;
}
.reel.is-playing .reel-glyph::before{ opacity:0; }
.reel.is-playing .reel-glyph::after{ opacity:1; }

/* Idle or blocked: the glyph is the invitation, so it stays put. */
.reel:not(.is-playing) .reel-glyph,
.reel.is-blocked .reel-glyph{ opacity:1; transform:scale(1); }
/* Playing: it is only the pause control, so it waits to be asked for. */
@media (hover:hover){
  .reel.is-playing:hover .reel-glyph{ opacity:1; transform:scale(1); }
}
.reel .reel-surface:focus-visible .reel-glyph{ opacity:1; transform:scale(1); }

/* The sound disc carries the playback ring around it. Faint until the
   reel is hovered or actually audible — it should read as available,
   not as chrome bolted onto the design. */
.reel-sound{
  border-radius:50%;
  background:rgba(12,6,3,0.5);
  backdrop-filter:blur(4px);
  -webkit-backdrop-filter:blur(4px);
  opacity:0.55;
  transform:scale(.94);
  transition:opacity var(--dur-fast) var(--ease-out-soft),
             transform var(--dur-fast) var(--ease-out-expo),
             background var(--dur-fast) ease;
}
@media (hover:hover){
  .reel:hover .reel-sound{ opacity:1; transform:scale(1); }
  .reel-sound:hover{ background:rgba(12,6,3,0.72); }
}
.reel.is-live .reel-sound{ opacity:1; transform:scale(1); }
.reel-sound:focus-visible{ opacity:1; transform:scale(1); outline:2px solid var(--gold-300); outline-offset:3px; }

.reel-sound svg{ position:absolute; inset:0; width:100%; height:100%; }
/* The ring starts at twelve o'clock and runs clockwise. */
.reel-sound > svg:first-child{ transform:rotate(-90deg); }
.reel-ring-track{ fill:none; stroke:rgba(152,86,28,0.2); stroke-width:2; }
.reel-ring{
  fill:none;
  stroke:var(--gold-300);
  stroke-width:2;
  stroke-linecap:round;
  /* dasharray/offset are written by script.js each frame — no transition
     here, or the ring would lag its own animation. */
}
.reel-sound-icon{
  fill:none;
  stroke:var(--gold-100);
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
  padding:9px;
  box-sizing:border-box;
}

/* A slow push-in on hover, matching the .pf-card / .space-card vocabulary.
   Scoped to :not([data-anim]) — that attribute is only absent once the
   settle reveal has finished and retired itself — because both the
   transform and the transition here would otherwise tie on specificity
   with settle's own rules above and, sitting later in the file, quietly
   win: the reel would lose its --py offset and its --dur-veil lag. */
@media (hover:hover){
  .reel:not([data-anim]) video{
    transition:transform var(--dur-slow) var(--ease-out-expo);
  }
  .reel:not([data-anim]):hover video{ transform:scale(1.03); }
}

@media (max-width:860px){
  /* Touch has no hover, so both controls stay legible at all times. */
  .reel-sound{ opacity:1; transform:scale(1); right:10px; bottom:10px; }
  .reel-glyph{ width:56px; height:56px; margin:-28px 0 0 -28px; }
}

.testimonial-avatar{ transition:transform var(--dur-mid) var(--ease-out-expo); }
.testimonial-author:hover .testimonial-avatar{ transform:scale(1.08); }
.tg-item{ transition:transform var(--dur-mid) var(--ease-out-expo), box-shadow var(--dur-mid) var(--ease-out-soft); }
.tg-item:hover{ transform:translateY(-5px); box-shadow:0 28px 50px rgba(28,17,9,0.26); }

/* The three guide arcs draw themselves in. pathLength is normalised to 1 by
   script.js so the dash maths survives every resize. */
.arc-lines circle{
  stroke-dasharray:1;
  stroke-dashoffset:0;
  transition:stroke-dashoffset 1.8s var(--ease-out-soft);
  transition-delay:var(--d, 0ms);
}
.clients-arc[data-arc]:not(.in) .arc-lines circle{ stroke-dashoffset:1; }
.client-list li{
  transition:transform var(--dur-mid) var(--ease-out-expo),
             box-shadow var(--dur-mid) var(--ease-out-soft),
             opacity var(--dur-mid) var(--ease-out-soft);
}
.client-list li:hover{
  box-shadow:0 14px 30px rgba(0,0,0,0.42), 0 0 0 3px rgba(152,86,28,0.55);
}
/* The discs are absolutely positioned by a translate, so their reveal has to
   compose with it rather than replace it — hence the custom properties. */
.client-list li[data-anim="disc"]{
  transform:translate(-50%,-50%) scale(var(--s, 1));
  opacity:var(--o, 1);
}
.client-list li[data-anim="disc"]:not(.in){ --s:.7; --o:0; }
.client-list li[data-anim="disc"]:hover{ --s:1.08; }
@media (max-width:860px){
  /* In the mobile grid the discs are static, so the centring translate is gone. */
  .client-list li[data-anim="disc"]{ transform:scale(var(--s, 1)); }
}

.cta .eyebrow.small-caps{ transition:letter-spacing 1.4s var(--ease-out-expo), opacity var(--dur-slow) ease, transform var(--dur-slow) var(--ease-out-expo); }
.cta .eyebrow.small-caps[data-anim]:not(.in){ letter-spacing:0.42em; }

/* ------------------------------------------------------------
   About page
   ------------------------------------------------------------ */
.ap-intro-image::after{
  transition:transform var(--dur-slow) var(--ease-out-expo), opacity var(--dur-slow) ease;
  transition-delay:calc(var(--d, 0ms) + 260ms);
  transform-origin:left top;
}
.ap-intro-image[data-anim]:not(.in)::after{ transform:translate(-30px,-30px); opacity:0; }

.vm-card{ transition:box-shadow var(--dur-mid) var(--ease-out-soft); }
.vm-card:hover{ box-shadow:0 32px 64px rgba(0,0,0,0.4); }
.vm-mission li::before{ transition:transform var(--dur-mid) var(--ease-out-expo); }
.vm-mission[data-anim]:not(.in) li::before{ transform:scale(0); }

.goal-pill{
  transition:border-color var(--dur-fast) ease,
             background var(--dur-fast) ease,
             color var(--dur-fast) ease,
             transform var(--dur-slow) var(--ease-out-expo),
             opacity var(--dur-slow) ease;
}
.goal-pill:hover{ border-color:var(--gold-300); background:rgba(152,86,28,0.12); }


/* ------------------------------------------------------------
   Portfolio page
   ------------------------------------------------------------ */
.pf-card{ transition:transform var(--dur-mid) var(--ease-out-expo), box-shadow var(--dur-mid) var(--ease-out-soft); }
.pf-card:hover{ transform:translateY(-6px); box-shadow:0 38px 76px rgba(0,0,0,0.38); }
.pf-media{ overflow:hidden; }
.pf-media img{ transition:transform 1s var(--ease-out-expo); }
.pf-card:hover .pf-hero{ transform:scale(1.05); }
/* Thumbnails lift in sequence behind the hero's zoom. */
.pf-card:hover .pf-media img:not(.pf-hero){ transform:translateY(-5px); }
.pf-card:hover .pf-media img:nth-of-type(3){ transition-delay:60ms; }
.pf-card:hover .pf-media img:nth-of-type(4){ transition-delay:120ms; }
.pf-card:hover .pf-media img:nth-of-type(5){ transition-delay:180ms; }
.pf-band-inner{ transition:transform var(--dur-mid) var(--ease-out-expo); }
.pf-card:hover .pf-band-inner{ transform:translateY(-4px); }
.pf-cta{ transition:color var(--dur-fast) ease, border-bottom-color var(--dur-fast) ease; }
.pf-cta::after{
  content:"\2192";
  display:inline-block;
  margin-left:0;
  max-width:0;
  opacity:0;
  overflow:hidden;
  vertical-align:bottom;
  transition:max-width var(--dur-mid) var(--ease-out-expo),
             margin-left var(--dur-mid) var(--ease-out-expo),
             opacity var(--dur-fast) ease;
}
.pf-card:hover .pf-cta::after{ max-width:1.4em; margin-left:.5em; opacity:1; }

/* ------------------------------------------------------------
   Project detail pages
   ------------------------------------------------------------ */
.pj-hero img{ will-change:transform; }
.pj-gallery figure{ transition:box-shadow var(--dur-mid) var(--ease-out-soft); }
.pj-gallery figure:hover{ box-shadow:0 26px 54px rgba(0,0,0,0.36); }
.pj-nav strong{
  display:inline-block;
  transition:transform var(--dur-mid) var(--ease-out-expo), color var(--dur-fast) ease;
}
.pj-nav-prev:hover strong{ transform:translateX(-6px); }
.pj-nav-next:hover strong{ transform:translateX(6px); }
.pj-nav-all{ transition:background var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out-expo); }
.pj-nav-all:hover{ transform:translateY(-2px); }

/* ------------------------------------------------------------
   Mobile drawer
   ------------------------------------------------------------ */
@media (max-width:860px){
  .main-nav{ transition:transform .45s var(--ease-out-expo), visibility .45s; }
  .main-nav a{
    opacity:0;
    transform:translateX(24px);
    transition:opacity .4s var(--ease-out-soft), transform .5s var(--ease-out-expo);
  }
  .main-nav.open a{ opacity:1; transform:none; }
  .main-nav.open a:nth-child(1){ transition-delay:.10s; }
  .main-nav.open a:nth-child(2){ transition-delay:.16s; }
  .main-nav.open a:nth-child(3){ transition-delay:.22s; }
  .main-nav.open a:nth-child(4){ transition-delay:.28s; }
  .main-nav.open a:nth-child(5){ transition-delay:.34s; }
  .nav-scrim{
    opacity:0;
    transition:opacity var(--dur-fast) ease;
  }
  .nav-scrim.visible{ opacity:1; }
  .menu-toggle span{
    transition:transform var(--dur-fast) var(--ease-out-expo), opacity .2s ease;
  }
  /* A fixed header that comes and goes is more distracting than useful on a
     phone, where the drawer is one tap away anyway. */
  .site-header.retracted{ transform:none; }
  .site-header.scrolled .logo-img{ height:48px; }
}

/* ------------------------------------------------------------
   Reduced motion
   ------------------------------------------------------------
   script.js skips the whole reveal layer when this is set, so nothing
   here should ever be reachable — it is a backstop in case a reveal is
   mid-flight when the preference changes.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce){
  [data-anim],
  [data-anim]:not(.in){
    opacity:1 !important;
    transform:none !important;
    clip-path:none !important;
  }
  [data-anim="settle"]:not(.in) > img,
  [data-anim="settle"]:not(.in) > video,
  [data-anim="settle"]:not(.in) > a > img,
  [data-anim="mask-up"]:not(.in) .mask-inner{ transform:none !important; filter:none !important; opacity:1 !important; }
  .client-list li[data-anim="disc"]{ --s:1; --o:1; }
  .hl-line > span{ animation:none; }
  .about-badge, .about-badge svg, .side-tab-stack,
  .featured-emblem img{ animation:none; }
  .side-tab-whatsapp::before{ animation:none; opacity:0; }
  /* script.js already declines to autoplay the reels under this preference;
     this only takes the hover push-in off them too. The overlay itself
     stays — it is the section's only control surface. */
  .reel:not([data-anim]):hover video{ transform:none; }
  .about-badge{ opacity:1; }
  .side-tab-stack{ transform:translateY(-50%); opacity:1; }
  .arc-lines circle{ stroke-dashoffset:0 !important; }
  .timeline::before{ transform:scaleY(1) !important; }
  .scroll-progress{ display:none; }
  .quote p[data-anim]:not(.in) .qw > span{ opacity:1; transform:none; }
  .cta .eyebrow.small-caps[data-anim]:not(.in){ letter-spacing:0.22em; }
}
