/* SeekLab shared stylesheet. Generated from the original inline <style>.
   Font URLs are relative to this file (public/css/), hence ../fonts/. */
  /* ---------- Outfit (self-hosted, woff2) ---------- */
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Thin.woff2') format('woff2');       font-weight: 100; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-ExtraLight.woff2') format('woff2'); font-weight: 200; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Light.woff2') format('woff2');      font-weight: 300; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Regular.woff2') format('woff2');    font-weight: 400; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Medium.woff2') format('woff2');     font-weight: 500; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-SemiBold.woff2') format('woff2');   font-weight: 600; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Bold.woff2') format('woff2');       font-weight: 700; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-ExtraBold.woff2') format('woff2');  font-weight: 800; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Outfit'; src: url('../fonts/Outfit-Black.woff2') format('woff2');      font-weight: 900; font-style: normal; font-display: swap; }


  /* ---------- Google Sans (self-hosted, variable woff2) ----------
     One variable file per subset covers the whole 400-700 range, which is every weight
     this site uses. Downloaded from Google Fonts, which publishes Google Sans under the
     SIL Open Font License (metadata reports license "ofl", isOpenSource true), so
     self-hosting is permitted and no request reaches a third party at runtime.
     Outfit stays in the stack as a local fallback and is not fetched while this loads. */
  @font-face {
    font-family: 'Google Sans';
    src: url('../fonts/GoogleSans-latin.woff2') format('woff2-variations'),
         url('../fonts/GoogleSans-latin.woff2') format('woff2');
    font-weight: 400 700; font-style: normal; font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  @font-face {
    font-family: 'Google Sans';
    src: url('../fonts/GoogleSans-latin-ext.woff2') format('woff2-variations'),
         url('../fonts/GoogleSans-latin-ext.woff2') format('woff2');
    font-weight: 400 700; font-style: normal; font-display: swap;
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }

  html { overflow-x: clip; }
  /* Page ground. The ember blobs sit above this as their own elements, so the warm blur
     survives the switch from the old cream (#FBF7F0) to this neutral grey. */
  body { background: #F6F5F5; font-family: 'Google Sans', 'Outfit', -apple-system, sans-serif; overflow-x: clip; }

  /* ---------- Glass (light mode) ---------- */
  .glass { background: rgba(255,255,255,0.62); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border: 1px solid rgba(28,25,23,0.07); box-shadow: 0 1px 2px rgba(124,45,18,.04), 0 14px 34px -18px rgba(124,45,18,.16); }
  .glass-strong { background: rgba(255,255,255,0.8); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border: 1px solid rgba(234,88,12,0.16); box-shadow: 0 2px 4px rgba(124,45,18,.05), 0 22px 48px -20px rgba(124,45,18,.22); }
  .glass-orange { background: linear-gradient(135deg, rgba(249,115,22,0.12), rgba(249,115,22,0.03)); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(234,88,12,0.22); }

  /* backdrop-filter is one of the most expensive things a browser can composite, and
     it only earns its cost when there is real detail behind the surface. That is true
     of the floating navbar over the hero collage, and of nothing else here: the other
     39 glass cards sit on flat cream, where blurring returns identical pixels. Verified
     by A/B screenshot before removing. The translucent background alone carries the look. */
  section .glass, section .glass-strong, section .glass-orange,
  footer .glass, footer .glass-strong, footer .glass-orange {
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }

  .text-gradient { background: linear-gradient(100deg, #F97316 0%, #EA580C 45%, #C2410C 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
  .grid-bg { background-image: linear-gradient(rgba(234,88,12,0.055) 1px, transparent 1px), linear-gradient(90deg, rgba(234,88,12,0.055) 1px, transparent 1px); background-size: 56px 56px; }

  /* ---------- Relay-scale headlines ---------- */
  /* Outfit reads heavier than DM Sans at the same numeric weight, so the display type sits at
     Medium rather than Black. Tracking is relaxed a little too: tight negative letter-spacing
     was tuned for a heavy face and pinches the lighter one. */
  /* Sentence case, not uppercase. The brand brief rules Title Case out entirely, so the
     source copy is sentence case and the display face no longer transforms it. Tracking is
     eased back toward neutral: tight negative letter-spacing suits caps, and pinches
     lowercase, which has more varied letterforms to breathe between. */
  .headline { font-family: 'Outfit', sans-serif; font-weight: 600; letter-spacing: -0.02em; line-height: 1.05; }

  /* hero word stagger (animated by GSAP, hidden pre-init only if JS runs) */
  .hero-line { display: block; overflow: hidden; padding: 0 .05em .12em; margin-bottom: -.12em; }
  .hero-word { display: inline-block; will-change: transform; }

  /* Relay-style italic serif accent line */
  /* Accent phrase inside a headline. Formerly Playfair Display italic; now Outfit, so the
     site runs on a single self-hosted typeface with no third-party font request at all.
     Kept upright deliberately: Outfit ships no italic, so font-style:italic would force the
     browser to fake a slant, which shears the geometric letterforms badly. The accent now
     reads through weight and the ember gradient instead of through a change of face. */
  /* Weight and tracking deliberately match .headline exactly, so the accent differs from the
     rest of the headline only by colour. Not `inherit`: this class is also used on five
     standalone pull-quote paragraphs, which would otherwise drop to body weight. */
  .serif-impact { font-family: 'Outfit', sans-serif; font-style: normal; font-weight: 600; letter-spacing: -0.02em; }

  /* hero floating collage */
  .hero-item { will-change: transform; }
  .hero-float { animation: floatY 6s ease-in-out infinite; }
  /* transparent PNG cutouts get their own soft realistic shadow */
  .cutout { filter: drop-shadow(0 18px 24px rgba(28,25,23,.20)) drop-shadow(0 6px 10px rgba(28,25,23,.12)); }

  /* ---------- Scroll progress ---------- */
  #scroll-progress { transform-origin: 0 50%; transform: scaleX(0); }

  /* ---------- Reveal on scroll (GSAP-driven; CSS fallback below) ---------- */
  .reveal { opacity: 0; transform: translateY(32px); }
  .no-js .reveal, .motion-off .reveal { opacity: 1; transform: none; }

  /* scrub-fill story text */
  .fw { color: #DCD3C3; }
  .motion-off .fw { color: #1C1917; }

  /* ---------- SVG animations ---------- */
  @keyframes drawStroke { to { stroke-dashoffset: 0; } }
  .svg-draw { stroke-dasharray: 300; stroke-dashoffset: 300; animation: drawStroke 2.2s ease forwards; }
  .svg-draw-loop { stroke-dasharray: 160; stroke-dashoffset: 160; animation: drawStroke 2.4s ease infinite alternate; }

  @keyframes pulseDot { 0%,100% { opacity:.35; transform: scale(.85);} 50% { opacity:1; transform: scale(1.15);} }
  .svg-pulse { transform-origin: center; transform-box: fill-box; animation: pulseDot 2.2s ease-in-out infinite; }
  .svg-pulse.d2 { animation-delay: .4s; } .svg-pulse.d3 { animation-delay: .8s; }

  @keyframes floatY { 0%,100% { transform: translateY(0);} 50% { transform: translateY(-8px);} }
  .svg-float { animation: floatY 5s ease-in-out infinite; }
  .svg-float.slow { animation-duration: 7s; }

  @keyframes spinSlow { to { transform: rotate(360deg);} }
  .svg-spin { transform-origin: center; transform-box: fill-box; animation: spinSlow 14s linear infinite; }

  @keyframes orbitPath { 0% { transform: rotate(0deg) translateX(10px) rotate(0deg);} 100% { transform: rotate(360deg) translateX(10px) rotate(-360deg);} }
  .svg-orbit { transform-origin: center; transform-box: fill-box; animation: orbitPath 6s linear infinite; }

  @keyframes scanLine { 0% { transform: translateX(-100%);} 100% { transform: translateX(420%);} }
  .scan-line { animation: scanLine 3.8s ease-in-out infinite; }

  @keyframes barGrow { 0%,100% { transform: scaleY(.55);} 50% { transform: scaleY(1);} }
  .svg-bar { transform-origin: bottom; transform-box: fill-box; animation: barGrow 3s ease-in-out infinite; }
  .svg-bar.b2 { animation-delay: .3s;} .svg-bar.b3 { animation-delay: .6s;}
  .svg-bar.b4 { animation-delay: .9s;} .svg-bar.b5 { animation-delay: 1.2s;}

  @keyframes dashFlow { to { stroke-dashoffset: -28; } }
  .svg-flow { stroke-dasharray: 6 8; animation: dashFlow 1.6s linear infinite; }

  /* Translate only, deliberately. Scaling a blurred element forces the browser to
     re-rasterise the whole blurred surface every frame; translating an already
     rasterised layer is handled by the GPU for free. Keeping scale() here was
     re-blurring ~2.4x the viewport on every frame, which is what cooked the GPU. */
  @keyframes blobDrift { 0%,100% { transform: translate(0,0);} 33% { transform: translate(40px,-30px);} 66% { transform: translate(-30px,24px);} }
  .blob { animation: blobDrift 16s ease-in-out infinite; filter: blur(90px); will-change: transform; }
  .blob.b2 { animation-delay: -6s; }

  @keyframes shimmerSweep { 0% { transform: translateX(-150%) skewX(-15deg);} 100% { transform: translateX(350%) skewX(-15deg);} }

  @keyframes marquee { 0% { transform: translateX(0);} 100% { transform: translateX(-50%);} }
  .marquee-track { animation: marquee 32s linear infinite; }
  .marquee-track:hover { animation-play-state: paused; }

  /* Client logos: muted by default so eight brand palettes do not fight the page,
     full colour on hover. Heights are tuned per logo because the source files
     range from 0.80 to 4.84 in aspect ratio. */
  .client-logo { object-fit: contain; filter: grayscale(1) brightness(.72) contrast(1.15); opacity: .85; transition: filter .35s ease, opacity .35s ease; }
  .client-logo:hover { filter: none; opacity: 1; }

  @keyframes tickerUp { 0%,18% { transform: translateY(0);} 33%,51% { transform: translateY(-100%);} 66%,84% { transform: translateY(-200%);} 100% { transform: translateY(-300%);} }
  .ticker { animation: tickerUp 9s cubic-bezier(.65,0,.35,1) infinite; }

  @keyframes ringPing { 0% { transform: scale(.6); opacity:.9;} 100% { transform: scale(1.6); opacity:0;} }
  .svg-ping { transform-origin: center; transform-box: fill-box; animation: ringPing 2.4s ease-out infinite; }

  /* ---------- Trust Stack scrollytelling ---------- */
  .stack-viz-layer { opacity: .18; transition: opacity .5s ease, transform .5s cubic-bezier(.22,1,.36,1); transform: translateY(4px); }
  .stack-viz-layer.on { opacity: 1; transform: translateY(0); }
  .stack-card { transition: border-color .4s ease, background-color .4s ease, transform .4s ease; }
  .stack-card.active { border-color: rgba(234,88,12,.35); background: rgba(255,255,255,.85); transform: translateX(4px); }

  /* Off-screen sections stop animating. Measured on this page: 68 of 79 running
     animations were outside the viewport, all of it GPU work nobody could see. */
  .anim-idle, .anim-idle *, .anim-idle *::before, .anim-idle *::after { animation-play-state: paused !important; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
    .reveal { opacity:1; transform:none; }
    .fw { color:#1C1917; }
    .stack-viz-layer { opacity:1; transform:none; }
  }

/* ---------- Unverified proof placeholders ----------
   The brand brief lists results, client testimonials, pricing, headcount, and
   leadership detail as unverified, and says not to guess them. Any slot awaiting
   real, permissioned data is wrapped in .needs-proof so it is impossible to ship
   by accident. Grep the repo for "needs-proof" before launch; delete this rule
   only once every instance is replaced with an approved figure. */
.needs-proof {
  background: repeating-linear-gradient(45deg, rgba(234,88,12,.10) 0 8px, rgba(234,88,12,.18) 8px 16px);
  border: 1px dashed rgba(194,65,12,.55);
  color: #9A3412; border-radius: 6px;
  padding: 0 .4em; font-style: normal; font-weight: 600;
  white-space: normal; display: inline-block; max-width: 100%;
}
.needs-proof::before { content: '⚑ '; }

/* Depth tiers for the scattered client field. The reference fades some marks almost
   to nothing; these are real clients, so the range is kept shallow enough that every
   logo stays readable while the field still reads as layered rather than a table. */
.client-logo--near  { opacity: .92; }
.client-logo--mid   { opacity: .68; }
.client-logo--far   { opacity: .48; }
.client-logo--near:hover, .client-logo--mid:hover, .client-logo--far:hover { opacity: 1; }

/* Client marquee rows. Speed comes from an inline animation-duration per row, and the
   middle row runs in reverse, so the three rows drift apart instead of marching in step.
   Rows fade at both edges so logos enter and leave rather than popping. */
.client-row { -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
              mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.marquee-rev { animation-direction: reverse; }
.client-marquee:hover { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .client-marquee { animation: none !important; } }

/* ---------- Hero collage drag ----------
   The drag offset uses the independent CSS `translate` property, not `transform`:
   .hero-item's transform belongs to the GSAP scroll parallax and .hero-float's to the
   floatY keyframes, so writing transform here would fight both. `translate` composes
   with the image's existing rotate() and is otherwise unused.
   Gated to precise pointers. Every collage item is hidden below lg, so this never
   competes with touch scrolling. */
@media (pointer: fine) {
  #home .hero-item img.cutout { pointer-events: auto; cursor: grab; -webkit-user-drag: none; user-select: none; }
  #home .hero-item img.cutout.is-dragging { cursor: grabbing; }
}
/* Slight overshoot on the way home, so it settles like a magnet rather than a slider. */
#home .hero-item img.cutout.is-returning { transition: translate .8s cubic-bezier(.22,1.35,.36,1); }
@media (prefers-reduced-motion: reduce) {
  #home .hero-item img.cutout { pointer-events: none; }
}

/* ---------- Navbar width on scroll ----------
   At the very top the bar spans the full width inside the header's 16px side gap.
   Once scrolling starts it contracts to the content width, so the bar reads as part
   of the page rather than a floating pill while the hero is in view.
   max-width is not a compositor property, but this fires once per threshold crossing
   rather than every frame, so the layout cost is paid twice per scroll direction. */
.nav-shell { max-width: 100%; transition: max-width .55s cubic-bezier(.22,1,.36,1); }
#navbar.is-scrolled .nav-shell { max-width: 90rem; } /* matches maxWidth.site, 1440px */
@media (prefers-reduced-motion: reduce) {
  .nav-shell { transition: none; }
}


/* ---------- Outlined button ----------
   Stroke only at rest; on hover an ember fill sweeps in from the left, the label flips
   to white and the arrow nudges forward. Both animated properties are transform and
   colour, so the sweep stays on the compositor.
   The fill is a ::before at z-index -1 inside a stacking context created by z-index:0
   on the button, which keeps it behind the label without needing an extra wrapper. */
.btn-outline {
  position: relative; z-index: 0; overflow: hidden;
  border: 1px solid rgba(234,88,12,.45);
  color: #C2410C; border-radius: .75rem;
  transition: color .3s ease, border-color .3s ease;
}
.btn-outline::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(100deg, #F97316, #EA580C);
  transform: translateX(-101%);
  transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.btn-outline:hover, .btn-outline:focus-visible { color: #fff; border-color: transparent; }
.btn-outline:hover::before, .btn-outline:focus-visible::before { transform: translateX(0); }
.btn-outline svg { transition: transform .35s cubic-bezier(.22,1,.36,1); }
.btn-outline:hover svg, .btn-outline:focus-visible svg { transform: translateX(4px); }
.btn-outline:focus-visible { outline: 2px solid #FB923C; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .btn-outline::before { transition: none; }
  .btn-outline svg { transition: none; }
  .btn-outline:hover svg { transform: none; }
}




/* ---------- Trust Stack stage cards ----------
   The card carries a quiet surface at rest and turns orange on hover. The disc behind
   each icon is hidden until then, so the icon reads straight off the card and the disc
   arrives with the colour. */
.method-card {
  background: linear-gradient(170deg, #FFFFFF 0%, #FCFAF9 100%);
  border: 1px solid rgba(28,25,23,.07);
  box-shadow: 0 1px 2px rgba(124,45,18,.04), 0 14px 34px -18px rgba(124,45,18,.16);
  transition: background .35s ease, border-color .35s ease, box-shadow .35s ease,
              transform .35s cubic-bezier(.22,1,.36,1);
}
/* ember-700 into ember-800 rather than the brighter 500/600 steps: the body copy turns
   white here and needs 4.5:1, which #F97316 misses at 2.8:1 and #EA580C at 3.5:1. These
   two clear it at 5.2:1 and 7.4:1. */
.method-card:hover {
  background: linear-gradient(155deg, #C2410C 0%, #9A3412 100%);
  border-color: rgba(154,52,18,.55);
  box-shadow: 0 2px 6px rgba(124,45,18,.14), 0 26px 50px -20px rgba(124,45,18,.45);
  transform: translateY(-6px);
}
.method-card h3, .method-card p { transition: color .35s ease; }
.method-card:hover h3, .method-card:hover p { color: #FFFFFF; }

/* Stage badge: a floating white disc, revealed on hover. Shadow is soft and warm to
   match the palette. */
.stage-badge {
  background: transparent;
  border: 1px solid transparent;
  box-shadow: none;
  transform: scale(.92);
  /* Slight overshoot on the curve, so the disc pops in rather than fading up. */
  transition: background .35s ease, border-color .35s ease, box-shadow .35s ease,
              transform .45s cubic-bezier(.34,1.56,.64,1);
}
.method-card:hover .stage-badge {
  background: #FFFFFF;
  border-color: rgba(28,25,23,.05);
  box-shadow: 0 1px 2px rgba(124,45,18,.06), 0 10px 24px -12px rgba(124,45,18,.28);
  transform: scale(1);
}
.stage-badge img { transition: transform .45s cubic-bezier(.22,1,.36,1); }
.method-card:hover .stage-badge img { transform: scale(1.1); }

@media (prefers-reduced-motion: reduce) {
  .method-card, .method-card:hover { transform: none; }
  .stage-badge, .method-card:hover .stage-badge { transform: none;
    transition: background .35s ease, border-color .35s ease, box-shadow .35s ease; }
  .stage-badge img, .method-card:hover .stage-badge img { transform: none; transition: none; }
}

/* ---------- Services: pinned rail and artwork, scrolling copy ---------- */
.svc-num .svc-figure { font-weight: 600; }
.svc-num.active .svc-figure { color: #1C1917; }
.svc-num.active .svc-dot { opacity: 1; }
.svc-num:hover .svc-figure { color: #57534E; }
@media (min-width: 1024px) {
  .svc-panel { opacity: .35; transition: opacity .45s ease; }
  .svc-panel.active { opacity: 1; }
}
.svc-stage-art {
  opacity: 0; transform: scale(.94); pointer-events: none;
  transition: opacity .5s ease, transform .7s cubic-bezier(.22,1,.36,1);
}
.svc-stage-art.active { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .svc-panel, .svc-panel.active { opacity: 1; }
  .svc-stage-art { transition: none; }
}
#svc-rail { transition: transform .55s cubic-bezier(.22,1,.36,1); }
@media (prefers-reduced-motion: reduce) { #svc-rail { transition: none; } }

/* ---------- Featured work: dark portrait cards ----------
   Restyled after the reference supplied for this section: a circular portrait medallion
   over a dark card, three across. The reference fans its cards at an angle; these stay
   upright and column-aligned, which is what was asked for and also keeps the three
   metric rows sitting on one baseline. */
.work-card {
  /* Positioned so the photo and scrim layers below resolve against the card. Without
     this they resolve against the grid wrapper and wash the whole row. */
  position: relative;
  background: linear-gradient(168deg, #272220 0%, #1A1615 55%, #120F0E 100%);
  border: 1px solid rgba(255,255,255,.08);
  box-shadow: 0 2px 4px rgba(28,25,23,.10), 0 30px 60px -28px rgba(28,25,23,.55);
  transition: transform .45s cubic-bezier(.22,1,.36,1), border-color .3s ease, box-shadow .45s ease;
}
.work-card:hover {
  transform: translateY(-6px);
  border-color: rgba(251,146,60,.34);
  box-shadow: 0 2px 4px rgba(28,25,23,.10), 0 40px 72px -26px rgba(124,45,18,.50);
}
@media (prefers-reduced-motion: reduce) {
  .work-card, .work-card:hover { transform: none; transition: border-color .3s ease; }
}

/* Logo plate. The client marks are dark ink on transparent (measured luma 104-140) and
   are wide lockups, up to 4.2:1, so they need a light panel rather than the dark circular
   medallion this section first used: in a circle they render too small to read. */
.work-plate {
  background: linear-gradient(160deg, #FFFFFF 0%, #F7F4F1 100%);
  border: 1px solid rgba(255,255,255,.16);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.85), 0 8px 20px -12px rgba(0,0,0,.55);
}

/* Outlined capability pills, sitting at the foot of the copy. */
.work-tag {
  display: inline-flex; align-items: center;
  font-size: 11px; font-weight: 600; line-height: 1;
  color: rgba(255,255,255,.78);
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.16);
  border-radius: 9999px; padding: .4rem .7rem;
  transition: border-color .25s ease, color .25s ease;
}
.work-card:hover .work-tag { border-color: rgba(253,186,116,.34); color: rgba(255,255,255,.9); }

/* Faint horizontal rules running behind the row, the one background cue kept from the
   reference. Static gradient, no blur and no animation, so it costs nothing to composite
   and does not reopen the idle-GPU problem fixed in 6f3f7e5. */
.work-rules {
  background-image: repeating-linear-gradient(180deg, rgba(194,65,12,.13) 0 1px, transparent 1px 14px);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

/* ---------- Engagement plans ----------
   Structure taken from the pricing reference supplied for this section: mark and name on
   one row, copy, a dotted rule, the inclusions list, a second rule, then the investment
   line over a full-width pill. Two light cards and one inverted card, as in the reference.
   The inverted one is the Clarity Session rather than the last card, because the section
   copy says every route starts there and that is where the primary CTA lives. */
.plan-card {
  background: linear-gradient(170deg, #FFFFFF 0%, #FCFAF9 100%);
  border: 1px solid rgba(28,25,23,.08);
  box-shadow: 0 1px 2px rgba(124,45,18,.04), 0 18px 40px -24px rgba(124,45,18,.20);
  transition: transform .45s cubic-bezier(.22,1,.36,1), border-color .3s ease, box-shadow .45s ease;
}
.plan-card--dark {
  background: linear-gradient(168deg, #272220 0%, #1A1615 55%, #120F0E 100%);
  border-color: rgba(255,255,255,.08);
  box-shadow: 0 2px 4px rgba(28,25,23,.10), 0 30px 60px -28px rgba(28,25,23,.55);
}
.plan-card:hover {
  transform: translateY(-6px);
  border-color: rgba(251,146,60,.34);
  box-shadow: 0 2px 6px rgba(124,45,18,.08), 0 28px 54px -24px rgba(124,45,18,.32);
}
.plan-card--dark:hover {
  border-color: rgba(251,146,60,.40);
  box-shadow: 0 2px 6px rgba(28,25,23,.12), 0 34px 62px -24px rgba(28,25,23,.62);
}

/* Plan mark grows on the same overshoot curve as the Trust Stack badges, so the two
   sections feel like they belong to one system. */
.plan-mark { transition: transform .45s cubic-bezier(.34,1.56,.64,1); }
.plan-card:hover .plan-mark { transform: scale(1.14); }

/* Inclusion icons nudge in as the card lifts. */
.plan-card ul svg { transition: transform .35s cubic-bezier(.22,1,.36,1); }
.plan-card:hover ul svg { transform: translateX(2px); }

/* The dotted rules pick up a hint of the accent. */
.plan-rule { transition: background-image .35s ease; }
.plan-card:hover .plan-rule {
  background-image: radial-gradient(circle, rgba(234,88,12,.40) 0 1px, transparent 1px);
}
.plan-card--dark:hover .plan-rule {
  background-image: radial-gradient(circle, rgba(253,186,116,.48) 0 1px, transparent 1px);
}

@media (prefers-reduced-motion: reduce) {
  .plan-card, .plan-card:hover { transform: none; transition: border-color .3s ease; }
  .plan-mark, .plan-card:hover .plan-mark { transform: none; transition: none; }
  .plan-card ul svg, .plan-card:hover ul svg { transform: none; transition: none; }
}

/* Dotted rule. Drawn as a gradient rather than border-style:dotted, which renders as
   uneven squares at 1px in several browsers. */
.plan-rule {
  height: 1px;
  background-image: radial-gradient(circle, rgba(28,25,23,.26) 0 1px, transparent 1px);
  background-size: 6px 1px; background-repeat: repeat-x;
}
.plan-card--dark .plan-rule {
  background-image: radial-gradient(circle, rgba(255,255,255,.28) 0 1px, transparent 1px);
}

/* ---------- Button motion ----------
   Lift, sweep, and arrow nudge, shared by every CTA on the site. Everything is
   hover-driven: the sweep reuses the existing shimmerSweep keyframes but is attached on
   :hover rather than running infinite like .shimmer, so a page full of buttons costs
   nothing while it sits idle (6f3f7e5). Variants only differ in the hover shadow, which
   has to match the fill it sits under. */
.btn-fx { position: relative; overflow: hidden; transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s ease, background-color .2s ease; }
.btn-fx:hover { transform: translateY(-2px); }
.btn-fx:active { transform: translateY(0); }
.btn-fx .btn-arrow { transition: transform .3s cubic-bezier(.22,1,.36,1); }
.btn-fx:hover .btn-arrow { transform: translateX(3px); }

.btn-fx::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  transform: translateX(-150%) skewX(-15deg);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.30), transparent);
}
.btn-fx:hover::after { animation: shimmerSweep .9s ease-out; }
/* A white sweep is invisible on pale fills, so those get a dark one. */
.btn-fx--invert::after, .btn-fx--quiet::after { background: linear-gradient(90deg, transparent, rgba(28,25,23,.16), transparent); }

.btn-fx--ember:hover  { box-shadow: 0 12px 26px -12px rgba(234,88,12,.70); }
.btn-fx--ink:hover    { box-shadow: 0 12px 26px -12px rgba(28,25,23,.60); }
.btn-fx--invert:hover { box-shadow: 0 12px 26px -12px rgba(0,0,0,.55); }
.btn-fx--quiet:hover  { box-shadow: 0 10px 22px -14px rgba(124,45,18,.45); }

@media (prefers-reduced-motion: reduce) {
  .btn-fx, .btn-fx:hover, .btn-fx:active { transform: none; }
  .btn-fx:hover::after { animation: none; }
  .btn-fx:hover .btn-arrow { transform: none; }
}

/* Mobile service panels. The render leads the copy and drifts against the scroll as
   its panel crosses the viewport: --sp (0 to 1) is written per panel by site.js, and
   everything below is derived from it, so a frame is one style write per panel.
   Desktop hides these images entirely, hence the max-width guard. */
@media (max-width: 1023px) {
  .svc-shot img {
    transform: translate3d(0, calc((var(--sp, .5) - .5) * -34px), 0)
               scale(calc(.94 + var(--sp, .5) * .12));
    opacity: clamp(.4, calc(var(--sp, .5) * 2.6), 1);
    will-change: transform;
  }
}

@media (prefers-reduced-motion: reduce) {
  .svc-shot img { transform: none; opacity: 1; }
}

/* Work cards ride a horizontal snap rail below md, where three across will not fit.
   The scrollbar is hidden deliberately: the half-visible next card is the affordance.
   The rail bleeds to the screen edge (-mx-4 px-4 in the markup), so scroll-padding
   keeps a snapped card clear of that inset rather than flush against the bezel. */
.work-rail {
  scroll-snap-type: x mandatory;
  scroll-padding-left: 1rem;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.work-rail::-webkit-scrollbar { display: none; }

/* Above md the same element is a plain grid again, so snapping must not linger. */
@media (min-width: 768px) {
  .work-rail { scroll-snap-type: none; scroll-padding-left: 0; }
}

/* ---------- Campus photograph on a work card ----------
   The same file does two jobs: a sharp plate at the head of the card, and a blurred wash
   behind the whole card. Both are CSS background layers rather than <img>, so a card
   whose photograph is not in the repo yet keeps the charcoal gradient instead of
   rendering a broken image. The tradeoff is that validate.mjs only checks href and src,
   so a mistyped path here fails silently: confirm the card actually shows the photo. */

/* Blurred wash. Scaled up because blur leaves soft, transparent edges that would
   otherwise show as a pale border inside the card. Static, so it is painted once and
   costs nothing to hold. */
.work-photo-blur {
  position: absolute; inset: 0; z-index: 0;
  background-position: center; background-size: cover;
  filter: blur(18px) saturate(1.08);
  transform: scale(1.18);
  opacity: .85;
}

/* Scrim over the wash, weighted to the foot of the card where the copy sits. */
.work-scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(18,15,14,.58) 0%, rgba(18,15,14,.84) 46%, rgba(18,15,14,.96) 100%);
}

/* Sharp photograph at the head, masked so its foot dissolves into the card instead of
   ending on a hard edge. */
.work-photo-top {
  height: 11rem;
  background-position: center; background-size: cover;
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 46%, rgba(0,0,0,.45) 74%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 46%, rgba(0,0,0,.45) 74%, transparent 100%);
  transition: transform .7s cubic-bezier(.22,1,.36,1);
}
.work-card:hover .work-photo-top { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) { .work-card:hover .work-photo-top { transform: none; } }


/* Light variant of the work card, for a card carrying a photograph whose client mark
   has to appear in its own colours. The mark is dark ink on transparent, so a white
   body is what lets it sit unaltered rather than being knocked out to a silhouette. */
.work-card--light {
  background: linear-gradient(170deg, #FFFFFF 0%, #FCFAF9 100%);
  border-color: rgba(28,25,23,.08);
  box-shadow: 0 1px 2px rgba(124,45,18,.04), 0 18px 40px -24px rgba(124,45,18,.20);
}
/* White frosted scrim, so the blurred campus wash still shows as a tint under dark copy
   instead of muddying it. */
.work-scrim--light {
  background: linear-gradient(180deg, rgba(255,255,255,.40) 0%, rgba(255,255,255,.76) 38%, rgba(255,255,255,.84) 100%);
}
.work-tag--light {
  color: rgba(28,25,23,.72);
  background: rgba(28,25,23,.03);
  border-color: rgba(28,25,23,.14);
}
.work-card--light:hover .work-tag--light { border-color: rgba(234,88,12,.36); color: #1C1917; }

/* ---------- FAQ accordion ----------
   Built on <details>/<summary>, so open and close, keyboard focus and the accessibility
   tree all come from the element rather than from script. Numbered rows separated by
   hairlines rather than cards. */
.faq-list { counter-reset: faq; }
/* Counter rather than hardcoded digits, so the numbering survives adding or removing a
   question. */
.faq-list > li { counter-increment: faq; border-bottom: 1px solid rgba(28,25,23,.10); }
.faq-num::before { content: counter(faq); }
.faq-item summary { list-style: none; cursor: pointer; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::marker { content: ''; }
.faq-chev {
  transition: transform .3s cubic-bezier(.22,1,.36,1), border-color .3s ease, background .3s ease;
}
.faq-item[open] .faq-chev { transform: rotate(180deg); }
.faq-item summary:hover .faq-chev { border-color: rgba(234,88,12,.35); background: #FFFFFF; }
.faq-item summary:hover .faq-q { color: #C2410C; }
.faq-q { transition: color .25s ease; }
@media (prefers-reduced-motion: reduce) { .faq-chev { transition: border-color .3s ease; } }

/* Answer slides open instead of snapping. ::details-content is the only way to transition
   a <details> without script, since the browser stops rendering the closed content. Guarded
   by @supports: where it is unavailable the answer simply appears at full opacity rather
   than being stuck invisible behind a transition that never runs.
   interpolate-size is scoped to the item, not :root, so enabling keyword animation cannot
   affect transitions elsewhere on the page. */
@supports selector(::details-content) {
  .faq-item { interpolate-size: allow-keywords; }
  .faq-item::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size .38s cubic-bezier(.22,1,.36,1),
                content-visibility .38s allow-discrete;
  }
  .faq-item[open]::details-content { block-size: auto; }

  .faq-item p {
    opacity: 0; transform: translateY(-4px);
    transition: opacity .3s ease .05s, transform .3s ease .05s;
  }
  .faq-item[open] p { opacity: 1; transform: none; }

  @media (prefers-reduced-motion: reduce) {
    .faq-item::details-content { transition: none; }
    .faq-item p, .faq-item[open] p { opacity: 1; transform: none; transition: none; }
  }
}

/* ---------- Showcase: scattered field that zooms through ----------
   One custom property, --p, is written to the stage by JS on scroll. Everything below is
   derived from it in CSS, so a frame costs one style write rather than one per tile.
   Each tile carries --s (when it appears, staggered), --d (parallax depth) and --r
   (resting angle). */
/* Same ground as the rest of the page, so the section reads as part of it. */
.showcase-stage { background: #F6F5F5; }

.showcase-field { transform-origin: 50% 50%; will-change: transform, opacity; }
/* Phase two: the whole field pushes past the viewer and clears out. */
.showcase-field {
  --zoom: clamp(0, calc((var(--p, 0) - 0.42) * 2.1), 1);
  transform: scale(calc(1 + var(--zoom) * 1.85));
  opacity: calc(1 - clamp(0, calc((var(--p, 0) - 0.56) * 2.7), 1));
}

.showcase-tile {
  position: absolute;
  border-radius: 10px;
  overflow: hidden;
  --in: clamp(0, calc((var(--p, 0) - var(--s)) * 7), 1);
  opacity: var(--in);
  /* Parallax rides the same progress value, scaled per tile by its depth. */
  transform: translate3d(0, calc(var(--p, 0) * var(--d) * -70px), 0)
             scale(calc(0.86 + var(--in) * 0.14))
             rotate(var(--r));
  box-shadow: 0 18px 42px -22px rgba(124,45,18,.30);
}

/* The next section's header, waiting behind the field. */
/* Fills its tile regardless of the source ratio; the tile itself carries the real
   aspect-ratio, so nothing is cropped harder than intended. */
.showcase-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

.showcase-reveal {
  --up: clamp(0, calc((var(--p, 0) - 0.54) * 3), 1);
  opacity: var(--up);
  transform: scale(calc(0.92 + var(--up) * 0.08));
  pointer-events: none;
}
.showcase-reveal a { pointer-events: auto; }

/* Nothing scroll-driven: the field sits still, fully visible, with the header shown. */
@media (prefers-reduced-motion: reduce) {
  .showcase-field { transform: none; opacity: 1; }
  .showcase-tile { opacity: 1; transform: rotate(var(--r)); }
  .showcase-reveal { opacity: 1; transform: none; }
  .showcase-track { height: auto !important; }
  .showcase-stage { position: static !important; height: auto !important; padding: 6rem 0; }
}

/* ---------- Work index: filterable project grid ----------
   Cards hide via a class rather than inline style, so the filter never has to remember
   what display value a card started with. */
.wk-card { transition: transform .4s cubic-bezier(.22,1,.36,1), box-shadow .4s ease, opacity .25s ease; }
.wk-card[hidden] { display: none; }
.wk-shot {
  overflow: hidden; border-radius: 1.25rem;
  background: linear-gradient(160deg, #F1EEEC, #E7E2DE);
}
.wk-shot img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .6s cubic-bezier(.22,1,.36,1);
}
.wk-card:hover .wk-shot img { transform: scale(1.04); }
.wk-card:hover .wk-title { color: #C2410C; }
.wk-title { transition: color .25s ease; }

/* Outlined capability pills under each project. */
.wk-tag {
  display: inline-flex; align-items: center;
  font-size: 12px; font-weight: 500; line-height: 1;
  color: rgba(28,25,23,.72);
  background: #FFFFFF;
  border: 1px solid rgba(28,25,23,.12);
  border-radius: 9999px; padding: .45rem .8rem;
}

/* Search and filter controls. */
.wk-control {
  background: #FFFFFF;
  border: 1px solid rgba(28,25,23,.12);
  box-shadow: 0 1px 2px rgba(124,45,18,.04);
  transition: border-color .25s ease, box-shadow .25s ease;
}
.wk-control:focus-within { border-color: rgba(234,88,12,.45); box-shadow: 0 0 0 3px rgba(249,115,22,.14); }
.wk-control select, .wk-control input { background: transparent; border: 0; outline: none; }
.wk-control select { appearance: none; -webkit-appearance: none; padding-right: 1.5rem; }

@media (prefers-reduced-motion: reduce) {
  .wk-card, .wk-card:hover .wk-shot img { transform: none; transition: none; }
}

/* Stand-in for a project with no campaign artwork yet: the client's mark, or its name set
   large, on a warm ground. Deliberately not another client's artwork, which is the only
   other way to fill the slot. */
.wk-shot--mark {
  background: linear-gradient(150deg, #FFF7ED 0%, #F6EFE3 55%, #EFE5D3 100%);
  display: flex; align-items: center; justify-content: center;
}
.wk-shot--mark img { width: auto; height: auto; max-width: 58%; max-height: 44%; object-fit: contain; }
.wk-shot--mark .wk-wordmark {
  font-family: 'Outfit', sans-serif; font-weight: 600; letter-spacing: -0.02em;
  font-size: clamp(1.75rem, 4vw, 2.75rem); color: rgba(28,25,23,.34);
}
.wk-card:hover .wk-shot--mark img { transform: none; }

/* ---------- About: hero shot, circular timeline, team ---------- */

/* Full-bleed hero image. Falls back to a marked panel while no photograph exists, rather
   than borrowing a client's building and implying it is ours. */
.about-shot { overflow: hidden; border-radius: 1.75rem; background: linear-gradient(150deg,#FFF7ED 0%,#F6EFE3 55%,#EFE5D3 100%); }
.about-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.about-shot--empty { display: flex; align-items: center; justify-content: center; }

/* Circular timeline. Markers are placed radially: rotate to the angle, push out by the
   radius, then counter-rotate the label so it stays tangent like the reference. */
.tl-ring { position: relative; aspect-ratio: 1; }
.tl-ring::before {
  content: ''; position: absolute; inset: 0; border-radius: 9999px;
  border: 1px solid rgba(28,25,23,.12);
}
.tl-mark {
  position: absolute; top: 50%; left: 50%; transform-origin: 0 0;
  transform: rotate(var(--a)) translate(var(--r)) rotate(-90deg);
  display: flex; align-items: center; gap: .5rem; white-space: nowrap;
}
.tl-dot { width: 7px; height: 7px; border-radius: 9999px; background: rgba(28,25,23,.22); }
.tl-mark.is-active .tl-dot { background: #EA580C; }
.tl-year { font-family: 'Outfit', sans-serif; font-size: 1.05rem; color: rgba(28,25,23,.34); }
.tl-mark.is-active .tl-year { color: #1C1917; font-weight: 600; }

/* Team portraits. */
.team-shot { overflow: hidden; border-radius: 1.25rem; background: linear-gradient(150deg,#FFF7ED,#EFE5D3); }
.team-shot img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .6s cubic-bezier(.22,1,.36,1); }
.team-card:hover .team-shot img { transform: scale(1.04); }
.team-shot--empty { display: flex; align-items: center; justify-content: center; }

@media (prefers-reduced-motion: reduce) { .team-card:hover .team-shot img { transform: none; } }

/* ---------- Nav dropdown ----------
   A full-width panel under the nav bar rather than a narrow list, so it is positioned
   against .nav-shell instead of the trigger. Opens on hover and on :focus-within, so it
   is reachable by keyboard rather than being pointer-only. The ::before bridges the gap
   between bar and panel; without it the cursor leaves the hover area crossing that gap
   and the panel snaps shut. */
.nav-shell { position: relative; }

/* The hover path from trigger to panel crosses three zones that are not the trigger: the
   rest of the nav row, the shell's own bottom padding, and the visual gap. If any of them
   is outside .nav-drop the menu closes before the cursor arrives, which is the bug this
   layout had. So the trigger stretches to fill the row, and the panel is a transparent
   positioning box whose top padding covers the shell padding plus the gap. Only that
   padding strip overlaps the bar, and nothing interactive sits in it. */
/* The links row is centred in the shell by default, so it is only as tall as the text and
   leaves a dead strip beneath it. Stretching the row, then the trigger inside it, makes the
   trigger reach the shell's content edge, where the panel's padding takes over. */
.nav-links { align-self: stretch; }
.nav-drop { position: static; align-self: stretch; display: flex; align-items: center; }

.nav-drop__panel {
  position: absolute; top: calc(100% - 0.75rem); left: 0; right: 0;
  padding-top: calc(0.75rem + 0.7rem);
  /* The nav shell runs full width until the page is scrolled, so without this cap the
     panel stretches edge to edge at the top of the page. Matches maxWidth.site. */
  max-width: 90rem; margin-inline: auto;
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(8px);
  transition: opacity .22s ease, transform .3s cubic-bezier(.22,1,.36,1), visibility .22s;
}
.nav-drop:hover .nav-drop__panel,
.nav-drop:focus-within .nav-drop__panel {
  opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0);
}

/* The visible card, held inside that transparent box. */
.nav-drop__card {
  padding: 1rem;
  background: #FFFFFF;
  border: 1px solid rgba(28,25,23,.08);
  border-radius: 1.25rem;
  box-shadow: 0 4px 10px rgba(124,45,18,.05), 0 26px 54px -22px rgba(124,45,18,.28);
}

.nav-drop__grid { display: grid; gap: .4rem; grid-template-columns: repeat(2, minmax(0,1fr)); }
@media (min-width: 1100px) { .nav-drop__grid { grid-template-columns: repeat(4, minmax(0,1fr)); } }

/* Icon stacked over the text rather than beside it. */
.nav-drop__item {
  display: flex; flex-direction: column; align-items: flex-start; gap: .85rem;
  padding: 1rem; border-radius: 1rem;
  border: 1px solid transparent;
  transition: background .28s ease, border-color .28s ease;
}
.nav-drop__item:hover, .nav-drop__item:focus-visible {
  background: linear-gradient(160deg, #FFF7ED, #FDF2E4);
  border-color: rgba(234,88,12,.18);
}
/* No tile behind the artwork; the icons carry their own shape. */
.nav-drop__ico {
  flex: none; display: flex; align-items: center; justify-content: center;
  transition: transform .35s cubic-bezier(.34,1.56,.64,1);
}
.nav-drop__item:hover .nav-drop__ico { transform: scale(1.08) rotate(-3deg); }
.nav-drop__ico img { width: 3.5rem; height: 3.5rem; object-fit: contain; }

.nav-drop__title {
  display: block; font-size: 1.125rem; line-height: 1.3;
  font-family: 'Outfit', sans-serif; font-weight: 600; letter-spacing: -0.02em;
  color: #1C1917;
  transition: color .25s ease;
}
.nav-drop__item:hover .nav-drop__title { color: #C2410C; }
.nav-drop__desc { display: block; margin-top: .35rem; font-size: .875rem; line-height: 1.5; color: rgba(87,83,78,.92); }

@media (prefers-reduced-motion: reduce) {
  .nav-drop__panel { transition: opacity .01s, visibility .01s; transform: none; }
  .nav-drop__ico, .nav-drop__item:hover .nav-drop__ico { transform: none; transition: background .28s ease; }
}

/* ---------- Service detail pages ----------
   Layout after the supplied reference: split hero over a full-bleed panel, a sticky
   numbered deliverables list beside the overview, a bulleted why-it-matters, and a
   numbered two-column process grid. Numbering everywhere is CSS counters, so editing a
   list never means renumbering it. */
.svc-list { counter-reset: svcd; }
.svc-list > li { counter-increment: svcd; border-bottom: 1px solid rgba(28,25,23,.10); }
.svc-list > li:last-child { border-bottom: 0; }
.svc-list .svc-list__num::before { content: counter(svcd, decimal-leading-zero); }

.svc-proc { counter-reset: svcp; }
.svc-proc > li { counter-increment: svcp; }
.svc-proc .svc-proc__num::before { content: counter(svcp); }
/* ink-300 is not in the palette, so these counters were inheriting body colour and
   rendering dark. Set explicitly, and lighter than the deliverables numbers. */
.svc-proc__num { color: rgba(28,25,23,.20); }
.svc-list__num { color: rgba(28,25,23,.32); }

/* ---------- Eyebrow ----------
   One definition for the section label used across every page. Inline-flex so it
   shrink-wraps and centres itself inside a text-center parent without extra utilities,
   and gap-2 so an optional accent dot can sit inside without further markup. */
.eyebrow {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .375rem 1rem;
  background: rgba(255,255,255,.75);
  border: 1px solid rgba(28,25,23,.07);
  border-radius: 9999px;
  box-shadow: 0 1px 2px rgba(124,45,18,.05);
  font-size: 11px; font-weight: 600; line-height: 1.45;
  letter-spacing: .18em; text-transform: uppercase;
  color: #57534E;
}
