/* ===================================================================
   PAPI — Premium Business Template Studio
   Hero
=================================================================== */

:root{
  --ink: #0b0b0c;
  --ink-soft: #17171a;
  --cream: #f3ede2;
  --cream-dim: #cfc7b8;
  /* names kept as --gold* on purpose — every var(--gold...) reference
     site-wide still means "the one brand accent color," which is what
     actually matters; only the VALUE changed. This site briefly moved
     off gold to a light blue (each blue value was the gold's R/G/B with
     red and blue channels swapped, #RRGGBB → #BBGGRR); per direct
     request to move back to a yellow/gold metallic, every one of those
     swaps is now reversed back to its original gold value site-wide
     (this file and every hardcoded rgba()/hex duplicate of these vars),
     restoring the exact same luminance/contrast relationships the blue
     had (anywhere the blue was legible against black or white, this
     gold still is, since it's the same transform run in reverse). */
  --gold: #cba869;
  --gold-soft: #e4cd9a;
  --gold-deep: #8a6a1f; /* darker gold for legibility over light backgrounds */

  /* luxury white — used for the showcase section */
  --paper: #f8f6f2;
  --paper-ink: #17181a;
  --paper-dim: rgba(23,24,26,.5);

  /* --accent/--style-contrast used to be driven live, every frame, by
     the old hero particle field's color sweep — nothing updates them
     anymore (the hero background is a static white+bokeh now, see
     #hero below), so these are just fixed defaults. --style-contrast
     in particular used to default to a light cream (right for the old
     near-black hero); switched to a dark tone here since it's what the
     brand mark/CTA border/"purpose" fall back to against the new white
     hero background. --accent/--accent-tint restored to gold along
     with --gold* above, same reasoning */
  --accent: 203, 168, 105;      /* r,g,b */
  --style-contrast: 26, 20, 13;
  /* soft cream-blended tint of --accent, used for supporting text */
  --accent-tint: 222, 208, 180;

  --ease-out: cubic-bezier(.16,1,.3,1);
  --ease-in-out: cubic-bezier(.65,0,.35,1);
}

*{ margin:0; padding:0; box-sizing:border-box; }

/* Awwwards personality pass — a thin, persistent page-scroll progress
   indicator (see js/scroll-progress.js), reinforcing the "journey"
   framing the hero/process sections already establish all the way to
   the footer, without competing with the existing local 1-4 step dots
   inside How It's Built. transform:scaleX only (compositor-cheap,
   matches every other scroll-tied effect's own convention on this
   site) — the script sets a CSS custom property, never touches layout. */
.scroll-progress-track{
  position:fixed;
  top:0; left:0; right:0;
  height:2px;
  background:rgba(243,237,226,.08);
  z-index:1900;
  pointer-events:none;
}
.scroll-progress-fill{
  height:100%;
  width:100%;
  transform-origin:left center;
  transform:scaleX(var(--scroll-progress, 0));
  background:linear-gradient(90deg, #ffd23f, #fff28f);
  box-shadow:0 0 8px rgba(255,221,122,.5);
}
@media (prefers-reduced-motion:reduce){
  .scroll-progress-fill{ transition:none; }
}

html,body{
  min-height:100%;
  /* BUG FIX: per report, "on mobile there's a white/black gap at the
     bottom of the steps section" — js/scroll-dolly.js's extraY depth
     carry (see that file's own comment) translateY's sections like
     .comparison-section without changing their layout box, which
     briefly reveals whatever's behind them for however many px they've
     shifted — that's THIS element, body, showing through. --ink used
     to be a close-but-not-quite #0b0b0c, one shade lighter than every
     section's own #07060a; against near-black that tiny difference
     still read as a visible seam. Matching it exactly means any such
     gap (this one or a future one) is invisible regardless of cause,
     same fix already used once for the faq-section background seam. */
  background:#07060a;
  color:var(--cream);
  font-family:'Inter', system-ui, sans-serif;
  /* mobile Safari's own rubber-band/momentum overscroll can visibly
     wobble a position:fixed element pinned right at the very top of the
     page (this section's own room is pinned starting the instant
     scrolling begins, per _bindScrollTrigger's start:'top top') — a
     documented WebKit interaction, not anything this site's own JS is
     doing. Reported as the water/floor looking "jello"-like on first
     scroll on mobile specifically, which is exactly this class of
     symptom (the water's own moving reflection makes any such wobble
     the most visible thing on screen). This is the standard, safe fix */
  overscroll-behavior-y: none;
}

/* overflow-x:hidden on body only (not on html, and not on both) —
   both was already ruled out (see below): it makes each one's
   overflow-y compute to auto, turning body into a competing scroll
   container that breaks position:sticky for the showcase section. But
   html alone has its own, separate, well-documented WebKit/iOS bug:
   *any* explicit overflow on the root <html> element makes fixed-
   position descendants lay out in normal document flow instead of
   pinning to the viewport — confirmed directly as the cause of the
   showcase section's expanded fan card (reparented onto <body> with
   position:fixed) rendering at the very bottom of the page, among the
   last elements in body's own document flow, instead of centering
   on-screen. Putting it on body alone avoids both: only one element
   ends up with explicit overflow (no competing scroll container), and
   it isn't the root element (no fixed-position bug). */
body{ overflow-x:hidden; }

/* will-change promotes a dolly-driven section to its own compositor
   layer, so the first frame its transform actually changes doesn't
   cause a paint hitch.
   BUG FIX: per report, "when i refresh it the second time... black
   screen" — this used to be permanent, on all 7 sections, for the
   entire page's lifetime (including #liveDemoSection/#ourProcessSection,
   which is actually stale now — js/scroll-dolly.js's own header
   comment notes those two are no longer dolly-driven at all, their
   depth motion moved to the GSAP pins in js/scroll-journey-*.js
   instead, so promoting them here was doing nothing useful, just
   holding 2 extra full-viewport-sized GPU layers in memory for
   nothing). The remaining 5 are still real, but now js/scroll-dolly.js
   itself toggles this via IntersectionObserver (.js-will-change) only
   while a section is actually near the viewport, instead of holding
   every one of them in its own GPU layer for the whole session —
   substantially less standing GPU memory, which is one plausible
   contributor to a heavy page tipping over Safari's per-tab memory
   ceiling on a reload. */
.js-will-change{
  will-change:transform;
}

/* briefly locks scrolling — while the hero's background field grows
   to fill the screen, while the cube section has a face focused, and
   while a showcase fan card is expanded. Plain overflow:hidden on
   html/body (what used to live here) isn't reliably enough to block
   iOS Safari's own touch-driven momentum scrolling: a stray touch
   could still drag the page slightly during a "locked" animation, and
   the snap-back once the lock released was what showed up as a pause/
   freeze right as you scrolled from the hero into the next section.
   window.Papi.lockScroll()/unlockScroll() (see accent.js) actually
   fixes that by intercepting the touchmove/wheel input itself — NOT
   via position:fixed on body, which would hijack the containing block
   of every OTHER position:fixed element on the page (.site-header,
   an expanded fan card...) for the duration of the lock.
   This class just hides the root's own scrollbar/rubber-band to match. */
html.scroll-lock{ overflow:hidden; }

::selection{ background:rgba(var(--accent),.4); color:var(--cream); }

a{ color:inherit; text-decoration:none; }

/* custom cursor replaces the system pointer across the whole page */
body, a{ cursor:none; }

/* ===================================================================
   HERO / CANVAS FIELD
=================================================================== */
/* a natural-height section — content-sized rather than pinned to one
   viewport, since this now holds a full two-column intro (nav
   clearance + headline/CTA/trusted-by on one side, the liquid on the
   other) plus a process-steps row beneath it, not just the liquid mass
   on its own. The liquid mass itself still just wanders freely,
   forever, with "Papi" floating inside it — see js/hero-slime.js's own
   header — now confined to .hero-visual-col instead of the whole
   section (see below). */
/* BUG FIX: per report, "a big white bar sitting on the very top of the
   entire page" in mobile Safari on load. Root cause: this was still
   background:#ffffff, a leftover from a much older hero design (see
   the file-header comment above about "the hero background is a
   static white+bokeh now") that never got updated when the hero moved
   to today's all-dark theme. It stayed invisible for a long time since
   #processRoom (this element's only child) fully covers it at the same
   height — but js/scroll-journey-hero.js's GSAP pin now replaces
   #processRoom with a transparent .pin-spacer inside this exact
   element, which is what finally exposed the stale white underneath. */
#hero{
  position:relative;
  width:100%;
  background:#07060a;
  visibility:hidden;
  opacity:0;
  /* BUG FIX: per the recurring "page goes black after clicking Enter"
     reports — this used to be 1.6s. #hero's own non-1 opacity makes it
     a CSS containing block for any position:fixed descendant, and
     js/scroll-journey-hero.js's GSAP pin on .process-hero is exactly
     that. Reproduced directly: js/scroll-journey-process.js keeps its
     own independent ScrollTrigger.refresh() timers running (a load+
     600ms one-shot, plus a debounced <body> ResizeObserver, both
     completely unrelated to when a visitor actually clicks the loader)
     — if one of those fires while THIS transition is still mid-flight,
     GSAP can measure/rebuild the hero's pin-spacer against a hero that
     is neither fully the old containing-block state nor the new one,
     and the next section's pin-spacer ends up not reserving space
     correctly, sliding that section up to cover the hero completely
     (confirmed via a live repro: .pin-spacer count dropped and
     #liveDemoSection rendered at the very top of the viewport at
     scrollY 0). A 1.6s window gave those independent, arbitrarily-timed
     refresh() calls a wide target to land in; cutting it this short
     closes almost all of it without losing the reveal entirely. */
  transition:opacity .3s var(--ease-in-out);
}
#hero.is-visible{
  visibility:visible;
  opacity:1;
}


/* closing brand signature — bottom-center, fades in only once the
   quote section is reached, as a closing echo of the top-right brand
   mark/tagline rather than just ending on the form. Opacity/transform
   driven directly, continuously, by scroll position in quote-form.js
   (same convention as the rest of that section's reveal) rather than
   a CSS transition, which would fight a value set fresh every frame. */
.closing-brand{
  position:relative;
  z-index:1;
  margin-top:clamp(2.5rem, 6vw, 4rem);
  text-align:center;
  pointer-events:none;
  opacity:0;
  will-change:opacity, transform;
}
.closing-brand-mark{
  font-family:'Fraunces', serif;
  font-weight:500;
  font-size:clamp(1.2rem, 2.4vw, 1.7rem);
  letter-spacing:.28em;
  color:var(--gold-soft);
  text-shadow:0 2px 20px rgba(0,0,0,.5);
}
.closing-brand-line{
  margin-top:.35rem;
  font-size:.85rem;
  color:rgba(243,237,226,.75);
}
.closing-brand-line em{
  font-style:italic;
  color:#ffdd7a;
}


/* the film-grain overlay — see its own <div> comment in index.html for
   why it's a sibling, not a child, of .process-room-visual. The texture
   itself is a small tiled SVG feTurbulence noise field (fractalNoise,
   no displacement — a flat grayscale grain pattern, not the kind of
   filter that warps/displaces the element's own shape), inlined as a
   data URI so there's no extra image request; mix-blend-mode:overlay is
   what actually makes it read as film grain sitting ON the scene rather
   than a flat gray fog laid over it — overlay lightens already-light
   pixels and darkens already-dark ones, the same way real grain
   interacts with an image. Opacity baseline raised (0.05→0.12) — per
   direct request, after a direct unseen.co comparison: 0.05 barely
   registered at all, while unseen's own grain is genuinely visible
   texture sitting on top of the whole frame, not just a faint hint of
   one. Still a persistent, constant baseline across the whole hero
   (not only during the Section 1 → Section 2 transition); JS (see
   process-room.js's own _applyWipeGrain) still boosts it further on top
   of this baseline for that transition, peaking mid-wipe and easing
   back down to the baseline (never all the way to invisible) once done. */
.process-room-grain{
  position:absolute;
  inset:0;
  z-index:2;
  opacity:0.12;
  pointer-events:none;
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:180px 180px;
  will-change:opacity;
}
@media (prefers-reduced-motion:no-preference){
  .process-room-grain{
    /* the classic CSS film-grain trick — steps() jumps the background
       position in hard cuts rather than easing between them, which is
       what actually reads as flickering grain rather than a texture
       smoothly sliding around */
    animation:process-room-grain-jitter .5s steps(4) infinite;
  }
}
@keyframes process-room-grain-jitter{
  0%{ background-position:0 0; }
  25%{ background-position:-15% 8%; }
  50%{ background-position:6% -12%; }
  75%{ background-position:-8% -6%; }
  100%{ background-position:0 0; }
}




/* the rocket's own "landing" + the "back to top" control (see
   index.html's own comment for why this is a sibling of #liveDemoSection,
   not inside it) — a normal in-flow row sitting right above that
   section's own title, not fixed: it should only ever be part of
   Section 2's own arrival moment, not a permanent floating corner badge.
   display:none by default (toggled to flex from JS) so it takes no
   layout space until process-room.js actually reveals it */
/* display flips from JS immediately (see _completeDissolveHandoff), but
   the actual reveal is the opacity/transform pair below, toggled a tick
   later via the .is-visible class (see that same method) — flipping
   display and the transitioned properties in the same tick wouldn't
   animate at all, since the element goes straight from not-rendered to
   its own final state with nothing in between for the browser to
   transition from */
.process-arrival{
  display:none;
  position:absolute;
  top:0;
  left:0;
  right:0;
  align-items:center;
  justify-content:center;
  gap:1rem;
  padding:1.6rem 0;
  z-index:2;
  opacity:0;
  transform:translateY(-14px);
  transition:opacity .7s ease, transform .7s ease;
}
.process-arrival.is-visible{
  opacity:1;
  transform:translateY(0);
}
@media (prefers-reduced-motion:reduce){
  .process-arrival{ transition:none; }
}
.process-arrival-rocket{
  width:2.6rem;
  height:auto;
  flex-shrink:0;
  filter:drop-shadow(0 0 10px rgba(255,221,122,.35));
}
.process-arrival-flame{
  transform-origin:30px 66px;
  animation:processArrivalFlicker 1.4s ease-in-out infinite;
}
@keyframes processArrivalFlicker{
  0%, 100%{ transform:scaleY(1); opacity:.95; }
  50%{ transform:scaleY(.82); opacity:.75; }
}
.process-back-btn{
  display:flex;
  align-items:center;
  gap:.5rem;
  font-family:'Inter', sans-serif;
  font-size:.78rem;
  font-weight:600;
  letter-spacing:.04em;
  color:#f3ede2;
  background:rgba(255,255,255,.08);
  border:1px solid rgba(243,237,226,.22);
  border-radius:999px;
  padding:.6rem 1.1rem .6rem .85rem;
  cursor:pointer;
  transition:background .2s ease;
}
.process-back-btn:hover{ background:rgba(255,255,255,.14); }
.process-back-btn svg{ flex-shrink:0; }
@media (prefers-reduced-motion:reduce){
  .process-arrival-flame{ animation:none; }
}
@media (max-width:860px){
  /* stacked, not side-by-side — at this width the row was crowding
     right into .live-demo-eyebrow's own "See It Live" text just below.
     Stays position:absolute (see the base rule) rather than switching
     to relative/in-flow: flipping to in-flow adds real document height
     the instant JS reveals this row, which (confirmed directly) retro-
     actively shifts everything below it down by that same height —
     since _completeDissolveHandoff computes its own scroll landing
     spot and only THEN reveals this row, the page would land short by
     however tall this row turned out to be. margin-top below reserves
     the same visual room a different way, on .live-demo-eyebrow itself,
     which doesn't have that side effect */
  .process-arrival{ flex-direction:column; gap:.4rem; padding:1.4rem 0 0; }
  .process-arrival-rocket{ width:1.9rem; }
  .process-back-btn{ font-size:.7rem; padding:.55rem 1rem .55rem .75rem; }
  .live-demo-eyebrow{ margin-top:5.6rem; }
}

/* html{scroll-behavior:smooth} used to live here, easing every plain
   anchor-jump link sitewide (just the 3 neon-menu links currently) — but
   GSAP ScrollTrigger explicitly documents a conflict with that CSS
   property: it smooths EVERY scroll-position change page-wide, including
   ScrollTrigger's own continuous scrub updates while pinned, which reads
   as the pinned scene's scroll position visibly drifting/lagging behind
   the real scrollbar instead of tracking it exactly. process-room.js's
   own click handler on .process-neon-link now calls scrollIntoView({
   behavior:'smooth'}) directly instead — that's a one-off, self-contained
   animation per click rather than a standing page-wide property, so it
   doesn't fight ScrollTrigger's own scroll handling the same way. */
/* ===================================================================
   SCROLL REVEAL — per direct request: "add more creative animation to
   every section so it feels like an immersive flow." js/scroll-
   reveal.js toggles .is-revealed once each [data-reveal] element
   scrolls into view (one-shot, via IntersectionObserver). Plain
   data-reveal is a fade+rise; data-reveal="fade" is opacity-only (for
   an eyebrow/label, where a rising label reads oddly);
   data-reveal-delay (ms, applied as an inline transition-delay by the
   JS itself) staggers a group of these within the same section.
   Per direct follow-up feedback ("animations feel too stacked, make
   sure they are smooth not jumpy") the travel distance is much
   shorter (was 28px — a big translate watched WHILE actively
   scrolling is what read as a "jump"), the duration is shorter too,
   and it now uses plain ease-out instead of this site's usual
   --ease-out (that curve is tuned snappy/fast-start for click
   micro-interactions elsewhere, which only amplified the jumpiness
   here). js/scroll-reveal.js's own rootMargin was also widened so the
   reveal starts before the element is actually on-screen, letting it
   finish settling by the time it's actually seen instead of being
   caught mid-motion. */
[data-reveal]{
  opacity:0;
  transform:translateY(14px);
  transition:opacity .6s ease-out, transform .6s ease-out;
}
[data-reveal="fade"]{
  transform:none;
}
[data-reveal].is-revealed{
  opacity:1;
  transform:none;
}

/* ===================================================================
   CUSTOM CURSOR — elegant, lagging "drag" feel
=================================================================== */
.custom-cursor{
  position:fixed;
  top:0; left:0;
  width:0; height:0;
  z-index:2000;
  pointer-events:none;
  opacity:0;
  transition:opacity .4s var(--ease-out);
}
/* only revealed once the loader has fully finished (see loader.js) */
.custom-cursor.is-ready{ opacity:1; }
.custom-cursor.is-ready.is-hidden{ opacity:0; }
/* fades away if the pointer sits still for a while */
.custom-cursor.is-ready.is-idle{ opacity:0; transition:opacity 1s ease; }
/* hidden specifically over the process-room's own water, which has its
   own cursor-follow ripple — see js/process-room.js's own mouseenter/
   mouseleave listeners on the section container */
.custom-cursor.is-ready.is-suppressed{ opacity:0; }
.cursor-ring{
  position:absolute;
  top:0; left:0;
  width:36px; height:36px;
  margin:-18px 0 0 -18px;
  border-radius:50%;
  border:1px solid rgba(243,237,226,.7);
  transition:width .35s var(--ease-out), height .35s var(--ease-out), margin .35s var(--ease-out), border-color .35s var(--ease-out), opacity .3s ease;
  /* a slow, subtle breathing glow rather than a flat static shadow —
     something about the cursor is always gently alive as it travels
     around the site, not just when it's over something interactive */
  animation:cursorGlowPulse 2.6s ease-in-out infinite;
}
@keyframes cursorGlowPulse{
  0%, 100%{ box-shadow:0 0 16px rgba(255,221,122,.3); }
  50%{ box-shadow:0 0 28px rgba(255,221,122,.55); }
}
.cursor-dot{
  position:absolute;
  top:0; left:0;
  width:5px; height:5px;
  margin:-2.5px 0 0 -2.5px;
  border-radius:50%;
  background:var(--cream);
}
.custom-cursor.is-active .cursor-ring{
  width:64px; height:64px;
  margin:-32px 0 0 -32px;
  border-color:#ffdd7a;
  /* brighter, faster pulse while hovering something interactive —
     the glow itself calling out that this is clickable */
  animation:cursorGlowPulseActive 1.3s ease-in-out infinite;
}
@keyframes cursorGlowPulseActive{
  0%, 100%{ box-shadow:0 0 24px rgba(255,221,122,.55); }
  50%{ box-shadow:0 0 40px rgba(255,221,122,.85); }
}
/* Awwwards personality pass — site-wide cursor language (see
   js/cursor.js's generic [data-cursor] hook). Two variants, each a
   distinct affordance rather than the one generic "clickable" ring
   everything else on the site already uses:
   - .is-drag (Live Demo / Testimonials swipe rows): the ring itself
     becomes a horizontal pill, echoing the drag gesture rather than a
     point-and-click one.
   - .is-view (Live Demo browser cards): a quiet color-only shift to
     the glitch-highlight blue already established elsewhere on the
     site, deliberately NOT scaled up like .is-active — these cards
     aren't themselves clickable, so the cursor shouldn't claim they are. */
.custom-cursor.is-drag .cursor-ring{
  width:74px; height:34px;
  margin:-17px 0 0 -37px;
  border-radius:999px;
  border-color:#ffdd7a;
}
.custom-cursor.is-view .cursor-ring{
  border-color:#fff28f;
  animation:cursorGlowPulseActive 1.3s ease-in-out infinite;
}

@media (hover:none), (pointer:coarse){
  .custom-cursor{ display:none; }
  body, a{ cursor:auto; }
}

/* ===================================================================
   TESTIMONIALS — businesses across industries, swipeable left/right.
   Deliberately NOT a tall scroll-hijacked section like the showcase
   and cube — a visitor shouldn't have to scroll through all of them
   vertically just to reach the form below. Browsing them (horizontal
   swipe/drag) and moving down the page (vertical scroll) are now two
   fully independent motions. Still dark, with the cube section's
   amber accent, bookending the light showcase in between.
=================================================================== */
.testimonials-section{
  position:relative;
  z-index:1;
  /* per direct request: "there also seems to be waves around some
     sections, looks like they are like gradient layers, please remove
     that from all sections" — was a radial-gradient ellipse peaking at
     top-center; every section down the page had its own copy of this,
     so scrolling past each one's own bright-center/dark-edge boundary
     read as a repeating wave. Flattened to the same solid tone the old
     gradient's own darkest stop already used, so the overall darkness
     is unchanged but there's no curvature left to read as a wave */
  background:#07060a;
  padding:clamp(3.5rem, 8vw, 6rem) 0;
  overflow:hidden;
}
/* same ambient-drift convention as .comparison-section::before (see its
   own comment) — different position/duration/delay so the two don't
   read as an identical repeating pattern */
.testimonials-section::before{
  content:'';
  position:absolute;
  inset:-15% -10%;
  z-index:0;
  pointer-events:none;
  background:radial-gradient(circle at 72% 30%, rgba(255,221,122,.08), transparent 60%);
  opacity:.7;
  animation:ambientGlowDrift 26s ease-in-out infinite;
  animation-delay:-6s;
  will-change:opacity;
}

/* opacity/transform driven directly, continuously, by scroll position
   in testimonials.js as the section enters from below — not a fixed-
   duration animation triggered once, for the same reason as the cube
   title and quote form: it can never finish playing before the
   visitor actually scrolls to it, and reverses cleanly scrolling back up */
.testimonials-sticky{
  position:relative;
  z-index:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:clamp(1.2rem, 3vw, 2rem);
  padding:0 1.5rem;
  opacity:0;
  will-change:opacity, transform;
}

.testimonials-eyebrow{
  position:relative;
  z-index:1;
  font-size:.72rem;
  letter-spacing:.4em;
  text-transform:uppercase;
  color:rgba(228,205,154,.65);
}
/* its own font on purpose — everything else on the site is Fraunces
   (headings) or Inter (body); Space Grotesk's confident, slightly
   geometric character gives this one section its own distinct voice */
.testimonials-heading{
  position:relative;
  z-index:1;
  font-family:'Space Grotesk', 'Inter', sans-serif;
  font-weight:700;
  letter-spacing:-.01em;
  font-size:clamp(2rem, 5vw, 3.4rem);
  background:linear-gradient(100deg, var(--cream) 30%, #ffdd7a 65%, #ffb400 100%);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  text-align:center;
  margin-bottom:.5rem;
}
/* each word signs itself in on its own slight rotation, cascading
   left to right — a flourish rather than the whole line just fading
   in flat. Driven per-frame from testimonials.js, tied to scroll.
   Solid gold rather than inheriting the parent's transparent/
   background-clip gradient — that clip only masks the ancestor
   element's own text runs, so a wrapped span with no background of
   its own would just render invisible (inherited color:transparent
   with nothing to clip against). */
.testimonials-heading-word{
  display:inline-block;
  color:#ffdd7a;
  opacity:0;
  will-change:opacity, transform;
}
/* a real mic icon (not an emoji) riding in right after "say" — solid
   gold like the word beside it, same reason as .testimonials-heading-
   word above: the parent's gradient text-clip only masks its own text
   runs, so this needs its own explicit color rather than inheriting
   the transparent fill */
.testimonials-say-icon{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:.72em;
  height:.72em;
  margin-left:.32em;
  vertical-align:-.06em;
  color:#ffb400;
  opacity:0;
  will-change:opacity, transform;
}
.testimonials-say-icon svg{ width:100%; height:100%; }

.testimonials-hint{
  position:relative;
  z-index:1;
  display:flex;
  align-items:center;
  gap:.4em;
  font-size:.78rem;
  color:rgba(243,237,226,.45);
  margin-top:-.5rem;
}
/* a small continuous side-to-side nudge on the icon itself — reinforces
   the swipe affordance for as long as the section is on screen, on top
   of the one-time actual-scroll nudge in testimonials.js */
.testimonials-hint-icon{
  display:inline-block;
  animation:testimonialHintNudge 1.6s ease-in-out infinite;
}
@keyframes testimonialHintNudge{
  0%, 100%{ transform:translateX(0); }
  50%{ transform:translateX(3px); }
}

/* the swipeable row itself — native horizontal scroll with snap, so
   touch drag/swipe just works with no JS needed for the gesture */
.testimonial-stack{
  position:relative;
  z-index:1;
  width:100%;
  max-width:1100px;
  display:flex;
  gap:clamp(1rem, 2.5vw, 1.6rem);
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  padding:.5rem clamp(1.5rem, 8vw, 12vw) 1.5rem;
  /* fades each card out toward the edges as it scrolls past, rather
     than an abrupt clip at the row's own bounding box — same
     mask-image technique as .mock-services-marquee */
  -webkit-mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.testimonial-stack::-webkit-scrollbar{ display:none; }

.testimonial-card{
  position:relative;
  overflow:hidden;
  flex:0 0 auto;
  width:min(80vw, 420px);
  scroll-snap-align:center;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  padding:clamp(1.4rem, 3vw, 2.4rem);
  border-radius:22px;
  background:linear-gradient(165deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
  border:1px solid rgba(255,221,122,.16);
  box-shadow:0 40px 90px -30px rgba(0,0,0,.6), inset 0 1px 30px rgba(0,0,0,.28);
  opacity:.5;
  transform:scale(.94);
  transition:opacity .35s var(--ease-out), transform .35s var(--ease-out), box-shadow .35s var(--ease-out);
}
/* same soft-reflection convention as .live-demo-browser::after (see its
   own comment) — this element has no 3D preserve-3d to protect, so
   overflow:hidden + a contained top-down gradient is safe here */
.testimonial-card::after{
  content:'';
  position:absolute;
  inset:0 0 auto 0;
  height:35%;
  background:linear-gradient(180deg, rgba(243,237,226,.05), transparent);
  pointer-events:none;
}
.testimonial-card.is-active{
  opacity:1;
  transform:scale(1);
  box-shadow:0 46px 110px -26px rgba(0,0,0,.7), 0 0 0 1px rgba(255,221,122,.22);
}

.testimonial-icon{ font-size:2.2rem; margin-bottom:.5rem; }
.testimonial-industry{
  font-size:.72rem;
  letter-spacing:.32em;
  text-transform:uppercase;
  color:#ffdd7a;
  margin-bottom:.9rem;
}
.testimonial-quote{
  /* was Fraunces italic — a display serif in italic reads fine as a
     couple of words but got noticeably harder to read at full
     paragraph length; a plain sans-serif body face carries the actual
     review text much more comfortably */
  font-family:'Inter', sans-serif;
  font-weight:400;
  font-size:clamp(1.05rem, 2.2vw, 1.3rem);
  line-height:1.55;
  color:var(--cream);
  max-width:44ch;
  margin-bottom:1rem;
}
/* the "encouraging" beat: a concrete positive result, styled apart
   from the quote so it reads like a quick win at a glance */
.testimonial-result{
  display:inline-flex;
  align-items:center;
  gap:.4em;
  font-size:.8rem;
  font-weight:600;
  letter-spacing:.01em;
  color:#8CE99A;
  background:rgba(140,233,154,.12);
  border:1px solid rgba(140,233,154,.3);
  border-radius:999px;
  padding:.4em .9em;
  margin-bottom:1rem;
}
.testimonial-result::before{ content:'✓'; font-weight:700; }
/* Awwwards personality pass — a brief glitch-spark on the result badge
   right as its card becomes active, same highlight color (#fff28f) the
   hero/FAQ glitch moments already use. Border/glow only — the badge's
   own green stays green throughout, since that color carries real
   "verified result" meaning and shouldn't flicker away. Triggered by
   js/testimonials.js's updateActive(); self-removes on animationend. */
.testimonial-result.is-glitching{
  animation:testimonialResultGlitch .5s steps(2, end);
}
@keyframes testimonialResultGlitch{
  0%, 100%{ box-shadow:none; border-color:rgba(140,233,154,.3); }
  30%{ box-shadow:0 0 8px rgba(255,242,143,.55); border-color:#fff28f; }
  60%{ box-shadow:none; border-color:rgba(140,233,154,.3); }
  80%{ box-shadow:0 0 6px rgba(255,242,143,.4); border-color:#fff28f; }
}
.testimonial-stars{
  color:#ffdd7a;
  letter-spacing:.25em;
  font-size:.85rem;
  margin-bottom:.7rem;
}
.testimonial-name{
  font-size:.85rem;
  color:rgba(243,237,226,.7);
}
.testimonial-name span{ color:rgba(243,237,226,.45); }

.testimonials-controls{
  position:relative;
  z-index:1;
  display:flex;
  align-items:center;
  gap:1.4rem;
}
.testimonial-arrow{
  display:flex;
  align-items:center;
  justify-content:center;
  width:38px; height:38px;
  border-radius:50%;
  border:1px solid rgba(243,237,226,.25);
  background:transparent;
  color:rgba(243,237,226,.8);
  cursor:none;
  transition:border-color .3s var(--ease-out), color .3s var(--ease-out), background .3s var(--ease-out);
}
.testimonial-arrow:hover{ border-color:#ffdd7a; color:#ffdd7a; background:rgba(255,221,122,.08); }
@media (hover:none), (pointer:coarse){ .testimonial-arrow{ display:none; } }

.testimonial-dots{
  display:flex;
  gap:.55rem;
  align-items:center;
}
.testimonial-dot{
  width:8px;
  height:8px;
  border-radius:50%;
  border:none;
  padding:0;
  background:rgba(243,237,226,.25);
  cursor:none;
  transition:background .35s var(--ease-out), width .35s var(--ease-out);
}
.testimonial-dot:hover{ background:rgba(243,237,226,.5); }
.testimonial-dot.is-active{
  width:22px;
  border-radius:5px;
  background:#ffdd7a;
}

/* ===================================================================
   COMPARISON — a line chart, an outdated site vs a Papi-built one,
   across a few common metrics. Both lines draw themselves in
   left-to-right once the chart scrolls into view (the Papi line
   trailing the outdated one by a beat, so the motion itself tells the
   "overtakes it" story), data points fade in roughly as the line
   reaches them, and the stat percentages count up alongside. A big
   "5X" headline stat sits beside the chart with its own scale/fade
   reveal, "More results" fading in a beat after it.
=================================================================== */
.comparison-section{
  position:relative;
  /* per direct request: "there also seems to be waves around some
     sections, looks like they are like gradient layers, please remove
     that from all sections" — was a radial-gradient ellipse peaking at
     top-center; every section down the page had its own copy of this,
     so scrolling past each one's own bright-center/dark-edge boundary
     read as a repeating wave. Flattened to the same solid tone the old
     gradient's own darkest stop already used, so the overall darkness
     is unchanged but there's no curvature left to read as a wave */
  background:#07060a;
  padding:clamp(4rem, 10vw, 7rem) clamp(1.5rem, 5vw, 4.5rem);
  overflow:hidden;
}
@keyframes ambientGlowDrift{
  0%, 100%{ opacity:.5; transform:translate(0, 0); }
  50%{ opacity:.9; transform:translate(2%, -2%); }
}
@media (prefers-reduced-motion:reduce){
  .testimonials-section::before,
  .faq-section::before{ animation:none; }
  /* Awwwards personality pass — FAQ scan sweep, chevron glitch-spark,
     and answer depth-echo all skip straight to their final state */
  .faq-question::after{ display:none; }
  .faq-chevron.is-glitching{ animation:none; }
  .faq-answer p{ transition:none; opacity:1; transform:none; }
  .testimonial-result.is-glitching{ animation:none; }
}
.comparison-inner{
  position:relative;
  z-index:1;
  max-width:900px;
  margin:0 auto;
  text-align:center;
}
.comparison-eyebrow{
  font-size:.76rem;
  letter-spacing:.38em;
  text-transform:uppercase;
  color:#ffdd7a;
  margin-bottom:1rem;
}
.comparison-title{
  font-family:'Fraunces', serif;
  font-weight:400;
  font-size:clamp(1.9rem, 4.4vw, 2.8rem);
  color:var(--cream);
  line-height:1.15;
}
.comparison-title em{
  font-style:italic;
  color:#ffdd7a;
  /* a subtle pulsing text-shadow glow, tuned to this heading's own
     light blue (#ffdd7a) */
  animation:comparisonGlowPulse 3.6s ease-in-out infinite;
}
@keyframes comparisonGlowPulse{
  0%, 100%{
    text-shadow:
      0 0 9px rgba(255,221,122,.4),
      0 0 20px rgba(255,221,122,.24),
      0 0 36px rgba(255,221,122,.14);
  }
  50%{
    text-shadow:
      0 0 13px rgba(255,221,122,.62),
      0 0 28px rgba(255,221,122,.4),
      0 0 48px rgba(255,221,122,.24);
  }
}
.comparison-sub{
  max-width:520px;
  margin:1rem auto 0;
  color:rgba(243,237,226,.68);
  font-size:1rem;
  line-height:1.5;
}
.comparison-legend{
  display:flex;
  justify-content:center;
  gap:1.75rem;
  margin:2rem 0 3rem;
  font-size:.85rem;
  color:rgba(243,237,226,.75);
}
.comparison-legend-item{ display:inline-flex; align-items:center; gap:.5rem; }
.comparison-dot{ width:9px; height:9px; border-radius:50%; display:inline-block; }
.comparison-dot--old{ background:rgba(243,237,226,.3); }
.comparison-dot--papi{ background:linear-gradient(135deg, #ffd23f, #cba869); }

/* the chart and the big "5X" headline stat sit side by side — the
   chart takes the wider share, the stat lands toward the right-center
   of the row. Wraps to a stacked column on narrow screens (chart's own
   min-width forces the wrap before either one gets uncomfortably
   cramped) rather than needing a separate mobile layout. */
.comparison-content{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:clamp(2.5rem, 6vw, 5rem);
  margin-top:1rem;
  flex-wrap:wrap;
}
.comparison-chart-wrap{
  flex:1 1 420px;
  max-width:600px;
  min-width:280px;
}

/* aspect-ratio here has to exactly match the svg's own viewBox ratio
   (640:260) — combined with preserveAspectRatio="none" below, that's
   what lets the percentage-based .comparison-value overlays line up
   with the line's actual data points regardless of how large the
   chart itself renders */
.comparison-svg-box{
  position:relative;
  width:100%;
  aspect-ratio:640/260;
}
/* Awwwards personality pass — a faint one-shot scanline sweep across
   the chart as it reveals, echoing the hero's own digit-rain/scan
   motif. Purely CSS, triggered by the .is-visible class
   js/comparison-chart.js already sets on reveal — no JS change.
   Deliberately no overflow:hidden on the parent box: the papi line's
   own drop-shadow glow (.comparison-line--papi) and the value labels'
   translate(-50%,-160%) both already rely on painting outside this
   box's exact rect, and clipping that would visibly cut them off. The
   sweep gradient is soft-edged, so it reads fine without a hard clip. */
.comparison-svg-box::after{
  content:'';
  position:absolute;
  inset:0;
  z-index:2;
  background:linear-gradient(100deg, transparent 42%, rgba(255,221,122,.14) 50%, transparent 58%);
  transform:translateX(-120%);
  pointer-events:none;
}
.comparison-svg-box.is-visible::after{
  animation:comparisonScanSweep 1s var(--ease-out) .3s;
}
@keyframes comparisonScanSweep{
  from{ transform:translateX(-120%); }
  to{ transform:translateX(120%); }
}
.comparison-svg{ width:100%; height:100%; display:block; overflow:visible; }
.comparison-baseline{ stroke:rgba(243,237,226,.14); stroke-width:1.5; }
.comparison-line{ fill:none; stroke-linecap:round; stroke-linejoin:round; }
.comparison-line--old{ stroke:rgba(243,237,226,.4); stroke-width:3; }
.comparison-line--papi{
  stroke:#ffd23f;
  stroke-width:4;
  filter:drop-shadow(0 0 8px rgba(255,210,63,.5));
}
/* a slow pulsing glow once the line's drawn in — the "live" bit:
   something about the chart keeps moving on its own rather than
   sitting static once revealed (was a shimmer sweeping up the bar;
   same intent, adapted to a line) */
.comparison-svg-box.is-visible .comparison-line--papi{
  animation:comparisonLineGlow 3s ease-in-out 1.7s infinite;
}
@keyframes comparisonLineGlow{
  0%, 100%{ filter:drop-shadow(0 0 8px rgba(255,210,63,.45)); }
  50%{ filter:drop-shadow(0 0 15px rgba(255,210,63,.8)); }
}
/* transform-box:fill-box makes transform-origin:center scale from
   each circle's own middle — SVG shapes otherwise scale from the
   *viewport's* origin by default, not their own, which would have
   made them balloon outward from the top-left corner instead of
   simply growing in place */
.comparison-dot-group circle{
  opacity:0;
  transform-box:fill-box;
  transform-origin:center;
  transform:scale(.4);
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.comparison-dot-group--old circle{ fill:rgba(243,237,226,.55); }
.comparison-dot-group--papi circle{ fill:#fff6d8; stroke:#ffd23f; stroke-width:2; }
.comparison-svg-box.is-visible .comparison-dot-group circle{ opacity:1; transform:scale(1); }
/* staggered so each pair of dots appears roughly as the line actually
   reaches that point, not all three at once */
.comparison-dot-group--old circle:nth-child(1), .comparison-dot-group--papi circle:nth-child(1){ transition-delay:.35s; }
.comparison-dot-group--old circle:nth-child(2), .comparison-dot-group--papi circle:nth-child(2){ transition-delay:.95s; }
.comparison-dot-group--old circle:nth-child(3), .comparison-dot-group--papi circle:nth-child(3){ transition-delay:1.55s; }

.comparison-value{
  position:absolute;
  transform:translate(-50%, -160%);
  font-family:'Fraunces', serif;
  font-weight:600;
  font-size:1rem;
  color:#ffdd7a;
  white-space:nowrap;
  opacity:0;
  transition:opacity .5s var(--ease-out);
}
.comparison-svg-box.is-visible .comparison-value{ opacity:1; }
.comparison-value:nth-of-type(1){ transition-delay:.35s; }
.comparison-value:nth-of-type(2){ transition-delay:.95s; }
.comparison-value:nth-of-type(3){ transition-delay:1.55s; }

.comparison-axis{
  display:flex;
  margin-top:.5rem;
  font-size:.82rem;
  color:rgba(243,237,226,.6);
}
.comparison-axis span{ flex:1; text-align:center; }
.comparison-axis span:first-child{ text-align:left; }
.comparison-axis span:last-child{ text-align:right; }

/* the big headline stat — "5X" scales/fades in, then "More results"
   fades in a beat after via its own transition-delay, per the brief:
   a big number reveal followed by a fade-reveal of supporting copy */
.comparison-stat{
  flex:0 0 auto;
  text-align:center;
}
.comparison-stat-number{
  position:relative; /* anchors the ::before/::after echo rings below */
  font-family:'Fraunces', serif;
  font-weight:600;
  font-size:clamp(4.5rem, 11vw, 8rem);
  line-height:1;
  background:linear-gradient(180deg, #fff6d8 0%, #ffd23f 45%, #cba869 100%);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  opacity:0;
  transform:translateY(24px) scale(.85);
  transition:opacity .7s var(--ease-out), transform .7s var(--ease-out);
}
.comparison-stat-number span{ font-size:.7em; }
/* two "echoes" of the number itself — same gradient-clipped text,
   stacked directly on top of the real one via ::before/::after — that
   continuously zoom in and fade out, staggered half a cycle apart so
   there's always one mid-pulse. The real "5X" text stays put and
   fully legible throughout; only these copies pulse outward from it,
   the way an echo radiates from a fixed source rather than the source
   itself fading. Hardcoding the content here (rather than deriving it
   from the real text) is fine since this number is static copy, not
   dynamic data. */
.comparison-stat-number::before,
.comparison-stat-number::after{
  content:'5X';
  position:absolute;
  inset:0;
  background:inherit;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  pointer-events:none;
  opacity:0;
}
/* BUG FIX: per report, "the shining animation on the numbers section
   seems to pause after the first animation." Confirmed directly via
   the Web Animations API (document.getAnimations()) that this never
   actually stops or freezes — currentTime keeps advancing, playState
   stays 'running', forever. The real issue was ease-out: it front-
   loads all the visible motion into roughly the first third of each
   3s cycle, fading to near-zero opacity for the remaining two-thirds
   — technically still animating, but imperceptible for so long each
   cycle that it reads as "played once, then stopped." ease-in-out
   spreads the visible motion evenly across the whole cycle instead
   (matching every other "shine/glow" animation on the site, which are
   all ease-in-out already — this one was the one outlier on ease-out). */
.comparison-stat.is-visible .comparison-stat-number::before{
  animation:comparisonStatEcho 3s ease-in-out 1s infinite;
}
.comparison-stat.is-visible .comparison-stat-number::after{
  animation:comparisonStatEcho 3s ease-in-out 2.5s infinite;
}
@keyframes comparisonStatEcho{
  0%{ opacity:.5; transform:scale(1); }
  100%{ opacity:0; transform:scale(1.35); }
}
.comparison-stat-label{
  margin-top:.5rem;
  font-size:1rem;
  color:rgba(243,237,226,.7);
  max-width:220px;
  margin-left:auto;
  margin-right:auto;
  opacity:0;
  transition:opacity .6s ease .5s;
}
.comparison-stat.is-visible .comparison-stat-number{ opacity:1; transform:translateY(0) scale(1); }
.comparison-stat.is-visible .comparison-stat-label{ opacity:1; }

/* ===================================================================
   FAQ — light-blue-on-black accordion, one answer open at a time.
=================================================================== */
.faq-section{
  position:relative;
  /* BUG FIX: per request to make the site feel like one continuous
     world rather than separate sections — this was the one plain #000
     seam in an otherwise identical #07060a background shared by the
     hero/live-demo/process/comparison/testimonials sections. Matching
     it removes the one visible "reset" between sections that isn't
     the quote section's own intentional closing-chapter gradient. */
  background:#07060a;
  padding:clamp(4rem, 10vw, 7rem) clamp(1.5rem, 5vw, 4.5rem);
  /* contains the new ::before ambient glow's inset:-15% bleed within
     this section's own box, same reasoning as .comparison-section */
  overflow:hidden;
}
/* same ambient-drift convention as .comparison-section::before (see its
   own comment) — different position/duration/delay so the three don't
   read as an identical repeating pattern */
.faq-section::before{
  content:'';
  position:absolute;
  inset:-15% -10%;
  z-index:0;
  pointer-events:none;
  background:radial-gradient(circle at 45% 75%, rgba(255,221,122,.07), transparent 58%);
  opacity:.7;
  animation:ambientGlowDrift 24s ease-in-out infinite;
  animation-delay:-13s;
  will-change:opacity;
}
.faq-inner{
  position:relative;
  z-index:1;
  max-width:760px;
  margin:0 auto;
}
.faq-eyebrow{
  text-align:center;
  font-size:.76rem;
  letter-spacing:.38em;
  text-transform:uppercase;
  color:#ffdd7a;
  margin-bottom:1rem;
}
.faq-title{
  text-align:center;
  font-family:'Fraunces', serif;
  font-weight:400;
  font-size:clamp(1.9rem, 4.4vw, 2.8rem);
  color:var(--cream);
  margin-bottom:3rem;
}
.faq-title em{ font-style:italic; color:#ffdd7a; }

/* REVERTED per direct follow-up: "I did not want them to be swipeable,
   I want them to be accordion, but as I scroll one question, it goes
   to the other." Back to a plain vertical accordion list — see
   js/faq.js's own scroll-driven auto-advance for the new "scrolling
   moves through the questions" behavior instead of a swipeable row. */
.faq-item{
  border-bottom:1px solid rgba(255,221,122,.18);
}
.faq-item:first-child{ border-top:1px solid rgba(255,221,122,.18); }
.faq-question{
  position:relative;
  width:100%;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  padding:1.3rem 1.15rem;
  background:none;
  border:none;
  border-radius:16px;
  text-align:left;
  font-family:'Fraunces', serif;
  font-size:clamp(1.02rem, 2vw, 1.2rem);
  color:var(--cream);
  cursor:none;
  overflow:hidden;
  transition:color .25s var(--ease-out);
}
.faq-question:hover{ color:#ffdd7a; }
/* Awwwards personality pass — a faint one-shot scanline sweep on hover,
   echoing the hero's own digit-rain/scan motif at a whisper instead of
   this row staying visually unrelated to the rest of the site. Plain
   CSS animation restarting on :hover — no JS, no infinite loop. */
.faq-question::after{
  content:'';
  position:absolute;
  inset:0;
  background:linear-gradient(100deg, transparent 42%, rgba(255,221,122,.16) 50%, transparent 58%);
  transform:translateX(-120%);
  pointer-events:none;
}
.faq-question:hover::after{
  animation:faqScanSweep .7s var(--ease-out);
}
@keyframes faqScanSweep{
  from{ transform:translateX(-120%); }
  to{ transform:translateX(120%); }
}
.faq-chevron{
  flex:0 0 auto;
  width:10px;
  height:10px;
  border-right:1.5px solid #ffdd7a;
  border-bottom:1.5px solid #ffdd7a;
  transform:rotate(45deg);
  transition:transform .3s var(--ease-out);
}
.faq-item.is-open .faq-chevron{ transform:rotate(-135deg); }
/* Awwwards personality pass — a brief glitch-spark on the chevron right
   as a question opens, same highlight color (#fff28f) the hero's own
   title/CTA glitch-dissolve already established elsewhere on the site.
   Toggled by js/faq.js's openItem(); removed on animationend so it
   never lingers or restarts on its own. */
.faq-chevron.is-glitching{
  animation:faqChevronGlitch .4s steps(2, end);
}
@keyframes faqChevronGlitch{
  0%, 50%, 100%{ border-color:#ffdd7a; filter:none; }
  25%{ border-color:#fff28f; filter:drop-shadow(0 0 4px rgba(255,242,143,.85)); }
  75%{ border-color:#fff28f; filter:drop-shadow(0 0 6px rgba(255,242,143,.85)); }
}
.faq-answer{
  height:0;
  overflow:hidden;
  transition:height .35s var(--ease-out);
}
.faq-answer p{
  padding:0 1.15rem 1.5rem;
  color:rgba(243,237,226,.68);
  font-size:.98rem;
  line-height:1.6;
  /* Awwwards personality pass — a soft depth-echo settle instead of a
     flat height-only reveal, the same eased-arrival feel (fade + tiny
     rise) as scroll-journey-process.js's panel settle, scaled down to a
     few px since this is a whisper of that effect, not the full thing.
     Driven purely by .faq-item.is-open, already toggled by faq.js — no
     JS change needed for this part. */
  opacity:0;
  transform:translateY(6px);
  transition:opacity .3s var(--ease-out) .05s, transform .3s var(--ease-out) .05s;
}
.faq-item.is-open .faq-answer p{
  opacity:1;
  transform:translateY(0);
}

/* ===================================================================
   QUOTE — request-a-quote form. On brand: black and light blue only
   (was gold — changed per direct request). The section behind the
   white card runs a slow, solid (not blurred/translucent like the
   cube's glow) sweep between black and blue — the same "moving
   colors" idea as the cube's background, just more saturated, and
   kept strictly within the brand pair rather than a rainbow. The
   form's own border rotates through black/blue/cream so it always
   reads as contrasted against whatever's behind it. Not a tall sticky
   section like the others: a form should feel direct, not gimmicky.
=================================================================== */
.quote-section{
  position:relative;
  /* the gold stop stays muted/deep rather than bright — the white
     copy text sitting directly on top needs to stay legible through
     the whole sweep, not just the darker parts of it */
  /* per direct request to move the whole site's blue accent back to a
     yellow/gold metallic — this sweep was deliberately kept "strictly
     black→blue" (see the site-wide gold->blue migration this reverses,
     noted on :root above); every stop here is that same swap undone,
     so it now reads black to gold to black instead, same 26s
     cycle/timing untouched. */
  background:linear-gradient(120deg, #0a0908, #443312, #75591a, #443312, #0a0908);
  background-size:400% 400%;
  animation:quoteBgSweep 26s ease-in-out infinite;
  padding:clamp(4rem, 10vw, 8rem) clamp(1.5rem, 5vw, 4rem);
  overflow:hidden;
}
@keyframes quoteBgSweep{
  0%{ background-position:0% 50%; }
  50%{ background-position:100% 50%; }
  100%{ background-position:0% 50%; }
}
/* per a full-site heat audit: this section's own background sweep
   (and the submit button/heading shine inside it, sharing the same
   background-position technique) is paused off-screen by
   js/anim-idle.js's generic .is-anim-idle mechanism (see its own
   #quoteSection group and the shared CSS rule near the top of this
   file) rather than a bespoke rule here — consolidated so there's one
   canonical off-screen-pause system site-wide instead of two. */

.quote-inner{
  position:relative;
  z-index:1;
  max-width:1100px;
  margin:0 auto;
  display:grid;
  grid-template-columns:1fr 1.1fr;
  gap:clamp(2.5rem, 6vw, 5rem);
  align-items:center;
}

/* opacity/transform are driven directly, continuously, by scroll
   position in quote-form.js — not a CSS transition triggered once by
   an IntersectionObserver. A fixed-duration reveal could finish
   playing before or after the visitor's own scroll speed caught up to
   it, which is what read as the form just "slapping" into place;
   tying it straight to scroll position means it's always exactly as
   revealed as how far they've scrolled, at any scroll speed. */
.quote-copy{
  opacity:0;
  will-change:opacity, transform, filter;
}

.quote-eyebrow{
  font-size:.72rem;
  letter-spacing:.36em;
  text-transform:uppercase;
  color:#fff;
  opacity:.85;
  font-weight:600;
  margin-bottom:1rem;
}
/* the heading sweeps through the same palette as the section behind
   it — its own animation, offset in timing so it doesn't just match
   the backdrop 1:1 the whole time */
.quote-heading{
  font-family:'Fraunces', serif;
  font-weight:500;
  font-size:clamp(2rem, 4.4vw, 3.1rem);
  line-height:1.1;
  margin-bottom:1rem;
  background:linear-gradient(100deg, #fff, #ffe27a, #ffd23f, #fff6d8, #fff);
  background-size:300% 100%;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  animation:quoteHeadingSweep 10s ease-in-out infinite;
}
@keyframes quoteHeadingSweep{
  0%{ background-position:0% 50%; }
  50%{ background-position:100% 50%; }
  100%{ background-position:0% 50%; }
}
/* "free" gets its own brighter, faster shine layered on top of the
   heading's own slow overall sweep — a distinct highlight sweeping
   across just that one word rather than blending into the rest */
.quote-heading-shine{
  background:linear-gradient(100deg, #ffd23f 0%, #fff6d8 20%, #fff 40%, #fff6d8 60%, #ffd23f 100%);
  background-size:250% 100%;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  animation:quoteFreeShine 2.4s ease-in-out infinite;
}
@keyframes quoteFreeShine{
  0%{ background-position:150% 50%; }
  100%{ background-position:-50% 50%; }
}
.quote-sub{
  font-size:clamp(1rem, 1.6vw, 1.15rem);
  line-height:1.55;
  color:rgba(255,255,255,.85);
  max-width:44ch;
  margin-bottom:2.4rem;
}

.quote-proof{
  display:flex;
  flex-direction:column;
  gap:1.1rem;
}
.quote-rating{ display:flex; align-items:center; gap:.7rem; }
.quote-stars{ color:#ffd23f; letter-spacing:.15em; font-size:1.05rem; }
.quote-rating-text{ font-size:.9rem; color:rgba(255,255,255,.8); }
.quote-rating-text strong{ color:#fff; }

.quote-social{ display:flex; align-items:center; gap:.9rem; }
.quote-social-label{ font-size:.85rem; color:rgba(255,255,255,.8); }
.quote-social-links{ display:flex; gap:.6rem; }
.quote-social-link{
  display:flex;
  align-items:center;
  justify-content:center;
  width:38px; height:38px;
  border-radius:50%;
  overflow:hidden;
  box-shadow:0 8px 20px -8px rgba(0,0,0,.35);
  transition:transform .3s var(--ease-out), box-shadow .3s var(--ease-out);
}
.quote-social-link:hover{
  transform:translateY(-3px) scale(1.06);
  box-shadow:0 14px 26px -8px rgba(0,0,0,.45);
}
.quote-social-link svg{ display:block; }

/* ---- the form itself ---- */
/* used to be wrapped in an oversized rotating conic-gradient clipped
   down to a slim animated-rainbow-gold ring (the classic animated-
   gradient-border trick) — removed outright. That wrap's own rounded-
   corner clipping needing to track the swipeable stack's height
   (itself resized in JS whenever the visitor swaps between the quote
   and call-back panels) was the actual root of the recurring "gold bar"
   glitch: any brief mismatch between the two exposed this wrap's own
   gold background in the gap. With no gold background left to expose,
   there's nothing left for that gap to reveal — the form's own white
   background (on .quote-form-tabs/.quote-form-stack below) is what's
   actually visible now, full stop. */
/* opacity/transform driven directly by scroll position — see the note
   on .quote-copy above */
.quote-form-wrap{
  position:relative;
  opacity:0;
  will-change:opacity, transform;
  border-radius:23px;
  /* same blue-tinted border as .newsletter-popup-card/.process-panel —
     wrapped around the tabs+stack pair here (rather than on each
     separately) so there's one border, not a visible seam where two
     borders would otherwise meet at the tabs/stack boundary */
  border:1px solid rgba(255,221,122,.16);
  /* a plain, static shadow for depth — not a "container background",
     just enough definition that the card doesn't look like it's
     floating flat against the page */
  box-shadow:0 40px 80px -40px rgba(0,0,0,.4);
}

/* two panels — the original quote request, and a call-back request —
   swipeable like the testimonials row (native horizontal scroll-snap,
   so touch drag just works), with tabs above as the click/keyboard
   equivalent and to show which panel is active */
.quote-form-tabs{
  position:relative;
  z-index:1;
  display:flex;
  background:#08080b;
  border-radius:23px 23px 0 0;
  overflow:hidden;
}
.quote-form-tab{
  flex:1;
  padding:.9rem 1rem;
  border:none;
  border-bottom:2px solid transparent;
  background:transparent;
  font-family:'Inter', sans-serif;
  font-weight:600;
  font-size:.86rem;
  letter-spacing:.01em;
  color:var(--cream-dim);
  cursor:none;
  transition:color .25s var(--ease-out), background .25s var(--ease-out), border-color .25s var(--ease-out);
}
.quote-form-tab.is-active{
  color:var(--cream);
  background:#0d0d10;
  border-bottom-color:#ffdd7a;
}

.quote-form-stack{
  position:relative;
  z-index:1;
  display:flex;
  /* fixed to whichever panel is tallest (see quote-form.js) rather
     than resized to match whichever is currently active — resizing
     per-panel shrank the whole section in normal document flow every
     time the visitor swiped, which shifted everything below the form
     (the closing Papi signature) up and down on every swipe, on top of
     occasionally exposing a gap at the card's own rounded corners. A
     background here (rather than leaving this transparent) means the
     shorter call-back panel's leftover space below it just reads as
     more white card, not a gap into whatever is behind the section. */
  align-items:flex-start;
  height:0;
  background:#0d0d10;
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  border-radius:0 0 23px 23px;
}
.quote-form-stack::-webkit-scrollbar{ display:none; }
.quote-form-panel{
  flex:0 0 100%;
  min-width:100%;
  scroll-snap-align:start;
}

.quote-form{
  position:relative;
  z-index:1;
  background:#0d0d10;
  padding:clamp(1.6rem, 3vw, 2.4rem);
  display:flex;
  flex-direction:column;
  gap:1.3rem;
}

/* a bright blue sweep for a premium feel — brightens toward the
   middle (not darker bronze/gold stops) so it still reads clearly
   against the form's now-dark background, with a highlight band
   passing through periodically like light catching brushed metal */
.quote-form-title{
  font-family:'Fraunces', serif;
  font-weight:600;
  font-size:clamp(1.3rem, 2.6vw, 1.7rem);
  text-align:center;
  margin:0 0 -.3rem;
  background:linear-gradient(100deg, #ffd23f 0%, #fff6d8 25%, #fff 50%, #fff6d8 75%, #ffd23f 100%);
  background-size:250% 100%;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  animation:quoteFormTitleShine 6s ease-in-out infinite;
}
@keyframes quoteFormTitleShine{
  0%{ background-position:0% 50%; }
  50%{ background-position:100% 50%; }
  100%{ background-position:0% 50%; }
}

.quote-row{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:1.3rem;
}

/* grid items default to a min-width based on their content's own
   intrinsic size, not the track's — a native date input's internal
   day/month/year + calendar-icon layout on iOS has a wide enough
   intrinsic minimum that it could force its whole grid track past the
   form's own edge instead of shrinking to fit, which is what showed up
   as the date field sticking out past the form on iPhone */
.quote-field{ position:relative; min-width:0; }
.quote-field input,
.quote-field textarea,
.quote-field select{
  width:100%;
  min-width:0;
  font-family:'Inter', sans-serif;
  font-size:1rem;
  color:var(--cream);
  background:rgba(243,237,226,.05);
  border:1.5px solid rgba(243,237,226,.14);
  border-radius:12px;
  padding:1.1rem .9rem .5rem;
  transition:border-color .3s var(--ease-out), box-shadow .3s var(--ease-out), background .3s var(--ease-out);
  resize:none;
}
.quote-field textarea{ padding-top:1.3rem; }
.quote-field input:focus,
.quote-field textarea:focus,
.quote-field select:focus{
  outline:none;
  border-color:#ffdd7a;
  background:rgba(243,237,226,.08);
  /* restored to its original gold value (rgba(202,153,20,.16)), per
     the site-wide blue->gold reversal on :root — this was a bare
     rgba() rather than a var(--gold...) token, so it needed its own
     explicit swap-back too */
  box-shadow:0 0 0 4px rgba(202,153,20,.16);
}
.quote-field label{
  position:absolute;
  left:.95rem;
  top:.95rem;
  font-size:1rem;
  color:var(--cream-dim);
  pointer-events:none;
  transform-origin:left top;
  transition:transform .22s var(--ease-out), color .22s var(--ease-out), top .22s var(--ease-out);
}
.quote-field label span{ color:rgba(207,199,184,.7); }
.quote-field input:focus + label,
.quote-field input:not(:placeholder-shown) + label,
.quote-field textarea:focus + label,
.quote-field textarea:not(:placeholder-shown) + label{
  top:.4rem;
  transform:scale(.72);
  color:#ffdd7a;
}

/* the business/industry field — a native <select> (its options scroll
   on both a phone's native wheel picker and a desktop dropdown) rather
   than a free-text field, so a visitor picks their trade from a fixed
   list instead of typing it out. Selects don't support
   :placeholder-shown, so the floated-label state is driven by a
   .has-value class toggled in quote-form.js instead. */
.quote-field--select select{
  appearance:none;
  -webkit-appearance:none;
  cursor:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cfc7b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right .9rem center;
  background-size:16px;
  padding-right:2.2rem;
}
.quote-field--select select:invalid{ color:var(--cream-dim); }
.quote-field--select select.has-value{ color:var(--cream); }
.quote-field--select select:focus + label,
.quote-field--select select.has-value + label{
  top:.4rem;
  transform:scale(.72);
  color:#ffdd7a;
}

/* the preferred-call-date field — a native date input so tapping it
   opens the platform's own calendar (day-of-month picker) rather than
   free-text. Like <select>, it doesn't support :placeholder-shown, so
   the floated-label state is driven by the same .has-value class
   (toggled in quote-form.js) instead.
   overflow:hidden is load-bearing here, not decorative — iOS Safari's
   native date control is a shadow-DOM layout of separate day/month/
   year segments plus the calendar glyph, and on a narrow field it can
   render wider than the input's own box without the browser clipping
   it on its own; only overflow:hidden directly on the input forces
   that shadow content back inside the border-box, which is what
   showed up as the control spilling out past the form's edge. */
/* color-scheme:dark so the native calendar-icon/day-month-year chrome
   (browser-drawn, not something this stylesheet can reach directly)
   renders light-on-dark instead of assuming a light field and drawing
   itself barely-visible dark-on-dark */
.quote-field--date input{ color:var(--cream); overflow:hidden; color-scheme:dark; }
.quote-field--date input:focus + label,
.quote-field--date input.has-value + label{
  top:.4rem;
  transform:scale(.72);
  color:#ffdd7a;
}

.quote-submit{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:.5rem;
  padding:1rem 1.5rem;
  border:none;
  border-radius:14px;
  background:linear-gradient(120deg, #8a6a1f, #ffd23f, #cba869, #8a6a1f);
  background-size:300% 100%;
  animation:quoteBgSweep 8s ease-in-out infinite;
  color:#0a0908;
  font-size:1rem;
  font-weight:700;
  letter-spacing:.01em;
  cursor:none;
  overflow:hidden;
  transition:transform .3s var(--ease-out), box-shadow .3s var(--ease-out);
  /* restored to its original gold glow (see the .quote-field:focus
     box-shadow fix above for the same reversal) */
  box-shadow:0 16px 34px -14px rgba(138,106,31,.6);
}
.quote-submit:hover{ transform:translateY(-2px); box-shadow:0 20px 40px -14px rgba(138,106,31,.7); }
.quote-submit:active{ transform:translateY(0) scale(.98); }
.quote-submit-check{
  display:none;
}

.quote-form.is-sending .quote-submit{ opacity:.75; pointer-events:none; }
.quote-form.is-sent .quote-submit-label{ display:none; }
.quote-form.is-sent .quote-submit-check{ display:block; }
.quote-form.is-sent .quote-submit{ background:linear-gradient(135deg, #34c77b, #1fa863); color:#fff; animation:none; }

.quote-success{
  font-size:.9rem;
  color:#1fa863;
  text-align:center;
  opacity:0;
  max-height:0;
  overflow:hidden;
  transition:opacity .4s var(--ease-out), max-height .4s var(--ease-out), color .3s var(--ease-out);
}
.quote-form.is-sent .quote-success{ opacity:1; max-height:60px; }
.quote-form.is-error .quote-success{ opacity:1; max-height:60px; color:#e0483e; }

@media (max-width:860px){
  .quote-inner{ grid-template-columns:1fr; }
  .quote-copy{ text-align:center; }
  .quote-proof{ align-items:center; }
  .quote-rating{ justify-content:center; }
  .quote-social{ justify-content:center; }
  .quote-sub{ margin-left:auto; margin-right:auto; }
}

@media (max-width:560px){
  .quote-row{ grid-template-columns:1fr; }
}

/* ===================================================================
   NEWSLETTER POPUP — shown once, the first time the visitor scrolls
   past the live-demo section (see js/newsletter-popup.js). The form
   itself reuses .quote-field/.quote-submit/.quote-form wholesale (a
   white-background card, same as the quote form further down the
   page) rather than a second parallel set of input styles.

   REDESIGNED per direct request: "I do not want the pop up to just
   pop up and cover the screen... something that is immersive, doesnt
   take the entire page, something sleek, small, but powerful." Was a
   full-viewport dark backdrop + centered modal; now a small card
   anchored to the bottom-right corner (bottom sheet on mobile) that
   slides up + fades in on its own, with nothing dimming or blocking
   the rest of the page — no backdrop element at all anymore, and
   js/newsletter-popup.js no longer calls lockScroll()/unlockScroll().

   RECOLORED per direct request: "white is not a good color for the
   theme of the website" — was a plain white card (a leftover from
   before this site moved to its current dark theme), which read as a
   jarring, disconnected flash against every dark section around it.
   Now reuses the exact card language already established elsewhere on
   this site (.process-panel/.live-demo-browser: #0d0d10 + a faint
   blue-tinted border + the same AO-style shadow) rather than inventing
   a new one, so it reads as one more piece of the same system instead
   of a foreign popup. .quote-form below gets the identical treatment
   so the popup and the main form read as the same component in two
   places, per the same request. */

.newsletter-popup{
  position:fixed;
  right:1.25rem;
  bottom:1.25rem;
  z-index:900;
  display:flex;
  justify-content:flex-end;
  opacity:0;
  pointer-events:none;
  transform:translateY(20px);
  transition:opacity .4s var(--ease-out), transform .4s var(--ease-out);
}
.newsletter-popup.is-visible{
  opacity:1;
  pointer-events:auto;
  transform:translateY(0);
}
/* per iPhone-heat investigation: this popup sits at opacity:0 (not
   display:none) for however long it takes to trigger, or forever if
   the visitor never scrolls to it — its own quoteFormTitleShine
   animation would otherwise keep running, invisibly, the entire time.
   Updated for the tab-first redesign below: the card (and this shine
   inside it) now stays collapsed/invisible through the whole
   is-visible-but-not-yet-expanded tab stage too, potentially for the
   rest of the session if a visitor never clicks the tab — gating on
   is-expanded instead of is-visible closes that same gap again. */
.newsletter-popup:not(.is-expanded) .newsletter-popup-highlight{
  animation-play-state:paused;
}

/* per direct request: a minimal, bouncing "tab" is the actual first
   thing shown (see js/newsletter-popup.js's open()) — the full card
   below stays hidden until this is clicked (expand()). Same dark-glass
   pill language as .process-hero-start/.live-demo-arrow rather than a
   new visual language. */
.newsletter-popup-tab{
  display:flex;
  align-items:center;
  gap:.55rem;
  padding:.7rem 1.1rem .7rem .6rem;
  border-radius:999px;
  border:1px solid rgba(255,221,122,.5);
  background:#1a1508;
  color:var(--gold-soft);
  font-family:'Inter', sans-serif;
  font-size:.78rem;
  font-weight:700;
  letter-spacing:.02em;
  white-space:nowrap;
  cursor:none;
  box-shadow:0 16px 34px -14px rgba(0,0,0,.6);
  transition:opacity .25s var(--ease-out), transform .25s var(--ease-out), border-color .25s ease;
  animation:newsletterTabBounce 3.2s ease-in-out infinite;
}
.newsletter-popup-tab:hover{ border-color:#ffdd7a; }
.newsletter-popup-tab-badge{
  display:flex;
  align-items:center;
  justify-content:center;
  width:26px;
  height:26px;
  flex:0 0 auto;
  border-radius:50%;
  background:rgba(255,221,122,.18);
  color:#ffdd7a;
  font-size:.68rem;
  font-weight:800;
}
@keyframes newsletterTabBounce{
  0%, 55%, 100%{ transform:translateY(0); }
  65%{ transform:translateY(-10px); }
  75%{ transform:translateY(0); }
  83%{ transform:translateY(-5px); }
  91%{ transform:translateY(0); }
}
@media (prefers-reduced-motion:reduce){
  .newsletter-popup-tab{ animation:none; }
}
/* the card overlays the same bottom-right anchor point rather than
   sitting in normal flow next to the tab, so "expanding" reads as the
   tab itself growing into the card, not a second element appearing
   beside it */
.newsletter-popup-card{
  position:absolute;
  right:0;
  bottom:0;
  width:min(360px, calc(100vw - 2.5rem));
  max-height:calc(100vh - 2.5rem);
  overflow-y:auto;
  background:#0d0d10;
  border:1px solid rgba(255,221,122,.16);
  border-radius:16px;
  padding:clamp(1.2rem, 3vw, 1.5rem);
  box-shadow:0 24px 60px -16px rgba(0,0,0,.6), 0 4px 18px -4px rgba(0,0,0,.35), inset 0 1px 30px rgba(0,0,0,.28);
  opacity:0;
  transform:scale(.9) translateY(10px);
  transform-origin:bottom right;
  pointer-events:none;
  transition:opacity .3s var(--ease-out), transform .3s var(--ease-out);
}
.newsletter-popup.is-expanded .newsletter-popup-tab{
  opacity:0;
  transform:scale(.85);
  pointer-events:none;
}
.newsletter-popup.is-expanded .newsletter-popup-card{
  opacity:1;
  transform:scale(1) translateY(0);
  pointer-events:auto;
}
.newsletter-popup-close{
  position:absolute;
  top:.65rem;
  right:.65rem;
  width:28px;
  height:28px;
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:50%;
  border:none;
  background:rgba(243,237,226,.08);
  color:var(--cream);
  cursor:none;
  transition:background .2s var(--ease-out);
}
.newsletter-popup-close:hover{ background:rgba(243,237,226,.16); }
.newsletter-popup-eyebrow{
  font-size:.66rem;
  font-weight:700;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:#ffdd7a;
  margin-bottom:.5rem;
  padding-right:1.5rem; /* room for the close button */
}
.newsletter-popup-title{
  font-family:'Fraunces', serif;
  font-weight:500;
  font-size:clamp(1.15rem, 3vw, 1.3rem);
  line-height:1.25;
  color:var(--cream);
  margin-bottom:.6rem;
}
/* same bright blue sweep as .quote-form-title, tuned for this same
   dark backdrop (brightens toward the middle rather than darkening —
   the inverse of what reads well on a light card) */
.newsletter-popup-highlight{
  background:linear-gradient(100deg, #ffd23f 0%, #fff6d8 25%, #fff 50%, #fff6d8 75%, #ffd23f 100%);
  background-size:250% 100%;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  animation:quoteFormTitleShine 6s ease-in-out infinite;
}
.newsletter-popup-sub{
  font-size:.82rem;
  line-height:1.45;
  color:rgba(243,237,226,.66);
  margin-bottom:1.1rem;
}
.newsletter-popup-card .quote-form{
  background:none;
  padding:0;
}
/* tighter field spacing than the full-size .quote-form uses, so the
   compact card doesn't feel cramped edge-to-edge but still reads as
   "small" rather than a full form page */
.newsletter-popup-card .quote-field{ margin-bottom:.85rem; }
.newsletter-popup-card .quote-submit{ padding:.75rem 1.1rem; font-size:.85rem; }

@media (max-width:640px){
  /* the tab still hugs the right edge here (justify-content:flex-end
     on .newsletter-popup itself handles that) — this just widens the
     fixed anchor box so the expanded card reads as a compact bottom
     sheet, still nowhere near covering the page, at phone widths */
  .newsletter-popup{
    right:.75rem;
    left:.75rem;
    bottom:.75rem;
  }
}

/* ===================================================================
   FAQ CHATBOT — per direct request: "Add a FAQ chatbot, where someone
   might ask the most asked questions, and my bot answers them." Same
   dark-glass card language as every other panel on this site
   (.newsletter-popup-card/.live-demo-browser/.process-panel), anchored
   bottom-LEFT specifically so it never overlaps the newsletter tab's
   bottom-right corner. Structurally the same launcher-then-panel
   pattern as the newsletter popup's tab-then-card redesign just above.
=================================================================== */
.faq-bot{
  position:fixed;
  left:1.25rem;
  bottom:1.25rem;
  z-index:900;
}
.faq-bot-launcher{
  display:flex;
  align-items:center;
  justify-content:center;
  width:52px;
  height:52px;
  border-radius:50%;
  border:1px solid rgba(255,221,122,.5);
  background:#1a1508;
  color:#ffdd7a;
  cursor:none;
  box-shadow:0 16px 34px -14px rgba(0,0,0,.6), 0 0 0 0 rgba(255,221,122,.35);
  transition:transform .25s var(--ease-out), border-color .25s ease, opacity .25s ease;
  animation:faqBotPulse 3.4s ease-in-out infinite;
}
.faq-bot-launcher:hover{ border-color:#ffdd7a; transform:translateY(-3px); }
/* box-shadow, not background-position — see the site-wide heat audit
   this pairs with (js/accent.js's IntersectionObserver comment); a
   single small circle is cheap enough not to need that same
   off-screen-pause treatment, but it's still the right property to
   animate here regardless */
@keyframes faqBotPulse{
  0%, 100%{ box-shadow:0 16px 34px -14px rgba(0,0,0,.6), 0 0 0 0 rgba(255,221,122,.35); }
  50%{ box-shadow:0 16px 34px -14px rgba(0,0,0,.6), 0 0 0 8px rgba(255,221,122,0); }
}
@media (prefers-reduced-motion:reduce){
  .faq-bot-launcher{ animation:none; }
}
.faq-bot-panel{
  position:absolute;
  left:0;
  bottom:0;
  width:min(340px, calc(100vw - 2.5rem));
  max-height:min(480px, calc(100vh - 6rem));
  display:flex;
  flex-direction:column;
  background:#0d0d10;
  border:1px solid rgba(255,221,122,.16);
  border-radius:16px;
  box-shadow:0 24px 60px -16px rgba(0,0,0,.6), 0 4px 18px -4px rgba(0,0,0,.35), inset 0 1px 30px rgba(0,0,0,.28);
  opacity:0;
  transform:scale(.9) translateY(10px);
  transform-origin:bottom left;
  pointer-events:none;
  transition:opacity .3s var(--ease-out), transform .3s var(--ease-out);
  overflow:hidden;
}
.faq-bot.is-open .faq-bot-launcher{
  opacity:0;
  transform:scale(.85);
  pointer-events:none;
}
.faq-bot.is-open .faq-bot-panel{
  opacity:1;
  transform:scale(1) translateY(0);
  pointer-events:auto;
}
.faq-bot-header{
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:.9rem 1rem;
  border-bottom:1px solid rgba(255,221,122,.12);
  flex:0 0 auto;
}
.faq-bot-title{
  font-family:'Fraunces', serif;
  font-size:1rem;
  color:var(--gold-soft);
}
.faq-bot-close{
  display:flex;
  align-items:center;
  justify-content:center;
  width:26px;
  height:26px;
  border-radius:50%;
  border:none;
  background:rgba(243,237,226,.08);
  color:var(--cream);
  cursor:none;
  transition:background .2s var(--ease-out);
}
.faq-bot-close:hover{ background:rgba(243,237,226,.16); }
.faq-bot-log{
  flex:1;
  overflow-y:auto;
  padding:.9rem 1rem;
  display:flex;
  flex-direction:column;
  gap:.6rem;
  min-height:140px;
}
.faq-bot-msg{
  max-width:85%;
  padding:.55rem .8rem;
  border-radius:14px;
  font-size:.82rem;
  line-height:1.45;
}
.faq-bot-msg--bot{
  align-self:flex-start;
  background:rgba(255,221,122,.1);
  color:var(--cream);
  border-bottom-left-radius:4px;
}
.faq-bot-msg--user{
  align-self:flex-end;
  background:rgba(243,237,226,.1);
  color:var(--cream);
  border-bottom-right-radius:4px;
}
/* per direct request: don't show every suggested question at once (the
   knowledge base is long enough now that a wrapped grid of chips ate
   most of the panel's height) — a single horizontally-scrolling row,
   same swipeable-row convention as .testimonial-stack/.live-demo-stack/
   .quote-form-stack elsewhere on the site, shows roughly 1-3 chips at a
   time with the rest one swipe away instead of all of them wrapped and
   crammed in up front. */
.faq-bot-suggestions{
  flex:0 0 auto;
  display:flex;
  flex-wrap:nowrap;
  gap:.4rem;
  padding:0 1rem .8rem;
  overflow-x:auto;
  scroll-snap-type:x proximity;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  mask-image:linear-gradient(90deg, transparent, #000 3%, #000 94%, transparent);
  -webkit-mask-image:linear-gradient(90deg, transparent, #000 3%, #000 94%, transparent);
}
.faq-bot-suggestions::-webkit-scrollbar{ display:none; }
.faq-bot-chip{
  flex:0 0 auto;
  white-space:nowrap;
  scroll-snap-align:start;
  font-size:.7rem;
  padding:.4rem .7rem;
  border-radius:999px;
  border:1px solid rgba(255,221,122,.3);
  background:transparent;
  color:var(--gold-soft);
  cursor:none;
  transition:border-color .2s ease, background .2s ease;
}
.faq-bot-chip:hover{ border-color:#ffdd7a; background:rgba(255,221,122,.08); }
.faq-bot-form{
  flex:0 0 auto;
  display:flex;
  gap:.5rem;
  padding:.8rem 1rem;
  border-top:1px solid rgba(255,221,122,.12);
}
.faq-bot-form input{
  flex:1;
  min-width:0;
  background:rgba(243,237,226,.06);
  border:1px solid rgba(243,237,226,.16);
  border-radius:999px;
  padding:.55rem .9rem;
  color:var(--cream);
  font-size:.82rem;
  font-family:'Inter', sans-serif;
}
.faq-bot-form input:focus{ outline:none; border-color:#ffdd7a; }
.faq-bot-form button{
  display:flex;
  align-items:center;
  justify-content:center;
  width:38px;
  height:38px;
  border-radius:50%;
  border:none;
  background:var(--gold);
  color:#0a0908;
  cursor:none;
  flex:0 0 auto;
  transition:transform .2s var(--ease-out);
}
.faq-bot-form button:hover{ transform:scale(1.06); }
@media (max-width:640px){
  .faq-bot{ left:.75rem; bottom:.75rem; }
  .faq-bot-panel{ width:min(340px, calc(100vw - 1.5rem)); }
}

/* ===================================================================
   LIVE DEMOS — real, live iframe embeds of sites Papi has actually
   built (not screenshots) inside a browser-chrome-style frame; the
   visitor scrolls *inside* the frame to explore the real site. Cards
   are built by js/live-demo.js from a small data array — same
   swipeable-row pattern as .testimonial-stack (native horizontal
   scroll-snap, centered via side padding rather than justify-content,
   which stays reachable-from-the-first-pixel regardless of item count
   — justify-content:center on an overflowing flex row is a documented
   cross-browser scroll trap that can strand content before the
   visible start unreachable).
=================================================================== */
.live-demo-section{
  position:relative;
  z-index:1;
  /* per direct request: "there also seems to be waves around some
     sections, looks like they are like gradient layers, please remove
     that from all sections" — was a radial-gradient ellipse peaking at
     top-center; every section down the page had its own copy of this,
     so scrolling past each one's own bright-center/dark-edge boundary
     read as a repeating wave. Flattened to the same solid tone the old
     gradient's own darkest stop already used, so the overall darkness
     is unchanged but there's no curvature left to read as a wave */
  background:#07060a;
  padding:clamp(3.5rem, 8vw, 6rem) 0;
}
.live-demo-inner{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:clamp(1.2rem, 3vw, 2rem);
  padding:0 1.5rem;
  text-align:center;
  opacity:0;
  will-change:opacity, transform;
}
.live-demo-eyebrow{
  font-size:.76rem;
  letter-spacing:.38em;
  text-transform:uppercase;
  color:#ffdd7a;
}
.live-demo-title{
  font-family:'Fraunces', serif;
  font-weight:400;
  font-size:clamp(1.9rem, 4.4vw, 2.8rem);
  line-height:1.15;
  color:var(--cream);
}
.live-demo-title em{
  font-style:italic;
  color:#ffdd7a;
}
.live-demo-sub{
  max-width:520px;
  margin-top:-.6rem;
  color:rgba(243,237,226,.68);
  font-size:1rem;
  line-height:1.5;
}

.live-demo-stack{
  position:relative;
  z-index:1;
  width:100%;
  max-width:1200px;
  display:flex;
  gap:clamp(1.2rem, 3vw, 2rem);
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  /* far less side padding than testimonials' own version of this row —
     that padding exists there to let adjacent cards peek in from the
     edges, which matters much less here where the card itself needs
     to be as wide as possible (see .live-demo-card below) */
  padding:.5rem clamp(1rem, 4vw, 3rem) 1.5rem;
}
.live-demo-stack::-webkit-scrollbar{ display:none; }

.live-demo-card{
  flex:0 0 auto;
  /* wide enough on desktop to clear the embedded site's own ~980px/
     720px responsive breakpoints — at the old 640px cap every visitor
     was seeing that site's *mobile* layout (hamburger nav, stacked
     sections, its scroll-driven route animation turned off entirely)
     squeezed into a short, wide frame, which read as cluttered rather
     than showing off the real design. Comfortably narrower viewports
     naturally still get a smaller card here too, so the embedded
     site's own mobile layout keeps showing there, correctly */
  width:min(94vw, 1040px);
  scroll-snap-align:center;
  display:flex;
  flex-direction:column;
  align-items:center;
}
.live-demo-browser{
  position:relative;
  width:100%;
  border-radius:16px;
  overflow:hidden;
  /* per direct follow-up request ("make the entire card gold yellow, i
     do not want those grays in there") — the old #0d0d10 was a neutral
     near-black with no gold cast at all, and every accent inside this
     card (bar/dots/url pill below) was a low-opacity tint faint enough
     to read as plain gray against it. Background swapped for a dark
     tone with a real gold cast, and every accent below bumped to a
     clearly gold opacity rather than a barely-there one. */
  background:#1a1508;
  border:1px solid rgba(255,221,122,.5);
  box-shadow:0 40px 90px -30px rgba(0,0,0,.6), inset 0 1px 30px rgba(0,0,0,.28);
}
/* soft reflection: a static, low-opacity top-down gradient, never a
   live blur/backdrop-filter — kept faint and top-only per "without
   making the scene glossy". pointer-events:none so it never blocks
   clicking/dragging inside the real iframe underneath. */
.live-demo-browser::after{
  content:'';
  position:absolute;
  inset:0 0 auto 0;
  height:40%;
  background:linear-gradient(180deg, rgba(255,221,122,.1), transparent);
  pointer-events:none;
}
.live-demo-browser-bar{
  display:flex;
  align-items:center;
  gap:.5rem;
  padding:.65rem .9rem;
  background:rgba(255,221,122,.16);
}
.live-demo-dot{
  width:8px;
  height:8px;
  border-radius:50%;
  background:rgba(255,221,122,.6);
  flex:0 0 auto;
}
.live-demo-url{
  flex:1;
  margin-left:.4rem;
  font-size:.72rem;
  color:rgba(255,221,122,.85);
  background:rgba(255,221,122,.1);
  border-radius:999px;
  padding:.3em .9em;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
/* clamped against vw, not vh — this doesn't multiply into anything
   that defines the page's total scroll distance the way the site's
   tall sticky sections do, so it was never at risk of the dramatic
   in-app-browser jump those needed --stable-vh to fix. Sized off vw
   instead of vh anyway, purely so this card's own height has zero
   dependency on viewport height at all rather than a bounded one. */
.live-demo-frame-wrap{
  position:relative;
  width:100%;
  /* raised alongside the card's own wider footprint above — the
     embedded site's hero is a full scroll-driven "journey" section
     that needs real height to read as intended rather than looking
     cramped, and clearing its 720px-tall compact-mode breakpoint too
     avoids it falling back to that shorter, tighter-spaced version.
     The 480px floor this used to have was well under that site's own
     hero content height even in its normal (non-compact) mobile
     layout, which is what still read as cluttered on an iPhone —
     raised to 680px so the floor itself clears that same threshold,
     not just the vw-scaled value at wider widths. */
  height:clamp(680px, 60vw, 780px);
  background:#fff;
}
.live-demo-iframe{
  width:100%;
  height:100%;
  border:0;
  display:block;
  background:#fff;
}
/* shown in place of the iframe until the visitor explicitly clicks to
   load the live site (js/live-demo.js) — a real <button>, not a plain
   label, so this doubles as the click target. Dark to match the
   surrounding all-black page rather than the near-white the embedded
   client sites themselves use — those stay white once actually loaded
   (real light-themed sites), but this placeholder needed to not read
   as a flash of unstyled content against everything else here. */
.live-demo-loading{
  position:absolute;
  inset:0;
  width:100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:.7rem;
  background:#0d0d10;
  border:0;
  cursor:pointer;
  transition:opacity .4s ease;
}
.live-demo-load-icon{
  display:flex;
  align-items:center;
  justify-content:center;
  width:2.6rem;
  height:2.6rem;
  border-radius:50%;
  border:1px solid rgba(255,221,122,.35);
  color:#ffdd7a;
  font-size:.8rem;
  transition:background .25s ease, transform .25s ease, border-color .25s ease;
}
.live-demo-load-btn:hover .live-demo-load-icon,
.live-demo-load-btn:focus-visible .live-demo-load-icon{
  background:rgba(255,221,122,.12);
  border-color:rgba(255,221,122,.6);
  transform:scale(1.06);
}
.live-demo-load-text{
  font-size:.8rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:rgba(243,237,226,.55);
}
.live-demo-load-btn:disabled{ cursor:default; }
.live-demo-load-btn:disabled .live-demo-load-icon{ opacity:.4; }
.live-demo-card.is-loaded .live-demo-loading{
  opacity:0;
  pointer-events:none;
}

.live-demo-name{
  margin-top:1.1rem;
  font-family:'Fraunces', serif;
  font-size:1.1rem;
  color:var(--cream);
}
.live-demo-industry{
  font-size:.72rem;
  letter-spacing:.28em;
  text-transform:uppercase;
  color:rgba(243,237,226,.5);
  margin-top:.3rem;
}
.live-demo-visit{
  margin-top:.8rem;
  font-size:.85rem;
  color:#ffdd7a;
  text-decoration:none;
  border-bottom:1px solid rgba(255,221,122,.4);
  padding-bottom:2px;
  cursor:none;
  transition:border-color .3s var(--ease-out);
}
.live-demo-visit:hover{ border-color:transparent; }

.live-demo-controls{
  position:relative;
  z-index:1;
  display:flex;
  align-items:center;
  gap:1.4rem;
}
.live-demo-arrow{
  display:flex;
  align-items:center;
  justify-content:center;
  width:38px; height:38px;
  border-radius:50%;
  border:1px solid rgba(243,237,226,.25);
  background:transparent;
  color:rgba(243,237,226,.8);
  cursor:none;
  transition:border-color .3s var(--ease-out), color .3s var(--ease-out), background .3s var(--ease-out);
}
.live-demo-arrow:hover{ border-color:#ffdd7a; color:#ffdd7a; background:rgba(255,221,122,.08); }
@media (hover:none), (pointer:coarse){ .live-demo-arrow{ display:none; } }
/* hidden outright with a single demo — no point browsing a list of one */
.live-demo-controls.is-single{ display:none; }

.live-demo-dots{
  display:flex;
  gap:.55rem;
  align-items:center;
}
.live-demo-dot-btn{
  width:8px;
  height:8px;
  border-radius:50%;
  border:none;
  padding:0;
  background:rgba(243,237,226,.25);
  cursor:none;
  transition:background .35s var(--ease-out), width .35s var(--ease-out);
}
.live-demo-dot-btn:hover{ background:rgba(243,237,226,.5); }
.live-demo-dot-btn.is-active{
  width:22px;
  border-radius:5px;
  background:#ffdd7a;
}

/* ===================================================================
   Process Hero — per direct request: "remove the background but keep
   the elements in place" — the photographed room photo is gone. What
   used to be here in its place was a liquid-glass metaball canvas
   (js/process-hero-slime.js); per a later direct request that's gone
   too, so the hero's whole visual is now js/hero-matrix.js's own
   falling-number rain over a plain dark radial-gradient base (still
   has some depth to it, not a flat single color) plus a faint film-
   grain layer so the dark areas read as textured rather than empty.
   Headline, CTA, and social icons sit on top (the 4 process-step
   hotspots that used to live here moved to their own #ourProcessSection
   further down the page — see that section's own comment).
=================================================================== */
.process-hero{
  position:relative;
  width:100%;
  /* BUG FIX: per report, "instagram browser opens but nothing shows,
     just a blank page." These three were declared 100svh THEN 100vh —
     backwards: whichever is declared LAST wins when both are
     supported, so the plain 100vh (unstable in Instagram's in-app
     browser — see the long --stable-vh comment at the top of
     index.html) was overriding the safer 100svh the whole time,
     defeating it entirely. This is the hero GSAP pins via
     js/scroll-journey-hero.js with start:'bottom bottom', which "pin
     engages essentially at page load" per that file's own comment —
     any bad/unstable height reading right when GSAP first measures
     this element feeds straight into that pin. Reordered so each line
     only IMPROVES on the one before: 100vh (universal baseline) ->
     100svh (overrides it where supported, avoids mobile Safari's own
     URL-bar jump) -> the JS-measured --stable-vh custom property
     (overrides both, in every browser, once index.html's early inline
     script sets it) — the same value already purpose-built for
     exactly Instagram's in-app browser, previously computed and set
     but never actually consumed anywhere. */
  min-height:100vh;
  min-height:100svh;
  min-height:calc(var(--stable-vh, 1vh) * 100);
  overflow:hidden;
  /* per direct request: "there also seems to be waves around some
     sections... please remove that from all sections" — flattened
     from a radial-gradient vignette to a flat solid tone (matching
     that gradient's own darkest stop) for the same reason as every
     other section below */
  background:#07060a;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
}
/* per direct request: a falling-numbers matrix rain running
   continuously as the hero's whole background visual (js/hero-
   matrix.js) — z-index sits below .process-room-grain (z-index:2) so
   the grain layer visibly sits on top of it, and below .process-hero-
   copy (z-index:5) so it never competes with the title/CTA for
   legibility */
.process-hero-matrix{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  display:block;
  z-index:1;
  pointer-events:none;
}

.process-hero-copy{
  position:relative;
  z-index:5;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:clamp(1.6rem, 3.4vw, 2.4rem);
  padding:0 1.5rem;
  /* BUG FIX: per request for "very soft floating atmosphere... slight
     inertia on every animated object" — a barely-there drift, entirely
     independent of js/scroll-journey-hero.js's own pin-scrubbed tweens
     on this same element (which only ever touch `opacity` here, never
     `transform` — see that file's own tl.to(heroCopy,...) call — so
     this CSS transform and that JS opacity tween never fight over the
     same property). Kept extremely small (3px) and slow (24s) so it
     reads as ambient breathing, not motion. */
  animation:heroCopyFloat 24s ease-in-out infinite;
}
@keyframes heroCopyFloat{
  0%, 100%{ transform:translateY(0); }
  50%{ transform:translateY(-3px); }
}
@media (prefers-reduced-motion:reduce){
  .process-hero-copy{ animation:none; }
}
.process-hero-title{
  font-family:'Fraunces', serif;
  font-weight:400;
  font-style:italic;
  font-size:clamp(2.4rem, 6vw, 4.4rem);
  line-height:1.08;
  max-width:16ch;
  transition:opacity .45s ease, filter .45s ease, transform .45s ease;
  /* a brushed-metal look reads as alternating light/dark bands of the
     SAME hue catching light, not a flat fill — this bands warm cream/
     white highlights between the site's own gold tones (--gold/--gold-
     soft/--gold-deep, see :root) rather than a flat fill, which is what
     keeps it feeling like a premium metal finish instead of a plain
     yellow title */
  color:var(--gold-soft); /* fallback for browsers without text-clip gradients */
  background:linear-gradient(115deg,
    #faf8ee 0%,
    var(--gold-soft) 16%,
    var(--gold) 34%,
    #fffdf5 50%,
    var(--gold-deep) 66%,
    var(--gold-soft) 84%,
    #faf8ee 100%);
  background-size:220% 100%;
  background-position:0% 50%;
  -webkit-background-clip:text;
  background-clip:text;
  -webkit-text-fill-color:transparent;
  text-shadow:0 4px 40px rgba(4,8,12,.4), 0 0 30px rgba(228,205,154,.3);
  animation:processHeroTitleShine 7s ease-in-out infinite;
}
@keyframes processHeroTitleShine{
  0%, 100%{ background-position:0% 50%; }
  50%{ background-position:100% 50%; }
}
/* per direct request: "animate 'Building Websites that Matter' into
   something that turns like a glitch numbers that are like the matrix
   as the numbers in the back scale, so it can be one immersive flow" —
   js/scroll-journey-hero.js splits the title into these spans and
   scrubs them between their real letter and a random matrix digit as
   the hero's own portal pin scrolls. is-glitching overrides the
   title's own gradient text-fill (see .process-hero-title above) with
   a flat bright gold matching js/hero-matrix.js's own head-character
   color, so a glitching character visually pops out of the metallic
   gradient and reads as becoming one of the falling digits behind it,
   instead of just fading out on its own. */
.hero-title-char{
  display:inline-block;
}
.hero-title-char.is-glitching{
  background:none;
  -webkit-text-fill-color:#fff28f;
  color:#fff28f;
  text-shadow:0 0 6px rgba(255,242,143,.85), 0 0 16px rgba(255,221,122,.6);
}
/* same technique, applied to the "View our work" button's own text —
   see js/scroll-journey-hero.js's CTA BUTTON GLITCH note. No gradient
   text-fill to override here (the button uses a plain solid color, not
   the title's metallic gradient), just the same gold glow on glitch. */
.hero-cta-char{
  display:inline-block;
}
.hero-cta-char.is-glitching{
  color:#fff28f;
  text-shadow:0 0 6px rgba(255,242,143,.85), 0 0 14px rgba(255,221,122,.6);
}
/* per direct request: "dissolve the title" while a step is open — a
   soft fade/blur/lift out, not just a plain opacity cut */
.process-hero-title.is-dissolved{
  opacity:0;
  filter:blur(10px);
  transform:translateY(-8px) scale(1.02);
  pointer-events:none;
}
/* per direct request: "make it glitch appear back when someone clicks
   out of the step" — originally an RGB-split + horizontal slice
   jitter; transition:none so the plain fade above doesn't fight the
   keyframes while this class is active.
   Per a later direct request ("a different glitch form after every
   step, to add style") this became 4 distinct variants — process-
   hero.js's own hideReveal() cycles through .is-glitch-a/-b/-c/-d in
   sequence (one per .process-hero-title element, so it's always
   exactly one variant class at a time) rather than always replaying
   the same one. Base rule below only sets the shared timing; each
   modifier class supplies its own animation-name.
   Per a still later direct request ("not something colorful, but more
   so something that is spatial blurriness") every variant below had
   its colored text-shadow (the RGB chromatic-aberration fringing) and
   variant C's hue-rotate stripped out entirely — none of the 4 tint
   the text away from its own normal gradient anymore. What used to be
   color is now filter:blur() jumping between sharp and heavily
   blurred at each of the 8 steps, so the "settling into focus" motion
   IS the glitch, not a color flash. */
.process-hero-title.is-glitching-in{
  transition:none;
  /* steps(8,end) hard-cuts between the 8 keyframe segments below instead
     of smoothly tweening them — steps(1,end) was tried first and was
     wrong: with only 1 step for the WHOLE duration it collapses every
     keyframe in between into a single start→end jump, skipping the
     entire jitter sequence (confirmed by tracing computed styles frame
     by frame — it snapped straight to settled almost immediately) */
  animation-duration:.65s;
  animation-timing-function:steps(8, end);
  animation-fill-mode:both;
}
.process-hero-title.is-glitching-in.is-glitch-a{ animation-name:processHeroTitleGlitchIn; }
.process-hero-title.is-glitching-in.is-glitch-b{ animation-name:processHeroTitleGlitchVertical; }
.process-hero-title.is-glitching-in.is-glitch-c{ animation-name:processHeroTitleGlitchFlicker; }
.process-hero-title.is-glitching-in.is-glitch-d{ animation-name:processHeroTitleGlitchBlocks; }
@keyframes processHeroTitleGlitchIn{
  0%{
    opacity:0;
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
    filter:blur(14px);
  }
  8%{
    opacity:1;
    filter:blur(1px);
    transform:translate3d(-6px,0,0);
    clip-path:inset(8% 0 62% 0);
  }
  18%{
    filter:blur(8px);
    transform:translate3d(5px,0,0);
    clip-path:inset(58% 0 6% 0);
  }
  28%{
    filter:blur(2px);
    transform:translate3d(-4px,0,0);
    clip-path:inset(18% 0 44% 0);
  }
  38%{
    filter:blur(6px);
    transform:translate3d(3px,0,0);
    clip-path:inset(0 0 72% 0);
  }
  50%{
    filter:blur(1px);
    transform:translate3d(-2px,0,0);
    clip-path:inset(64% 0 4% 0);
  }
  62%{
    filter:blur(4px);
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
  }
  78%{
    filter:blur(1px);
  }
  100%{
    opacity:1;
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
    filter:blur(0);
  }
}
/* variant B — vertical shards + skew, instead of variant A's
   horizontal slice/RGB-jitter, so it reads as a distinctly different
   "form" of glitch rather than the same effect replaying */
@keyframes processHeroTitleGlitchVertical{
  0%{
    opacity:0;
    transform:translate3d(0,0,0) skewX(0deg);
    clip-path:inset(0 0 0 0);
    filter:blur(14px);
  }
  10%{
    opacity:1;
    filter:blur(7px);
    transform:translate3d(0,-4px,0) skewX(6deg);
    clip-path:inset(0 68% 0 0);
  }
  22%{
    filter:blur(1px);
    transform:translate3d(0,3px,0) skewX(-5deg);
    clip-path:inset(0 4% 0 62%);
  }
  34%{
    filter:blur(5px);
    transform:translate3d(0,-2px,0) skewX(3deg);
    clip-path:inset(0 40% 0 18%);
  }
  46%{
    filter:blur(1px);
    transform:translate3d(0,2px,0) skewX(-2deg);
    clip-path:inset(0 72% 0 0);
  }
  60%{
    filter:blur(3px);
    transform:translate3d(0,0,0) skewX(0deg);
    clip-path:inset(0 0 0 64%);
  }
  75%{
    filter:blur(1px);
    clip-path:inset(0 0 0 0);
  }
  88%{
    filter:blur(2px);
  }
  100%{
    opacity:1;
    transform:translate3d(0,0,0) skewX(0deg);
    clip-path:inset(0 0 0 0);
    filter:blur(0);
  }
}
/* variant C — power-flicker/strobe: brightness + opacity flicker
   instead of positional jitter, no clip-path movement at all, so it
   reads as a "signal cutting out" rather than a "shattering" glitch */
@keyframes processHeroTitleGlitchFlicker{
  0%{
    opacity:0;
    filter:blur(12px) brightness(1);
    transform:scale(1);
  }
  10%{
    opacity:1;
    filter:blur(6px) brightness(2.2);
    transform:scale(1.03);
  }
  22%{
    opacity:.2;
    filter:blur(9px) brightness(.4);
    transform:scale(.99);
  }
  34%{
    opacity:1;
    filter:blur(2px) brightness(1.8);
  }
  46%{
    opacity:.35;
    filter:blur(7px) brightness(.5);
  }
  60%{
    opacity:1;
    filter:blur(1px) brightness(1.4);
  }
  75%{
    opacity:.6;
    filter:blur(4px) brightness(1);
  }
  88%{
    opacity:1;
    filter:blur(1px) brightness(1);
  }
  100%{
    opacity:1;
    filter:blur(0) brightness(1);
    transform:scale(1);
  }
}
/* variant D — blocky corruption sweep: bigger, chunkier clip-path
   jump-cuts than variant A's fine scanlines, plus a contrast punch on
   each cut, so it reads as heavier digital corruption rather than a
   fine positional jitter */
@keyframes processHeroTitleGlitchBlocks{
  0%{
    opacity:0;
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
    filter:blur(16px) contrast(1);
  }
  9%{
    opacity:1;
    filter:blur(2px) contrast(1.6);
    transform:translate3d(8px,0,0);
    clip-path:inset(0 0 82% 0);
  }
  20%{
    filter:blur(9px) contrast(1.4);
    transform:translate3d(-7px,0,0);
    clip-path:inset(76% 0 0 0);
  }
  31%{
    filter:blur(1px) contrast(1.9);
    transform:translate3d(6px,0,0);
    clip-path:inset(30% 0 46% 0);
  }
  42%{
    filter:blur(6px) contrast(1.2);
    transform:translate3d(-5px,0,0);
    clip-path:inset(0 0 0 0);
  }
  55%{
    filter:blur(1px) contrast(1.3);
    transform:translate3d(4px,0,0);
    clip-path:inset(48% 0 12% 0);
  }
  68%{
    filter:blur(3px) contrast(1.1);
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
  }
  82%{
    filter:blur(1px) contrast(1.1);
  }
  100%{
    opacity:1;
    transform:translate3d(0,0,0);
    clip-path:inset(0 0 0 0);
    filter:blur(0) contrast(1);
  }
}
/* per direct request: "move the view our work button above" — this
   used to sit absolutely pinned near the section's bottom edge and
   start locked/dim until all 4 steps (formerly also in this section)
   were opened. Now it's just a normal in-flow third child of
   .process-hero-copy, right under the title (inheriting that flex
   column's own gap for spacing) — always active, no lock/unlock state
   at all, since the steps it used to gate on live in their own section
   further down the page now. */
.process-hero-cta{
  position:relative;
  z-index:5;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  font-family:'Inter', sans-serif;
  font-size:.82rem;
  font-weight:700;
  letter-spacing:.14em;
  text-transform:uppercase;
  text-decoration:none;
  border-radius:999px;
  padding:1rem 2.4rem;
  white-space:nowrap;
  overflow:hidden;
  /* per direct request: still read as "white" even with the brushed-
     metal band gradient (the light cream/white stops dominated at a
     glance) — rebuilt on the same dark-glass base as .process-hero-
     start below (so the two CTAs feel like a matched pair) with an
     aqua gradient glow behind it, PLUS the metallic shine sweeping
     across on top — gradient, glow, and metallic shine together
     instead of any one of them alone */
  color:var(--cream);
  background-color:rgba(10,16,20,.55);
  background-image:linear-gradient(120deg,
    rgba(228,205,154,0) 0%,
    rgba(228,205,154,.5) 22%,
    rgba(255,255,255,.55) 40%,
    var(--gold) 55%,
    rgba(228,205,154,.5) 72%,
    rgba(228,205,154,0) 100%);
  background-size:100% 100%, 240% 100%;
  background-position:0 0, 0% 50%;
  background-repeat:no-repeat;
  border:1px solid rgba(228,205,154,.65);
  box-shadow:0 8px 30px rgba(0,0,0,.35), 0 0 34px rgba(203,168,105,.5), inset 0 1px 0 rgba(255,255,255,.2);
  animation:processHeroCtaShine 6.5s ease-in-out infinite;
  transition:transform .25s var(--ease-out), box-shadow .25s var(--ease-out), background-color .25s ease;
}
@keyframes processHeroCtaShine{
  0%, 100%{ background-position:0 0, 0% 50%; }
  50%{ background-position:0 0, 100% 50%; }
}
/* per direct request: "make that button have the shine sweep actually
   move" — it previously only swept once, on hover; this now sweeps
   continuously on a loop (a long pause at each end so it reads as an
   occasional glint, not a constant strobe), then speeds up as an extra
   "catching the light" cue right when someone's about to click */
.process-hero-cta::before{
  content:'';
  position:absolute;
  inset:-60% -30%;
  background:linear-gradient(115deg, transparent 42%, rgba(255,255,255,.8) 50%, transparent 58%);
  transform:translateX(-140%);
  pointer-events:none;
  animation:processHeroCtaGlint 3.4s ease-in-out infinite;
}
@keyframes processHeroCtaGlint{
  0%{ transform:translateX(-140%); }
  35%{ transform:translateX(140%); }
  100%{ transform:translateX(140%); }
}
/* per a full-site heat audit: .process-hero-title/.process-hero-cta
   (both animate background-position — a real repaint, not just a
   composite) and the 4 simultaneous .process-reveal-title shines (all
   4 process-step panels exist in the DOM at once, see
   js/scroll-journey-process.js) are paused off-screen by
   js/anim-idle.js's generic .is-anim-idle mechanism (see its #hero and
   #ourProcessSection groups) rather than a bespoke rule here —
   consolidated so there's one canonical off-screen-pause system
   site-wide instead of two. */
.process-hero-cta:hover::before{
  animation-duration:1.1s;
}
.process-hero-cta:hover{
  transform:translateY(-2px) scale(1.03);
  background-color:rgba(16,24,30,.6);
  box-shadow:0 12px 38px rgba(0,0,0,.4), 0 0 44px rgba(203,168,105,.65), inset 0 1px 0 rgba(255,255,255,.28);
}
.process-hero-cta:active{ transform:translateY(0) scale(.98); }

/* per direct request: "add the social media icons we had before,
   under the view our work button" — sized down (32px vs 38px) since
   this is a compact hero accent under the CTA rather than a
   standalone footer row. Per direct follow-up feedback ("make the
   social icons feel part of the theme, those icons look old") this
   dropped the original full-color brand-logo circles for the same
   dark-glass pill look already established on .process-hero-start
   right above it (in the top-right corner) — border + translucent
   fill + a cream icon that brightens to the site's aqua accent on
   hover, same as every other icon-in-a-circle on this page. */
.process-hero-social{
  position:relative;
  z-index:5;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:.7rem;
  margin-top:.4rem;
}
.process-hero-social-link{
  display:flex;
  align-items:center;
  justify-content:center;
  width:32px;
  height:32px;
  border-radius:50%;
  /* per direct request to add the site's gold tone to these — was
     plain cream by default, only turning gold on hover; now gold at
     rest too, matching .process-hero-start beside it */
  color:var(--gold-soft);
  background:rgba(12,16,20,.35);
  border:1px solid rgba(228,205,154,.5);
  transition:transform .3s var(--ease-out), border-color .25s ease, background-color .25s ease, color .25s ease;
}
.process-hero-social-link:hover{
  transform:translateY(-3px) scale(1.08);
  color:#ffdd7a;
  border-color:#ffdd7a;
  background-color:rgba(255,221,122,.12);
}
.process-hero-social-link svg{ display:block; width:16px; height:16px; }

/* contrasting on purpose — solid cream is already the primary CTA
   above, so this one inverts to a translucent outline against the
   photo instead of competing for the same "solid light pill" read */
.process-hero-start{
  position:absolute;
  top:clamp(1.4rem, 3vw, 2.4rem);
  right:clamp(1.4rem, 3vw, 2.4rem);
  z-index:6;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  font-family:'Inter', sans-serif;
  font-size:.78rem;
  font-weight:600;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--cream);
  text-decoration:none;
  background:rgba(12,16,20,.35);
  /* per direct request: outline this one in the site's gold accent
     instead of the plain cream it used before, so it visually pairs
     with the now-gold "View our work" CTA beside it */
  border:1px solid rgba(228,205,154,.6);
  border-radius:999px;
  padding:.7rem 1.5rem;
  backdrop-filter:none;
  transition:background .25s ease, border-color .25s ease, transform .25s var(--ease-out);
  /* per direct request: glow like the FAQ chatbot launcher button —
     same faqBotPulse keyframe/timing, reused rather than inventing a
     second near-identical animation. Included in #hero's own group in
     js/anim-idle.js so this pauses off-screen the same way every other
     infinite animation on the site does. */
  animation:faqBotPulse 3.4s ease-in-out infinite;
}
.process-hero-start:hover{
  background:rgba(228,205,154,.14);
  border-color:var(--gold-soft);
  transform:translateY(-1px);
}
@media (prefers-reduced-motion:reduce){
  .process-hero-start{ animation:none; }
}

/* ===================================================================
   OUR PROCESS — the interactive dot line + reveal panel moved out of
   the hero (per direct request: "move the Step process right after
   the live Demo, so people are more interacting with how things are
   made" — real proof first, then how it's actually built). Same dark-
   section pattern (and matching eyebrow/title color) as .live-demo-
   section right above it, for visual continuity between the two.
=================================================================== */
.our-process-section{
  position:relative;
  z-index:1;
  min-height:80vh;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  overflow:hidden;
  /* per direct request: "there also seems to be waves around some
     sections... please remove that from all sections" — flattened to
     the same solid tone used across every other section now, for one
     continuous dark background with no visible seams while scrolling */
  background:#07060a;
  padding:clamp(4rem, 9vw, 6.5rem) 1.5rem;
}
.our-process-inner{
  position:relative;
  z-index:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:clamp(1rem, 2.6vw, 1.6rem);
  transition:opacity .5s ease, filter .5s ease;
}
/* per direct request: "make the background in the process section
   darken when the step is clicked" — process-hero.js toggles this on
   #ourProcessSection itself the instant a step opens/closes. Stacks
   with .process-reveal's own radial-gradient darkening (that overlay
   already sits above this at z-index:8 while open) rather than
   replacing it — this dims what's still peeking out around/behind the
   reveal card, so the open step reads as a spotlit focus */
.our-process-section.is-active .our-process-inner{
  opacity:.22;
  filter:blur(3px);
}
.our-process-eyebrow{
  font-size:.76rem;
  letter-spacing:.38em;
  text-transform:uppercase;
  color:#ffdd7a;
}
.our-process-title{
  font-family:'Fraunces', serif;
  font-weight:400;
  font-size:clamp(1.9rem, 4.4vw, 2.8rem);
  line-height:1.15;
  color:var(--cream);
  max-width:26ch;
}
.our-process-title em{
  font-style:italic;
  color:#ffdd7a;
}
.our-process-sub{
  max-width:480px;
  margin-top:-.4rem;
  color:rgba(243,237,226,.68);
  font-size:1rem;
  line-height:1.5;
}

/* per direct request for a GSAP ScrollTrigger cinematic journey — the
   pinned 400vh depth-travel stage for the 4 process steps (see
   js/scroll-journey-process.js, which builds the 4 .process-panel
   children from window.PapiSteps and drives their transform every
   scroll frame). perspective lives HERE, local to this one stage —
   never on <body> — after perspective on <body> caused a real bug
   earlier this session (translateZ projected from the document's
   vertical midpoint instead of the viewport, dragging the quote
   section up on top of FAQ on mobile). Panel typography reuses the
   existing .process-reveal-icon/-index/-title/-text classes directly
   (same premium metallic-title treatment, no new type rules needed) —
   only the card chrome itself (background/border/shadow, matching
   .live-demo-browser's own values for consistency) is new here. */
.process-stage{
  position:relative;
  width:100%;
  max-width:1100px;
  height:clamp(360px, 48vh, 480px);
  margin-top:clamp(2rem, 5vh, 3.5rem);
  perspective:1400px;
}
.process-panel{
  /* centered via inset:0 + margin:auto (pure layout, no transform) —
     js/scroll-journey-process.js's own GSAP tweens (xPercent/z/
     rotationY/scale) are the ONLY transform this element ever gets;
     centering it via a CSS transform:translate(-50%,-50%) instead would
     get silently overwritten the first time GSAP touches the inline
     transform (inline style always wins over a stylesheet rule for the
     same property), breaking the anchor point entirely */
  position:absolute;
  inset:0;
  margin:auto;
  height:fit-content;
  z-index:1;
  width:min(88vw, 26rem);
  padding:2.2rem 1.8rem;
  border-radius:16px;
  background:#0d0d10;
  border:1px solid rgba(255,221,122,.16);
  /* BUG FIX: per request to "strengthen depth... add subtle ambient
     occlusion... without making the scene glossy" — the added `inset`
     shadow is a standard CSS-only AO fake (darkens just inside the
     edges, no `filter`). Shared with .live-demo-browser/.testimonial-
     card below for a consistent card language. No ::after reflection
     overlay on this element specifically: it uses transform-style:
     preserve-3d for its GSAP rotationY tilt, and overflow:hidden (which
     a reflection overlay would need for containment) forces preserve-3d
     to flatten in every browser — skipped here to protect the 3D
     depth effect, which matters more for this element. */
  box-shadow:0 40px 90px -30px rgba(0,0,0,.6), inset 0 1px 30px rgba(0,0,0,.28);
  text-align:center;
  transform-style:preserve-3d;
  will-change:transform, opacity;
}
/* BUG FIX: per report, on mobile "you can barely read things because
   one step is seen behind the other" — a narrower panel here gives
   js/scroll-journey-process.js's own wider ±78% mobile xPercent shift
   more room to actually clear the active panel's text instead of
   still sitting mostly underneath it */
@media (max-width:640px){
  .process-panel{ width:min(80vw, 20rem); }
}
@media (prefers-reduced-motion:reduce){
  .process-panel{ position:static; margin:0 auto 1.5rem; }
  .process-stage{ height:auto; perspective:none; }
}

/* ---- the 4 steps, now a plain line instead of pinned to photo
   markers (per direct request — see index.html's own comment on
   #processHotspotLine for the full "loads as you click" behavior).
   .process-hotspot itself is just a centering box around its own dot
   — the label is pulled out via position:absolute below (see
   .process-hotspot-label) so it never affects the box's own height,
   which is what keeps every dot's center sitting exactly on
   .process-hotspot-track's own vertical center for free ---- */
.process-hotspot-line{
  position:relative;
  z-index:5;
  display:flex;
  align-items:center;
  justify-content:space-between;
  width:min(90vw, 32rem);
  margin-top:clamp(1.4rem, 3vh, 2.2rem);
}
.process-hotspot-track{
  position:absolute;
  left:0;
  right:0;
  top:50%;
  height:2px;
  transform:translateY(-50%);
  background:rgba(243,237,226,.16);
  border-radius:999px;
  overflow:hidden;
  z-index:0;
  pointer-events:none;
}
.process-hotspot-track-fill{
  position:absolute;
  inset:0;
  transform:scaleX(0);
  transform-origin:0 50%;
  background:linear-gradient(90deg, var(--gold-soft), var(--gold), rgba(228,205,154,.9));
  box-shadow:0 0 14px rgba(212,175,110,.55);
  transition:transform .7s var(--ease-out);
}
.process-hotspot{
  position:relative;
  z-index:2;
  display:flex;
  align-items:center;
  justify-content:center;
  background:none;
  border:none;
  padding:.9rem;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* per direct request: "add numbers to the dots... do not want people
   to get confused" — sized up from the old plain 10px dot so a digit
   actually reads inside it, centered via flex */
.process-hotspot-dot{
  width:20px;
  height:20px;
  border-radius:50%;
  background:var(--cream);
  box-shadow:0 0 0 0 rgba(243,237,226,.55);
  animation:processHotspotPulse 2.6s ease-in-out infinite;
  display:flex;
  align-items:center;
  justify-content:center;
  transition:width .3s var(--ease-out), height .3s var(--ease-out);
}
.process-hotspot-number{
  font-family:'Inter', sans-serif;
  font-size:.66rem;
  font-weight:800;
  color:#16110a;
  line-height:1;
  pointer-events:none;
  user-select:none;
  transition:font-size .3s var(--ease-out);
}
@keyframes processHotspotPulse{
  0%, 100%{ box-shadow:0 0 0 0 rgba(243,237,226,.4); }
  50%{ box-shadow:0 0 0 10px rgba(243,237,226,0); }
}
/* per direct request: "have number 1 bigger in pulsing, and then
   switch the bigger pulsing to every other number as they interact"
   — process-hero.js's own updateEmphasis() keeps exactly one hotspot
   in this state at a time, starting at #1, moving to the next number
   in sequence each time any step is opened, so the "which one's next"
   cue visibly walks 1→2→3→4→1... instead of every dot pulsing
   identically forever */
.process-hotspot.is-emphasized .process-hotspot-dot{
  width:30px;
  height:30px;
  animation-name:processHotspotPulseBig;
  animation-duration:1.8s;
  animation-timing-function:ease-in-out;
  animation-iteration-count:infinite;
}
.process-hotspot.is-emphasized .process-hotspot-number{
  font-size:.86rem;
}
@keyframes processHotspotPulseBig{
  0%, 100%{ box-shadow:0 0 0 0 rgba(228,205,154,.6); }
  50%{ box-shadow:0 0 0 15px rgba(228,205,154,0); }
}
/* per direct request: each dot "loads" (permanently) the moment it's
   clicked — process-hero.js's own markLoaded() adds this class and
   swaps the number for a checkmark, once per step, forever (not just
   while its reveal panel is open — that's .is-active below) */
.process-hotspot.is-loaded .process-hotspot-dot{
  background:var(--gold);
}
.process-hotspot.is-loaded .process-hotspot-number{
  color:#241b0a;
}
/* per direct request: "make the dots stop pulsing when the viewer is
   in section 2, and when they get back to section 1 they start
   glowing again" — process-hero.js's own IntersectionObserver toggles
   this class purely off real scroll intersection (not a viewport-size
   media query), so it applies identically on desktop and mobile. The
   second, more specific selector guarantees this wins over the
   emphasized dot's own animation shorthand-equivalent above regardless
   of source order (both would otherwise tie on specificity). Targets
   .our-process-section now that the dots live there instead of the
   hero (see index.html's own comment on #ourProcessSection). */
.our-process-section.is-scrolled-away .process-hotspot-dot,
.our-process-section.is-scrolled-away .process-hotspot.is-emphasized .process-hotspot-dot{
  animation-play-state:paused;
}
/* pulled out of the button's own flex flow (position:absolute) so its
   height never counts toward .process-hotspot's box — that's what
   keeps every dot's own vertical center landing exactly on
   .process-hotspot-track's center regardless of whether a label is
   currently visible */
.process-hotspot-label{
  position:absolute;
  top:100%;
  left:50%;
  margin-top:.6rem;
  transform:translate(-50%, -4px);
  font-family:'Inter', sans-serif;
  font-size:.72rem;
  font-weight:600;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--cream);
  opacity:0;
  transition:opacity .25s ease, transform .25s ease;
  white-space:nowrap;
  pointer-events:none;
}
.process-hotspot:hover .process-hotspot-label,
.process-hotspot:focus-visible .process-hotspot-label,
.process-hotspot.is-active .process-hotspot-label{
  opacity:1;
  transform:translate(-50%, 0);
}
.process-hotspot:hover .process-hotspot-dot,
.process-hotspot.is-active .process-hotspot-dot{
  background:var(--gold-soft);
}

/* ---- reveal panel — a full-bleed radial gradient anchored at
   --reveal-x/--reveal-y (set by js/process-hero.js to whichever
   hotspot was actually clicked), so the light reads as spreading
   outward FROM that exact point, plus the step's own title/copy
   fading and sliding in on top. One shared panel, re-targeted per
   click, rather than 4 separate overlays stacking ---- */
.process-reveal{
  position:absolute;
  inset:0;
  z-index:8;
  display:flex;
  align-items:center;
  justify-content:center;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  /* BUG FIX: a bare "circle at X Y" defaults to farthest-corner sizing
     — when X/Y sits near an edge (most of these hotspots do), the
     distance to the FAR corner is huge, so the bright early colour
     stops (0%-46%) still spread most of the way across the section
     regardless of which corner they're anchored to, reading as "always
     centered" rather than "sourced from wherever was clicked." A fixed
     pixel radius keeps the bright zone visually anchored right at that
     point instead of scaling with how off-center it happens to be */
  background:radial-gradient(circle 640px at var(--reveal-x, 50%) var(--reveal-y, 50%), rgba(138,106,31,.55) 0%, rgba(8,12,16,.86) 46%, rgba(4,6,9,.96) 78%);
  transition:opacity .6s var(--ease-in-out), visibility 0s linear .6s;
  /* per direct request: "make the steps swipeable... allow for them to
     be able to swipe into the other step" — pan-y lets real vertical
     scroll gestures pass through untouched; horizontal ones are
     entirely ours (process-hero.js calls preventDefault on those once
     its own direction-lock decides a gesture is horizontal) */
  touch-action:pan-y;
}
.process-reveal.is-visible{
  opacity:1;
  visibility:visible;
  pointer-events:auto;
  transition:opacity .6s var(--ease-in-out), visibility 0s linear 0s;
}
.process-reveal-card{
  position:relative;
  max-width:min(90vw, 30rem);
  text-align:center;
  padding:0 1.5rem;
  transform:translateY(14px);
  opacity:0;
  transition:opacity .55s var(--ease-out) .1s, transform .55s var(--ease-out) .1s;
}
.process-reveal.is-visible .process-reveal-card{
  opacity:1;
  transform:translateY(0);
}
/* per direct request: swipe-triggered step swaps crossfade this inner
   wrapper (not the whole card, which would also flicker the close
   button) on a fast, fixed .16s timing — process-hero.js's own
   swapToStep() toggles this class then updates the text/icon/anchor
   underneath while it's invisible, deliberately NOT reusing the
   card's own slower .55s open/close transition above */
.process-reveal-content{
  transition:opacity .16s ease;
}
.process-reveal-content.is-swiping{
  opacity:0;
}
.process-reveal-close{
  position:absolute;
  top:-2.6rem;
  right:0;
  width:2.2rem;
  height:2.2rem;
  border-radius:50%;
  border:1px solid rgba(243,237,226,.4);
  background:rgba(255,255,255,.06);
  color:var(--cream);
  font-size:1.3rem;
  line-height:1;
  cursor:pointer;
  transition:background .2s ease, border-color .2s ease;
}
.process-reveal-close:hover{ background:rgba(255,255,255,.14); border-color:var(--cream); }
/* per direct request: "add icons to the steps, to show more life" —
   process-hero.js swaps this single shell's innerHTML per step (see
   STEPS[key].icon); stroke:currentColor so the color/glow live here,
   not baked into each icon's own markup */
.process-reveal-icon{
  width:42px;
  height:42px;
  margin:0 auto .9rem;
  display:block;
  color:var(--gold-soft);
  filter:drop-shadow(0 0 10px rgba(228,205,154,.5));
}
.process-reveal-index{
  display:block;
  font-family:'Space Grotesk', sans-serif;
  font-size:.85rem;
  font-weight:600;
  letter-spacing:.2em;
  color:var(--gold-soft);
  margin-bottom:.9rem;
}
/* per direct request: "make the titles inside the steps to have the
   same metallic aqua tone" — identical technique to .process-hero-
   title above (shares its processHeroTitleShine keyframe), so the
   reveal panel's own title reads as the same premium brushed metal,
   not a separate flat cream fill */
.process-reveal-title{
  font-family:'Fraunces', serif;
  font-style:italic;
  font-weight:400;
  font-size:clamp(2rem, 4.5vw, 3rem);
  margin-bottom:1.1rem;
  color:var(--gold-soft);
  background:linear-gradient(115deg,
    #faf8ee 0%,
    var(--gold-soft) 16%,
    var(--gold) 34%,
    #fffdf5 50%,
    var(--gold-deep) 66%,
    var(--gold-soft) 84%,
    #faf8ee 100%);
  background-size:220% 100%;
  background-position:0% 50%;
  -webkit-background-clip:text;
  background-clip:text;
  -webkit-text-fill-color:transparent;
  text-shadow:0 4px 30px rgba(4,8,12,.35);
  animation:processHeroTitleShine 7s ease-in-out infinite;
}
.process-reveal-text{
  font-family:'Inter', sans-serif;
  font-size:1rem;
  line-height:1.6;
  color:rgba(243,237,226,.82);
}

/* mobile portrait tweaks — kept scoped to portrait only (matches this
   site's established convention: rotate to landscape and a phone just
   gets the same layout as a real desktop, never a sideways-squeezed
   portrait layout) even though there's no longer a separate mobile
   photo driving that distinction */
@media (max-width:860px) and (orientation:portrait){
  .process-hero{
    min-height:100svh;
  }
  /* per direct request: "Building Websites" then enter, "that Matter"
     on mobile — the shared <br> already splits it there, but the old
     12ch max-width was narrow enough that "Building Websites" itself
     wrapped again into "Building" / "Websites", giving 3 lines instead
     of 2. Shrinking the font size (rather than just widening max-width)
     is what actually lets the full "Building Websites" line fit */
  .process-hero-title{
    max-width:20ch;
    font-size:clamp(1.9rem, 8.6vw, 2.6rem);
  }
  .process-hero-cta{
    padding:.75rem 1.5rem;
    font-size:.68rem;
    letter-spacing:.1em;
  }
  /* per direct request: "make [Start a project] show on mobile on the
     top right corner" — it was pinned bottom-right to avoid crowding
     the (now-removed) short-hero crop; the new portrait mobile photo
     doesn't need that crop anymore, so this just inherits the same
     top-right spot the desktop button already uses, sized down */
  .process-hero-start{
    top:1.1rem;
    right:1.1rem;
    padding:.55rem 1.1rem;
    font-size:.68rem;
  }
  .process-hotspot-label{ display:none; }
  .process-reveal-card{ padding:0 1.25rem; }
}

/* per iPhone-heat investigation: sitewide "infinite" CSS keyframe
   animations (glow pulses, shines, sweeps) have nothing to ever stop
   them, unlike this codebase's own JS-driven effects (hero-matrix.js's
   canvas, cursor-trail.js, scroll-dolly.js) which already pause off-
   screen. An off-screen infinite animation is still real, sustained
   compositor/paint work for zero visible benefit. js/anim-idle.js
   toggles this class via IntersectionObserver on each animated
   section/element once it scrolls out of view — !important because it
   needs to reliably beat ~15 differently-specific existing selectors
   without editing any of them. */
.is-anim-idle,
.is-anim-idle::before,
.is-anim-idle::after{
  animation-play-state:paused !important;
}

