:root{
  --bg:#000000; --card:#090909; --border:#1f1f1f;
  --muted-surface:#121212; --input:#1b1b1b;
  --fg:#fafafa; --muted:#b4b4b4; --muted-2:#999999;
  /* --lime now holds Chuck's real brand blue, not literally lime green, kept the variable
     name to avoid renaming ~15 references across the file. Originally sampled from a
     cert-badge graphic on rckstrmedia.com; upgraded this pass to #5d97fd, sampled directly
     from the actual RCKSTR logo mark (go.rckstrmedia.com/assets/rckstr-logo-*.avif), which is
     a more authoritative brand source than a third-party badge. It also tests better:
     7.31:1 on black (vs. 5.28:1 for the old value) and 6.58:1 for --lime-ink text on top of
     it in buttons (vs. 4.75:1), both comfortably clearing AA and now into AAA territory for
     normal text on black. --lime-ink itself is untouched, it's also reused on the
     Shopify-green icon badge from an earlier WAVE fix (8.8:1 there), changing it would have
     risked breaking that. Yellow/lime stays reserved for Chuck's ad-specific landing pages
     per his own notes, this is the core/premium site, so it gets his real blue instead. */
  /* --electric (a separate hand-picked #5674ff) got collapsed into --lime this pass:
     two independently-chosen hex values that happened to be close is exactly the kind
     of drift a real color system avoids, one brand blue, tints/shades derived from it
     via color-mix (already the pattern everywhere else on this site), not a second
     literal hex sitting next to the first one. */
  --lime:#5d97fd; --lime-ink:#07004b;
  /* v82: --premium used to be a light cream/tan palette (#f4ead5 bg, dark-brown text), a
     leftover from before the site settled on its dark, brand-blue aesthetic, the same
     kind of light-background break that got the #problem section cut entirely back in
     v67 for reading as "an abrupt light section in an otherwise dark, confident page."
     Eli flagged the one place it was still showing up (the .portfolio-cta/.post-cta-band
     closing CTA bands, present on nearly every page since they share this one stylesheet)
     and asked for the site's own faded blue instead. Kept the variable names rather than
     rename ~6 references (same call made for --lime when its meaning changed in v48/v51),
     just swapped what they hold: a low-opacity blue wash over the site's card-dark
     background instead of a solid light color, same "ambient blue glow" language already
     used for the hero/results/creative-teaser sections, so this now reads as one more dark
     section with a blue accent instead of a flash of cream breaking the page. */
  --premium:color-mix(in oklab, var(--lime) 16%, var(--card));
  --premium-border:color-mix(in oklab, var(--lime) 40%, var(--border));
  --premium-fg:var(--fg);
  --radius:.75rem; --radius-sm:.4rem;
  --ease-out:cubic-bezier(.22,.61,.36,1); --ease-smooth:cubic-bezier(.16,1,.3,1);
  --ease-spring:cubic-bezier(.34,1.56,.64,1); --ease-expo:cubic-bezier(.87,0,.13,1);
  --safe-top:env(safe-area-inset-top); --safe-bottom:env(safe-area-inset-bottom);
  --nav-h:76px;
}
*{box-sizing:border-box; margin:0; padding:0;}
html{background:var(--bg); scroll-behavior:smooth; scroll-padding-top:calc(var(--nav-h) + 40px); -webkit-tap-highlight-color:transparent;}
body{
  font-family:"Inter",system-ui,-apple-system,sans-serif; background:var(--bg); color:var(--fg);
  line-height:1.6; font-size:16px; -webkit-font-smoothing:antialiased;
  padding-bottom:calc(64px + env(safe-area-inset-bottom)); overflow-x:hidden;
}
@media(min-width:900px){ body{ padding-bottom:0; } }

/* v132: explicit sitewide focus-visible ring. Nothing was suppressing the browser's
   default outline before this (no `outline:none` reset anywhere), so keyboard focus was
   never actually invisible, but it was also never intentionally styled to match the
   brand. Adding one explicit, brand-blue ring here closes that gap for every link,
   button, input, and tabbable element sitewide, one rule instead of stumbling into
   inconsistent defaults per browser. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible{
  outline:2px solid var(--lime);
  outline-offset:2px;
  border-radius:4px;
}


img{max-width:100%; display:block;}
a{color:inherit; text-decoration:none;}
button{font-family:inherit; border:none; background:none; color:inherit; cursor:pointer;}
.wrap{max-width:1180px; margin:0 auto; padding:0 20px;}
@media(min-width:768px){ .wrap{padding:0 40px;} }

h1,h2,h3{font-weight:800; letter-spacing:-.02em; line-height:1.05;}
/* v105: Eli sent two screenshots pointing at section headers sitewide (creative-teaser,
   What We Do, Reviews) and said "lets center all the websites headers and lettering
   titles." These three classes (eyebrow/h2.section-title/.section-sub) are the shared
   pattern behind nearly every section on every page, so centering them here at the base
   covers all of them in one place instead of hunting down each section individually.
   Checked first that no section relies on this text sitting beside something else in a
   two-column layout (results/approach/reviews/book/creative-teaser all run the heading
   full-width on its own line, content grid below it, not beside it), so centering the text
   doesn't orphan anything. .eyebrow changed from inline-flex (shrink-wraps to content,
   position depends on an ancestor's text-align, easy to get wrong across different section
   markups) to a block-level flex with width:fit-content + margin:auto, which centers itself
   reliably regardless of what wraps it. This same margin-auto/text-align technique was
   already proven on .page-hero (a single interior-page hero already using it), just applied
   as the sitewide default now instead of a page-hero-only override. */
/* v202: dropped the decorative dash (::before) per Eli's ask. It was centering the
   dash-plus-text as one flex unit, not the text alone, so the label itself sat off-center
   by half the dash's width + gap. Simplified to plain centered text, no flex needed. */
.eyebrow{
  display:block; width:fit-content;
  margin:0 auto 14px; font-size:12px; font-weight:700;
  letter-spacing:.14em; text-transform:uppercase; color:var(--lime); text-align:center;
}
h2.section-title{ font-size:clamp(1.8rem,6vw,2.4rem); max-width:700px; margin-left:auto; margin-right:auto; text-align:center; }
.section-sub{ color:var(--muted); font-size:clamp(1rem,2.6vw,1.1rem); max-width:600px; margin-top:14px; line-height:1.6; margin-left:auto; margin-right:auto; text-align:center; }
section{ padding:56px 0; position:relative; }
@media(min-width:768px){ section{ padding:84px 0; } }
.section-compact{ padding:32px 0; }
@media(min-width:768px){ .section-compact{ padding:40px 0; } }

/* buttons */
.btn{
  position:relative; overflow:hidden; display:inline-flex; align-items:center; justify-content:center;
  gap:8px; padding:15px 28px; border-radius:var(--radius-sm); font-weight:700; font-size:15px;
  transition:transform .2s var(--ease-out); white-space:nowrap;
}
.btn:active{ transform:scale(.97); }
.btn-primary{ background:var(--lime); color:var(--lime-ink); }
/* hover-fill was still hardcoded to the old lime-green accent (#a8e015) from before the
   v48 blue rebrand, CSS variables don't retroactively fix literal hex values, so every
   primary button flashed bright green on hover/press even though the rest of the site had
   already moved to the real brand blue. Swapped to a lightened tint of the same --lime
   blue via color-mix, so hovering now reads as "this blue, but brighter," consistent with
   the rest of the palette instead of an unrelated leftover color. */
.btn-primary::before{
  content:""; position:absolute; inset:0; background:color-mix(in oklab, var(--lime) 65%, #fff 35%); transform:translateY(100%);
  transition:transform .35s var(--ease-expo); z-index:0;
}
.btn-primary:hover::before{ transform:translateY(0); }
.btn-primary > *{ position:relative; z-index:1; }
.btn-block{ width:100%; }

/* nav, permanently visible, CTA always in view */
.nav{
  position:fixed; top:0; left:0; right:0; z-index:60; padding-top:max(14px,var(--safe-top));
  padding-bottom:14px; background:rgba(0,0,0,.8); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  border-bottom:1px solid var(--border);
  /* v180: paired with nav-scroll.js, which toggles .nav-hidden on scroll direction. */
  transition:transform .3s var(--ease-out);
}
.nav.nav-hidden{ transform:translateY(-100%); }
@media(prefers-reduced-motion:reduce){ .nav{ transition:none; } .nav.nav-hidden{ transform:none; } }
/* grid instead of flex+space-between: with only 3 top-level items (logo, links, right-side
   group) and an unequal-width logo vs. right-side group, space-between never actually put
   the links group in the visual center of the bar, just wherever the leftover space landed
   it. A 3-column grid (auto / 1fr / auto) with the middle column's content centered inside
   its own track keeps the 3 nav links genuinely centered regardless of how wide the logo or
   the right-side group are. */
.nav-inner{ display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:16px; }
/* v143: Eli flagged the nav logo as small/low quality. The source file itself is plenty
   sharp (1860x320 native, checked directly), the problem was purely display size: at
   22px tall, the logo's own detail (the halftone dot graphic behind the star, the
   lightning-bolt cutout) has too few pixels to resolve cleanly and reads as blurry/soft
   regardless of source resolution. Bumped to 30px, ~36% bigger, still comfortably inside
   the nav bar's existing padding (14px top/bottom, height:auto row via align-items:center,
   so this doesn't require touching the bar's own padding). */
.nav-logo img{ height:30px; width:auto; }
.nav-links{ display:none; gap:28px; font-size:14px; font-weight:500; color:var(--muted); align-items:center; justify-self:center; }
.nav-links a{ transition:color .2s var(--ease-out); }
.nav-links a:hover{ color:var(--fg); }
@media(min-width:900px){ .nav-links{ display:flex; } }

/* the desktop bar grew to 10 links across this project (Results/What We Do/Services/
   Creative Portfolio/Blog/Case Studies/Meet The Team/Pricing/Reviews/FAQ), enough to wrap
   onto two lines at normal desktop widths, screenshotted and flagged directly. Keeps the
   three highest-traffic links inline (What We Do, renamed to "Services" this pass, plus
   Creative Portfolio and Pricing) and tucks the rest under one "More" trigger instead, same
   links, still one click away, just not all fighting for space in a single row.

   "More" used to sit inline right after Pricing, inside .nav-links itself. Moved out into
   its own .nav-right group alongside the Book a Call button (and the mobile hamburger,
   which only one of these two ever shows at a time) so it reads as a secondary utility
   control docked at the far right, not a 4th item competing with the 3 centered links. */
.nav-right{ display:flex; align-items:center; gap:16px; justify-self:end; }
@media(max-width:899px){ .nav-more{ display:none; } }
.nav-more{ position:relative; }
.nav-more-btn{
  display:flex; align-items:center; gap:5px; font-size:14px; font-weight:500; color:var(--muted);
  transition:color .2s var(--ease-out);
}
.nav-more-btn:hover, .nav-more-btn.open{ color:var(--fg); }
.nav-more-btn svg{ width:11px; height:11px; transition:transform .2s var(--ease-out); }
.nav-more-btn.open svg{ transform:rotate(180deg); }
.nav-more-menu{
  position:absolute; top:calc(100% + 16px); right:0; min-width:196px; padding:8px;
  background:rgba(9,9,9,.98); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  border:1px solid var(--border); border-radius:var(--radius); box-shadow:0 18px 40px rgba(0,0,0,.45);
  opacity:0; pointer-events:none; transform:translateY(-6px);
  transition:opacity .2s var(--ease-out), transform .2s var(--ease-out);
  display:flex; flex-direction:column; gap:2px;
}
.nav-more-menu.open{ opacity:1; pointer-events:auto; transform:translateY(0); }
.nav-more-menu a{
  padding:10px 12px; border-radius:6px; font-size:14px; font-weight:500; color:var(--muted);
  transition:background .15s var(--ease-out), color .15s var(--ease-out);
}
.nav-more-menu a:hover{ background:var(--card); color:var(--fg); }
.nav-cta{ padding:12px 18px; font-size:13.5px; min-height:44px; }
/* dropped entirely on mobile, not just faded on scroll: it's already covered by the hero
   CTA, the mobile menu's own CTA, and the sticky bottom bar, three's plenty without a
   fourth in the top nav too. Desktop keeps it since there's no bottom bar there. */
@media(max-width:899px){ .nav-cta{ display:none; } }

/* mobile quick-jump menu: this is a single long-scroll page, but it's long enough (9
   sections) that mobile had zero way to jump straight to Reviews or FAQ without manually
   scrolling past everything. Same links already in the desktop nav, just exposed via a
   hamburger toggle below 900px instead of hidden with no equivalent. */
.nav-menu-btn{
  display:flex; align-items:center; justify-content:center; width:40px; height:40px;
  margin-left:4px; flex-shrink:0;
}
@media(min-width:900px){ .nav-menu-btn{ display:none; } }
.nav-menu-btn svg{ width:22px; height:22px; }
.nav-menu-btn .icon-close{ display:none; }
.nav-menu-btn.menu-open .icon-menu{ display:none; }
.nav-menu-btn.menu-open .icon-close{ display:block; }
.mobile-menu{
  position:fixed; top:0; left:0; right:0; bottom:0; z-index:70; background:rgba(0,0,0,.97);
  display:flex; flex-direction:column; padding:calc(var(--nav-h) + var(--safe-top) + 20px) 20px 30px;
  opacity:0; pointer-events:none; transform:translateY(-8px);
  transition:opacity .25s var(--ease-out), transform .25s var(--ease-out);
  overflow-y:auto; -webkit-overflow-scrolling:touch;
}
.mobile-menu.open{ opacity:1; pointer-events:auto; transform:translateY(0); }
@media(min-width:900px){ .mobile-menu{ display:none; } }
.mobile-menu-links{ display:flex; flex-direction:column; }
.mobile-menu-links a{
  font-size:1.3rem; font-weight:700; padding:16px 0; border-bottom:1px solid var(--border);
}
.mobile-menu-cta{ margin-top:24px; }

/* logo strip, sits under the hero video now, not at the very top of the page.
   Rebuilt v12: the old single screen-blended glow blew out into a flat white void
   instead of highlighting a logo. Real fix uses two identical, perfectly-synced copies
   of the same scrolling strip: a legible dim base layer underneath, and a full-brightness
   copy on top that's only visible through a narrow mask window in the center, so exactly
   one logo at a time reads crisp and bright as it passes through, everything else stays
   a legible (not washed-out) dim grey. */
/* v156: Eli asked for "TRUSTED BY" to sit a little lower, more air between the CTA
   button and the label above the logos. */
.logo-strip{ padding-top:58px; position:relative; }
/* v160: Eli's crop of the mobile "Trusted by" band showed the crowd still clearly
   readable right behind the label, wanted that specific area faded solid black instead,
   a clean dark lead-in to the label rather than busy footage behind it. Rather than
   re-tune the whole hero-bg-overlay gradient again and risk undoing v159's crowd
   visibility earlier up the page, scoped this to the one element it's actually about:
   a ::before on .logo-strip itself, fading from transparent above (blends into whatever's
   showing higher up) to dark by the time "Trusted by" and the logos are on screen.
   v161: v160's version darkened the FULL width evenly, Eli liked the darkness directly
   behind the label but wanted the crowd/cars still visible at the left/right edges of
   the phone at that same height, not blacked out everywhere. Switched from a flat
   linear-gradient to a radial vignette, dark at center (right behind the text), fading
   out toward the sides so the edges stay lit with the video. That also meant fixing how
   this element gets its full-bleed width: the old `left/right:-9999px` trick makes the
   box itself enormous, so a horizontal percentage like "50%" or "58% wide" inside a
   radial-gradient would be measured against that huge box instead of the real screen,
   never actually reaching the edges. `left:50%; width:100vw; transform:translateX(-50%)`
   pins the box to the true viewport width instead, so the gradient's percentages line up
   with the real edges of the phone. */
/* v163: Eli's screenshot showed a visible line cutting across the CTA button itself, and
   two more near the stats. Root cause of the CTA one: the ellipse above faded to
   "transparent" at 92% of its own radius, not 100%, so right at the box's own top edge
   (-40px above .logo-strip, reaching up close to the button) there was still real
   leftover alpha (~.3-.35) instead of true zero. A gradient can only fade within its own
   element's box, so wherever that box's edge lands, if the value isn't already zero by
   then, you get a hard cutoff from "some darkening" to "none at all" right at that
   boundary, exactly a visible line. Pulled the top overshoot in (-40px->-10px) and pushed
   the ellipse's own transparent stop out to 100% (was 92%).
   v165: Eli reported the line still there after v163. Audited every other pseudo-element
   on the page (hero-bg-overlay's own radial layers, the mobile photo-reveal ::after, the
   footer/case-card/reviews glows) for the same "box edge with leftover alpha" bug, none
   of them have it, .logo-strip::before was the only one with a bounded box rather than a
   full inset:0 span. Removed the top overshoot entirely (-10px->0) so this element starts
   exactly at .logo-strip's own top edge and physically cannot extend upward far enough to
   touch the CTA button or anything above it, regardless of any remaining math. If the
   line is still visible after this, it's very likely one of two things I can't fix in
   CSS: a stale cached deploy (this has happened once already this session) or an actual
   railing/barrier in the Times Square footage itself passing behind the button as the
   video plays, not a rendering bug at all. Worth a hard refresh and a fresh screenshot to
   tell which. */
@media(max-width:699px){
  .logo-strip::before{
    content:"";
    position:absolute; top:0; bottom:-60px; left:50%; width:100vw; transform:translateX(-50%);
    background:radial-gradient(ellipse 55% 78% at 50% 55%, rgba(0,0,0,.95) 0%, rgba(0,0,0,.7) 45%, transparent 100%);
    pointer-events:none;
    z-index:0;
  }
  .trust-label, .trust-bar{ position:relative; z-index:1; }
}
.trust-label{ text-align:center; font-size:12px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); padding-bottom:20px; }
.trust-bar{
  /* v150: bumped 72px->88px for more visual weight. v152: pushed to 108px per "as big as
     they were or bigger." v153: at 108px only ~4 logos fit in the masked width at once,
     Eli said he liked the row spanning further across the page, more logos visible at a
     time, closer to how it read at the original size. Settled on 96px, a scale factor of
     4/3 off the original 72px base, big enough to still read as an intentional upsize but
     not so big it crowds the row down to a handful of marks. Container height must keep
     matching the img height or the logos clip against this fixed box. */
  /* v213: Eli asked for this row to run more horizontal, longer across the screen,
     same full-bleed breakout already used for .creative-teaser-track (v138): 100vw plus
     matching negative margins pulls it out to the true viewport width regardless of
     .hero .wrap's own 1180px cap, .trust-label above stays normally centered since only
     .trust-bar itself gets this treatment. Widens the masked viewing window so more logos
     are visible at once mid-scroll, same animation/content, just a bigger stage.
     v214: full 100vw read as logos running flush to the true screen edge with no visible
     margin, Eli asked for it not to go full screen, fade out a few inches before the edge
     instead. `--tb-inset` pulls the box in from each true viewport edge (clamps between
     32px on narrow phones and 140px on wide desktops, scaling with viewport via `8vw` in
     between) rather than sitting flush at 0, and the existing mask-image fade below still
     completes within that inset margin, so logos visibly fade to nothing with real
     breathing room left over before the frame edge, not right at it. */
  --tb-inset: clamp(32px, 8vw, 140px);
  position:relative; overflow:hidden;
  width:calc(100vw - (var(--tb-inset) * 2));
  margin-left:calc(50% - 50vw + var(--tb-inset)); margin-right:calc(50% - 50vw + var(--tb-inset));
  height:96px;
  pointer-events:none; user-select:none; -webkit-user-select:none;
  /* v223: fade edges via mask-image again, but a DIFFERENT implementation than the old
     desktop rule (kept in history via v221/v222 comments above, both now stale/removed):
     fixed percentage stops only, zero calc() anywhere inside the gradient. The old
     "reported twice on a real phone as a fully blank trust bar" failure was specifically
     tied to calc() inside the mask's gradient stops, this has none, just plain %.
     Why a mask and not a painted overlay (the thing that just failed as "boxes around the
     logos" in v222): a mask only reduces THIS element's own alpha at the edges, it doesn't
     paint any new color on top. The logos themselves fade to nothing while whatever's
     behind (`.hero-bg-overlay` + the real video, at whatever brightness it happens to be)
     just shows through unchanged, there's no separate opaque layer that can ever be a
     mismatched shade against its surroundings, the exact failure mode v222 just fixed by
     removing the overlay. This should get the visual fade back without reintroducing that
     bug. Applies at every breakpoint now, no mobile carve-out, if a real device shows it
     failing (mask rendering as fully hidden instead of faded) this is the first place to
     look, the fallback is the same "no mask, hard edge, overflow:hidden clips it" state
     this file was shipping just before v223. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%);
}
/* gap tightened from 64px, then 46px, still reported as a visible empty stretch at the
   loop seam (the join between the two duplicated copies). Measured the actual sprite
   pixels this round instead of guessing again: the logos inside the image itself sit
   roughly 22px apart at render scale, but the CSS gap at the seam was 46px, about double
   the image's own internal spacing, so the seam always read as an odd blank gap once
   per 34s loop no matter how the mask/timing lined up. Matching the seam gap to the
   image's real internal spacing removes the "restart" look entirely instead of just
   hiding it. */
/* v88: Eli reported the strip "doesn't start populated, you have to wait for it to cross
   the whole screen." Real bug, not an impression: the track is exactly two copies of the
   sprite (width 2W), centered via left:50% + translate(-50%,-50%), so the container's own
   center point sits at exactly W, i.e. precisely on the seam between copy 1 and copy 2.
   That means the animation's own 0% frame, the frame every page load starts on, is
   centered on the one spot in the whole loop that's the gap between copies, not on any
   logo. A negative animation-delay jumps straight to a frame partway through the cycle
   without changing the loop's math or timing at all, landing on a populated stretch of
   logos immediately instead of the seam. */
.trust-bar-track{
  position:absolute; top:50%; left:50%; display:flex; align-items:center; width:max-content;
  /* v188: duration 34s->64s. The sprite grew from 2578px to 4822px wide (10 new client
     logos appended), and this animation always travels exactly one sprite-width per loop,
     so at the old 34s the strip would have scrolled nearly twice as fast as the speed
     that's been tuned and approved across many rounds. Scaled the duration by the same
     ratio the width grew (4822/2578 x 34s = ~64s) so the real px/second pace on screen is
     unchanged, more logos, same speed. This 64s base applies to the single 21-logo strip
     the services/social pages still use; the homepage's two split rows override it below
     (v189), each scaled to its own narrower sprite so all three variants share one real
     px/second pace. */
  transform:translate(-50%,-50%); animation:trust-scroll 64s linear infinite; animation-delay:-10s; will-change:transform;
}
/* v140: the seam gap used to live on the flex container as `gap:22px`, which only inserts
   ONE gap total (between the two <img> copies). translateX(-50%) moves by half of the
   track's total width, i.e. one image (2578px) plus half that one gap (11px) = 2589px,
   not the full 2578+22=2600px a true seamless loop needs. Every 34s the animation was
   landing 11px short of where copy 2 actually starts, so the loop-restart snapped visibly
   instead of continuing smoothly, reading as the strip "ending" rather than looping. Fixed
   by moving the spacing onto margin-right on every img instead of a shared flex gap: now
   BOTH copies carry their own trailing 22px, so the track's total width is exactly
   2 x (image + margin), and -50% lands exactly on one full period every time, at any
   breakpoint (mobile's margin override below stays in sync the same way). */
/* v150/v152/v153: height 72->88->108->96px (see .trust-bar above). The seam-loop math
   from v140 depends on margin-right matching the sprite's own internal logo spacing AT
   THE RENDERED SIZE, computed straight off the original base (22px margin at 72px tall)
   rather than compounding each version's rounding: scale factor 96/72 = 4/3 exactly, so
   margin-right = 22 x 4/3 = ~29px. Skipping this scaling would reintroduce the exact
   stutter v140 fixed, just at a new size. */
.trust-bar-track img{ height:96px; width:auto; margin-right:29px; filter:grayscale(1) brightness(1.05) contrast(1.05); opacity:.75; }
@keyframes trust-scroll{
  from{ transform:translate(-50%,-50%) translateX(0); }
  to{ transform:translate(-50%,-50%) translateX(-50%); }
}
/* v189: homepage dual-row trust bar. Row 1 (original 11 logos, 2578px sprite) and row 2
   (the 10 newest, 2220px sprite) scroll in opposite directions, the "counter-scroll"
   roster effect Eli asked for. Durations are each scaled off the same tuned base pace
   (34s per 2578px, the speed approved across many earlier rounds): row 1 keeps 34s
   exactly, row 2 gets 34 x 2220/2578 = ~29s, so both rows and the 64s single-strip
   variant on other pages all move at the same real px/second. Row 2 reverses via
   animation-direction on both of its tracks (base + bright spotlight copy), keeping the
   two layers of that row perfectly synced the same way they already are on row 1, and
   gets its own animation-delay so its starting frame lands on a populated stretch of
   logos, same reasoning as the v88 fix.
   v215: row 2's sprite grew 2220px->2949px (7EVIN7INS + Jared Benjamin appended, Eli's
   reference screenshot). Scaled this row's own duration by the same ratio the width grew
   (29s x 2949/2220 = ~38.5s) rather than re-deriving from row 1's base pace, keeps row 2
   internally consistent with its own already-tuned per-px speed exactly like v188 did for
   the single-strip variant when ITS sprite grew. Real on-screen px/second is unchanged. */
.trust-bar-row1 .trust-bar-track{ animation-duration:34s; }
.trust-bar-row2 .trust-bar-track{ animation-duration:38.5s; animation-direction:reverse; animation-delay:-7s; }
/* v190: Eli's screenshot showed the two rows sitting too far apart. The gap isn't mostly
   the margin, it's dead space inside each bar: the bar is a fixed 96px tall (matching the
   img render height) but typical logos only occupy ~15-43px of that, vertically centered,
   so each bar carries ~26px+ of empty space on both edges before any margin is added.
   A negative margin pulls the rows through that built-in padding. -22px still leaves
   comfortable clearance even for the tallest logos in both rows (worst case would need
   about -52px before actual overlap), so this reads as "closer together" without any
   collision risk. */
.trust-bar + .trust-bar{ margin-top:-22px; }
@media(max-width:899px){
  /* mobile bars are 83px tall with proportionally smaller logos, same logic, gentler pull.
     v215: bumped from 599px to 899px to match the nav's own mobile-hamburger breakpoint
     (.nav-links/.nav-menu-btn switch at 900px) — see note below on why the mismatch mattered. */
  .trust-bar + .trust-bar{ margin-top:-14px; }
}
@media(prefers-reduced-motion:reduce){ .trust-bar-track{ animation:none; } }
@media(max-width:899px){
  /* v79: Eli's phone screenshot showed only a sliver of one logo readable in the whole
     "Trusted by" row, everything else empty dark space. Not a repeat of the old "fully
     blank" mask bug above (mask is already off here), this is a legibility/framing issue:
     50px was small for a wordmark being read on a phone at arm's length, and the old 44px
     gap meant a screenshot could land in a stretch with only one mark in frame. Bumped
     both so more of the row reads clearly at any given moment.
     v215: this whole block's breakpoint moved from 599px to 899px. Root cause of Eli's
     persisting real-device "big gaps/narrow inset row" bug (after the loading=eager fix
     already solved the blank-image race): the nav's own mobile-hamburger switch is at
     900px, but this block — which turns off the desktop mask-image fade and switches to
     the simple ::before/::after edge overlays below — only kicked in under 599px. That
     left a 600-899px dead zone where the nav was already showing mobile UI but the
     trust-bar still ran the full desktop mask-image fade stacked on top of the --tb-inset
     clamp margin, compounding into a much narrower, gappier-looking strip than intended.
     Confirmed via headless measurement at 750/768px: navMenuBtnVisible was 'flex' (mobile
     nav active) while maskImage was still the full desktop gradient, not 'none'. Matching
     both breakpoints at 900px removes the dead zone entirely. */
  .trust-bar{
    /* v150/v152/v153: scaled with the desktop bumps above, off the same original 62px
       base each time so rounding doesn't compound. v153's 4/3 scale factor: 62 x 4/3 =
       ~83px. */
    height:83px;
  }
  /* seam gap computed straight off the original 62px/18px base, same 4/3 scale factor
     used on desktop: margin-right = 18 x 4/3 = 24px. v140: moved from the flex `gap`
     prop to `margin-right` on the img, same reasoning as desktop above. */
  .trust-bar-track img{ height:83px; margin-right:24px; }
}
/* v88: the v84 mobile shimmer (a sweeping blue-white light band across the trust bar) got
   a direct "I hate that blue line thing" from Eli, so it's gone. The "make mobile feel
   alive" goal it was trying to solve moved to the hero photo itself instead, see the
   dark-to-light reveal on .hero-bg-overlay below. */
/* v221: .trust-bar-bright disabled at every width, not just mobile. Its entire purpose was
   a masked radial-gradient "spotlight" window, the same mask-image feature just removed
   from .trust-bar above for reliably failing on real devices. A masked duplicate track
   sitting at z-index:2 directly over the base track is exactly the shape of element that
   renders as a solid blacked-out patch if its mask silently fails instead of falling back
   to "no mask" (opaque duplicate) or "fully hidden" (invisible) — matches what Eli's
   screenshots show almost exactly: an opaque rectangular patch sitting mid-row. Left the
   HTML markup in place sitewide (harmless, aria-hidden, lazy-loaded, zero layout cost while
   display:none), reviving this is just deleting this one rule if a confirmed-safe masking
   approach turns up later. */
.trust-bar-bright{ display:none; }
.trust-bar-bright img{ filter:none; opacity:1; }

/* real, verifiable third-party credentials, shown in their own brand colors since these
   are trademarked partner/certification badges, not decorative logos, recoloring them
   to match the site palette would misrepresent the mark. Lives in the footer, not the
   hero, this is a quiet "yes we're really certified" footnote, not a hero-level trust
   claim, and it doesn't compete with the one thing the hero is trying to get a click on.
   Nested inside .footer-bottom now instead of carrying its own border-top/margin-top,
   that was a second full divider stacked directly under the footer-top one, all it did
   was add height. One divider for the whole bottom block instead of two. Badges sized up
   too, they were reading small/easy to miss against everything else in the footer. */
.cert-row{ display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:16px; }
/* v227: solid white pill swapped for the same dark-card-plus-border treatment .platform-tag
   already uses elsewhere on the site, both per Eli's ask for "non white backgrounds" and
   because it fixes a real bug the white pill was hiding: Varos and IAS's badge art is white
   logo-on-transparent (built to sit on a dark surface), so on the old #fff chip they were
   rendering as blank white rectangles, only Meta's self-contained circle badge and Google's
   full-color wordmark actually showed. Confirmed by compositing all the partner badges onto
   both a white and this site's real card-dark background: every one of them (Meta, Google,
   Varos, Rebuy, IAS) reads clearly here, none needed the white surface to begin with. */
.cert-chip{
  display:flex; align-items:center; justify-content:center; background:var(--card);
  border:1px solid var(--border); border-radius:12px; padding:11px 18px;
  transition:transform .2s var(--ease-out), border-color .2s var(--ease-out);
}
a.cert-chip:hover, a.cert-chip:focus-visible{ transform:translateY(-2px); border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); }
.cert-chip img{ display:block; width:auto; }
@media(max-width:599px){
  .cert-row{ gap:10px; }
  .cert-chip{ padding:8px 12px; }
  .cert-chip img{ height:28px !important; }
}

/* v212: back to the simple CSS-only sticky pin (GSAP+Lenis pulled back out per Eli's ask,
   too much back-and-forth for a plain "keep the video behind the next section as it
   scrolls up" effect). `.pin-media` holds just the video and sticks to the top of the
   viewport for its whole `.pin-wrap` parent's scroll range; `.pin-overlay` is pulled back
   up over it by a negative margin equal to one viewport height so `.hero`'s content sits
   directly on top of the sticky video, then `.creative-teaser` (opaque bg) scrolls up
   over both as normal, covering the pinned video, curtain-style. No JS. */
.pin-wrap{ position:relative; }
.pin-media{ position:sticky; top:0; height:100svh; z-index:0; overflow:hidden; }
.pin-overlay{ position:relative; z-index:1; margin-top:calc(-1 * 100svh); }

/* hero */
/* v152: Eli's screenshot showed the content block sitting high, hugging the top padding,
   with a big block of empty video/black space left over below the stats. Rather than
   guess a bigger padding-top number, made the whole content column vertically centered
   in a full-height hero: min-height:100svh + flex centering distributes that same
   leftover space evenly above and below instead of it all pooling at the bottom, which
   reads as "lower" without needing to hand-tune a px value that'd only be right at one
   viewport height. */
.hero{ position:relative; padding-top:calc(var(--nav-h) + var(--safe-top) + 32px); padding-bottom:48px; overflow:hidden; isolation:isolate; display:flex; flex-direction:column; justify-content:center; min-height:100svh; }
/* Pilothouse-style full-bleed background: a real video (Chuck's billboard-ad footage)
   sitting behind the copy, scaled up so it fills the frame with room to spare (the "zoom
   in more" ask), and darkened hard so white text stays readable over whatever's playing.
   Replaces the old subtle radial-glow ::before treatment entirely, a real video behind
   dark text does that job better than a flat gradient ever did. */
.hero-bg{ position:absolute; inset:0; overflow:hidden; background:#000; }
.hero-bg video{
  position:absolute; top:50%; left:50%; width:100%; height:100%; object-fit:cover;
  /* Times Square footage: billboards run up both sides and the center tower, the crowd
     sits in the bottom third or so. Shifted up a little to favor the boards, the real
     fix for the crowd is the overlay fade below, not the crop. */
  /* v175: Eli's screenshot showed the bright Sephora billboard sitting directly behind
     the "S" in "Scale," reading left-heavy against the darker, calmer area under
     "profitably." on the right. Nudged the horizontal crop window right (50%->56%) so
     the visible frame shifts toward that already-dark right-hand area instead of
     centering on Sephora's bright panel; the darkening overlay's spotlight ellipse below
     is still centered on the text column itself, untouched, so this is purely which
     slice of the source footage shows through it, not a change to how dark that slice
     reads. Kept the step modest (a single 6-point nudge) since this video's framing has
     already gone through several rounds (v90, v151) balancing "boards visible" against
     "text legible."
     v176: Eli's next screenshot showed no visible difference, Sephora still sitting
     behind the "S." Two real factors here, not just "the number was wrong": this is a
     playing, looping `<video>`, not a static image, so any two screenshots are two
     different moments in the loop regardless of crop, comparing them directly is noisy.
     And a 6-point object-position shift is genuinely small, on a video only moderately
     oversized relative to its box under `cover`, that can easily be a handful of pixels,
     too subtle to read on a screenshot. Eli confirmed the intent plainly this time: pull
     the video left so the S clears the billboard, there's dark room at the "profitably"
     end for whatever slides into view there instead. Pushed the same value much further
     (56%->72%, a 22-point move from the original 50%) so the shift is unambiguous rather
     than trying a second small step and risking a third "no difference" report. */
  object-position:72% 20%;
  /* pulled back further, zoomed in at all reads busier than it needs to be; playback is
     also slowed via JS (see hero-bg-video-rate script) so it's calmer overall. */
  transform:translate(-50%,-50%) scale(1.0);
}
.hero-bg-overlay{
  position:absolute; inset:0;
  /* two layers: a radial spotlight-darken behind the centered text column (keeps the
     words legible/"popping" against whatever color is playing there), sitting on top of
     a lighter overall vertical fade than before, sides read brighter/more colorful since
     they're outside that spotlight. Bottom fade to solid black pushed later, was ramping
     up by 55% down which read as starting too high in the frame, now holds off until
     ~70% and doesn't hit full black until 92%, letting more of the frame breathe first.
     Eased further this pass: the trust bar sits in roughly that 55-75% band, and the old
     .7-alpha-by-70% stop was crushing it to near-solid black, so logos read as floating
     on a flat dark box instead of on the video. Mid-range lightened (.45 by 60% instead
     of .7 by 70%) so real video detail shows through directly behind the logos, the
     bottom of the frame still eases to full black before the hero-stats divider, just
     later (96% instead of 92%), so the seam glow below still has a true black to fade
     into.
     Third layer added this pass: Eli liked the ambient blue glow bridging into #results
     below the trust bar and wanted a matching one bleeding into the video just above it
     too, so the carousel sits between two soft blue washes instead of only having one
     below. Painted first (topmost of the three), low-opacity brand blue, centered a
     little above where the trust bar sits so it reads as the video itself picking up a
     hint of brand color there, not a separate colored box on top of it.
     v90: Eli sent the raw source clip and it confirmed why the middle of the frame reads
     dead rather than showing the tall stacked billboards, that stack needs real vertical
     room the hero's short/wide crop doesn't have, so cover only ever shows a thin slice of
     it. Rather than chase centering a specific building in a shot that also pans (it'd
     drift out of alignment as it plays), leaned further into darkening the middle on
     purpose and let the wide boards on both sides (Sephora, Pandora, the T-Mobile/Wicked/
     Abu Dhabi wall) carry the color instead. Narrowed the spotlight ellipse (56%->50%
     width) so the darken doesn't bleed into those side boards at all, and pushed it darker
     and snappier (.9->.96 core, .55->.75 at 38%, transparent by 64% instead of 70%) so the
     center reads as a deliberate clean dark backdrop for the text rather than "video that
     happens to be dim there." Same overlay element serves mobile's static poster too, so
     the effect is automatically identical on both without any separate mobile rule. */
  /* v151: Eli's ask was explicit, darken the middle a little more so the side billboards
     pop with color and the H1 gets more focus, exactly the spotlight this ellipse was
     already built for (see v90 note above), just asked to go further. Narrowed the
     ellipse 50%->46% width so it hugs the text column tighter instead of bleeding toward
     the boards, and pushed the core/mid stops darker (.96->.98, .75->.85) with an earlier
     falloff (64%->58%) so the edge between "dark text backdrop" and "full-color board" is
     snappier, reads as a deliberate spotlight rather than a soft fade. */
  /* v154 widened this to 74%/46% at 26% mix, felt like it competed with the stats.
     v155 dialed it back to 62%/38% at 14%, past even the original. v156: Eli confirmed
     he actually liked v154's version, said it reads like real NYC-at-night color cast
     through a lens rather than a decorative add-on, just wanted it eased slightly, not
     removed. Landed between the two: 71%/44% at 21%, closer to v154's presence than
     v155's near-nothing version. */
  background:
    radial-gradient(ellipse 71% 44% at 50% 56%, color-mix(in oklab, var(--lime) 21%, transparent) 0%, transparent 78%),
    radial-gradient(ellipse 46% 92% at 50% 36%, rgba(0,0,0,.98) 0%, rgba(0,0,0,.85) 38%, transparent 58%),
    linear-gradient(180deg, rgba(0,0,0,.4) 0%, rgba(0,0,0,.3) 30%, rgba(0,0,0,.45) 60%, rgba(0,0,0,.75) 82%, #000 96%, #000 100%);
}
/* v159: Eli liked the v158 fix for the hard cutoff, but flagged that the street-level
   crowd and cars (the actual motion/energy in the footage, which he specifically wants
   visible on mobile) were reading too dark/washed out to see. Two compounding causes on
   mobile specifically: this shared overlay's tall spotlight ellipse (92% height, centered
   at 36%) reaches well down into where the crowd sits once the video's rendered that far
   down the page, and v158's own mask-image fade (70%->100% of the video's OWN box) was
   fading out almost exactly the crowd's own bottom-third region, the two effects stacked
   into "can barely see them." Mobile-only override: shrunk the spotlight ellipse's height
   (92%->52%) and raised its center (36%->28%) so it concentrates on the eyebrow/H1/
   subtext above the crowd instead of bleeding into it, and lightened the linear-gradient
   through the 25-60% band specifically (where the street level sits) while keeping the
   top dark enough for text contrast and the bottom still ramping to full black before the
   video's own fade-out, so the seam fix from v158 holds. */
/* v162: Eli's screenshot showed the subtext still hard to read against the video on
   mobile. The spotlight ellipse below (46% wide) was sized for the H1, which is short
   ("Scale profitably."), but the subtext wraps across 3 lines that run almost the full
   narrow mobile viewport, so its outer words on each wrapped line were falling outside
   the ellipse's dark core and sitting against whatever brightness the video happened to
   have there. Rather than zoom the video (would crop the side billboards tighter and
   doesn't fix a text-contrast problem) or go back to a harder text-shadow (that's what
   v157 fixed, was reading as a tacky outline), widened this ellipse (46%->88%) to
   comfortably cover the subtext's full wrapped width, keeping the height the same so it
   doesn't reach further down into crowd territory than before. */
@media(max-width:699px){
  .hero-bg-overlay{
    background:
      radial-gradient(ellipse 71% 44% at 50% 56%, color-mix(in oklab, var(--lime) 21%, transparent) 0%, transparent 78%),
      radial-gradient(ellipse 88% 52% at 50% 28%, rgba(0,0,0,.97) 0%, rgba(0,0,0,.8) 40%, transparent 68%),
      linear-gradient(180deg, rgba(0,0,0,.42) 0%, rgba(0,0,0,.26) 22%, rgba(0,0,0,.14) 42%, rgba(0,0,0,.18) 58%, rgba(0,0,0,.6) 78%, #000 94%, #000 100%);
  }
}
/* motion-sensitive: no video at all, just the poster frame under the same dark overlay. */
@media(prefers-reduced-motion:reduce){
  .hero-bg video{ display:none; }
  .hero-bg{ background:#000 url('images/hero-bg-poster.webp') center/cover no-repeat; }
}
/* v98: Eli confirmed the real hero video is fine on mobile data (measured 5.0MB, not the
   ~11MB this comment used to guess, see the cost breakdown given in chat), asked for it
   enabled there too, using the same "billboards on both sides" width-priority framing
   already worked out for the mobile poster below in v95. `object-fit:cover` can't do that
   framing, same 16:9-into-portrait geometry problem the poster hit: cover fills the tall
   mobile box by HEIGHT, which blows the video up to ~3x the phone's width, so only the
   center ~30% shows and both side billboards scale almost entirely out of frame. Instead of
   relying on object-fit, size the video directly: `width:155%; height:auto;` keeps its own
   16:9 ratio and prioritizes width the same way `background-size:155% auto` did for the
   photo, both billboards read again instead of just the center tower. `top:7%` approximates
   the same "center 15%" upward bias the photo used (favor the boards over the street-level
   crowd), translated from a background-position percentage to a directly-sized element.
   The video's own `poster="images/hero-bg-poster.webp"` attribute in index.html already
   covers the brief pre-load moment, so the v95 background-image fallback that used to sit
   here has been removed, it's no longer needed once the video itself is visible on mobile.
   `.hero-bg`'s plain #000 background still shows through in the letterboxed band above/
   below the video, same visual trade-off the photo made. */
/* v158: Eli's real-phone screenshot showed a hard line right where the video ends and
   flat black begins, right below the CTA button, called it "a cut off from the video."
   Root cause: v152 made the hero a full min-height:100svh column, but this video sizing
   was tuned back in v98 for a much shorter mobile hero. The source file is 1920x1080
   (16:9); at width:155% that renders at ~87% of the viewport's WIDTH tall, which on a
   typical tall phone (height roughly 2.2x width) only covers ~40% of the new full-height
   container, everything below is `.hero-bg`'s own flat #000 with no video texture at
   all, and the shared overlay gradient (built for a video that fills the whole height,
   like desktop's does) is nowhere near full black yet at that 40% mark, so the video's
   last visible row of pixels sits right next to solid black with nothing bridging them.
   Two fixes: bumped width 155%->190% so the video itself reaches further down before it
   ends (less pure-black dead zone below it), and added a mask-image fade on the video
   element's own box, transparent over its last 30%, so wherever it ends on any given
   phone's aspect ratio, it fades to nothing on its own rather than hard-cutting, blending
   into the flat black behind it instead of butting up against it. Deliberately avoided
   calc() in the mask value and any animation on this element, the combination that broke
   masking on mobile WebKit elsewhere in this file (see the trust-bar mask note above). */
@media(max-width:699px){
  .hero-bg video{
    display:block;
    width:190%; height:auto;
    left:50%; top:7%;
    transform:translate(-50%,0);
    /* v159: 70% was fading out almost exactly the crowd/cars in the video's own bottom
       third, the motion Eli specifically wants visible on mobile. Narrowed the fade zone
       to the video's last 12% (88%->100%) so nearly all of that footage stays visible,
       the overlay above now carries more of the "ease into black" job through its own
       darkening instead of transparency doing it, so the seam v158 fixed should still
       hold without erasing the content on top of it. */
    mask-image:linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
    -webkit-mask-image:linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
  }
}
/* v88: mobile has no video, so it also had no motion at all, the "make it feel alive"
   ask from before landed as a shimmer bar on the trust bar instead, which Eli didn't like
   and asked to be removed. Moved the idea to where it makes more sense, the hero photo
   itself: a one-time reveal instead of a loop, starts solid black and fades up into the
   real poster/overlay treatment over ~2.8s on load. Cheap (opacity only, no new asset,
   no video), reads as the photo "developing" into view rather than a static image that's
   just always been sitting there. Layered on .hero-bg-overlay (already sits on top of the
   poster) rather than the poster itself, so the permanent overlay gradients underneath are
   untouched once the reveal finishes. */
@media(max-width:699px){
  .hero-bg-overlay::after{
    content:"";
    position:absolute; inset:0; z-index:1;
    background:#000;
    animation:hero-photo-reveal 2.8s ease-out forwards;
    pointer-events:none;
  }
}
@keyframes hero-photo-reveal{
  from{ opacity:1; }
  to{ opacity:0; }
}
@media(max-width:699px) and (prefers-reduced-motion:reduce){
  .hero-bg-overlay::after{ animation:none; opacity:0; }
}
/* centered hero copy, matching the Pilothouse reference, and it happens to solve the
   earlier "dead space beside the text" complaint too, there's no more left column vs.
   empty right side once the video fills the whole frame and text sits in the middle.
   Text-shadow on all four (eyebrow/h1/sub/note) is a real outline, not just a soft glow,
   the four offset shadows at a small blur read as a hard edge around each letter so the
   words stay legible no matter how bright or busy the video gets behind them, a big soft
   shadow underneath adds depth on top of that. */
.hero-eyebrow{
  font-size:12px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:var(--muted);
  margin-bottom:18px; text-align:center;
  text-shadow:-1px -1px 2px rgba(0,0,0,.8), 1px -1px 2px rgba(0,0,0,.8), -1px 1px 2px rgba(0,0,0,.8), 1px 1px 2px rgba(0,0,0,.8);
}
.hero h1{
  /* v151: 3.6rem max was sized for a 2-3 line headline ("Turn paid social into a
     profitable new-customer acquisition engine."). Now that it's one short line ("Scale
     profitably."), that size leaves it looking small/lightweight against all the room
     around it, premium single-line heroes (Ridge, CTC, Haus) go noticeably bigger with
     less text, not the same size. Bumped to a 4.75rem max, 8.5vw so it scales up faster
     on tablet too, not just capped later. */
  font-size:clamp(2.4rem,9vw,5rem); letter-spacing:-.03em; max-width:820px; margin:0 auto; text-align:center;
  text-shadow:-1.5px -1.5px 2px rgba(0,0,0,.8), 1.5px -1.5px 2px rgba(0,0,0,.8), -1.5px 1.5px 2px rgba(0,0,0,.8), 1.5px 1.5px 2px rgba(0,0,0,.8), 0 8px 28px rgba(0,0,0,.55);
}
/* tried a lime highlight on the mechanism phrase (like scale1128.com's accent-word
   treatment), called it too much against the site's own black/lime balance, reverted to
   plain white. ".hl" span stays in the markup only because it also carries the
   non-breaking hyphen fix below, not for color. */
.hero h1 .hl{ color:inherit; }
.hero-sub{
  /* v149: Eli asked for more air between the (now much shorter, one-line) headline and
     the subtext, and for the hero stats further down the page. Both were the same root
     cause: the vertical rhythm here was tuned for a 2-3 line headline, so once "Scale
     profitably." became one short line, everything below it reads as bunched up close
     together. Bumped this gap 20px->36px. */
  margin:36px auto 0; font-size:clamp(1.02rem,2.6vw,1.2rem); color:var(--muted); max-width:600px; line-height:1.6; text-align:center;
  text-shadow:-1px -1px 2px rgba(0,0,0,.8), 1px -1px 2px rgba(0,0,0,.8), -1px 1px 2px rgba(0,0,0,.8), 1px 1px 2px rgba(0,0,0,.8);
}
.hero-ctas{ margin-top:30px; display:flex; flex-wrap:wrap; gap:14px; align-items:center; justify-content:center; }
.hero-note{
  font-size:13px; color:var(--muted-2);
  text-shadow:-1px -1px 2px rgba(0,0,0,.8), 1px -1px 2px rgba(0,0,0,.8), -1px 1px 2px rgba(0,0,0,.8), 1px 1px 2px rgba(0,0,0,.8);
}
/* v157: Eli's real-phone screenshot showed the eyebrow/subtext reading with a visible
   fringe/halo around the letters, a "sticker outline" look, on the subtext especially
   (against the lighter parts of the billboard footage). The 4-direction hard-edged
   shadow (fixed 1-1.5px offset, 2px blur) was tuned to read as a clean outline at
   desktop's much larger font sizes. At mobile's smaller sizes the same fixed-px offset
   is proportionally much bigger relative to the thinner letter strokes, so it stops
   reading as "legible against a busy background" and starts reading as an outline
   effect on its own. Swapped to a single soft shadow (no offset, wider blur) below
   699px, same legibility job, without the hard multi-directional edge that was causing
   the tacky look. */
@media(max-width:699px){
  .hero-eyebrow, .hero h1, .hero-sub, .hero-note{
    text-shadow:0 2px 10px rgba(0,0,0,.9), 0 0 4px rgba(0,0,0,.7);
  }
}
/* the old embedded explainer video (facade + sale-toast/revenue-card overlay) is pulled
   off the homepage hero per the repositioning notes, a premium background video replaces
   it here. Rules below are left in place, unused for now, since the same video is moving
   to a different page rather than being deleted outright. */
.hero-video{
  position:relative; border-radius:var(--radius); overflow:hidden; border:1px solid var(--border);
  aspect-ratio:16/9; background:var(--card);
}
.hero-video iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; border-radius:var(--radius); }
.video-facade{
  position:absolute; inset:0; width:100%; height:100%; padding:0; border:0; border-radius:var(--radius);
  overflow:hidden; cursor:pointer; background:var(--card);
}
.video-facade img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.video-play-btn{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:76px; height:76px;
  border-radius:50%; background:var(--lime); display:flex; align-items:center; justify-content:center;
  box-shadow:0 12px 34px rgba(0,0,0,.5); transition:transform .2s var(--ease-out);
}
.video-play-btn svg{ width:30px; height:30px; color:var(--lime-ink); margin-left:4px; }
.video-facade:hover .video-play-btn, .video-facade:active .video-play-btn{ transform:translate(-50%,-50%) scale(1.06); }
/* the divider above the stats used to be a flat, full-bleed 1px border, which read as a
   hard wall right where the hero-bg video/overlay has already faded to solid black, then
   nothing until the #results glow further down: a dead, un-lit gap Eli flagged from a
   screenshot. Fading hairline instead of a straight line, plus the new #results::after
   glow below, softens that seam instead of hard-cutting from photo to flat black. */
.hero-stats{
  /* v149 pushed this to 60px (was 34px) to fill leftover height from the shorter
     headline. v150: Eli compared against an older, longer-copy hero and said the current
     one doesn't flow as well, logo placement and overall rhythm read better in the old
     version. 60px read as its own isolated gap rather than a continuous flow; dialing
     back to 44px plus the bigger trust-bar logos above (more visual weight right where
     the gap was thinnest) closes that "empty stretch" feeling without going back to 34px
     and losing the breathing room the shorter headline needs. */
  margin-top:44px; display:grid; grid-template-columns:repeat(3,1fr); gap:16px 14px; padding-top:26px;
  background-image:linear-gradient(to right, transparent, var(--border) 18%, var(--border) 82%, transparent);
  background-position:top; background-size:100% 1px; background-repeat:no-repeat;
}
/* v162: Eli spotted a visible line right above the stats on mobile after the v160/v161
   darkening got added right above it. This divider (a flat #1f1f1f, opaque, no alpha)
   was always here and always subtle against a brighter backdrop, now that everything
   above it fades to near-black, a solid dark-grey line reads as a distinct lighter line
   against near-solid black instead of receding. Faded it to translucent white so it
   still marked the seam without standing out.
   v163: still visible enough to flag again, showing up as two close-together lines once
   the .logo-strip vignette's own edge sat right near it too (fixed separately above).
   Removed the divider outright on mobile instead of tuning it a third time: by the time
   the page reaches the stats, both the trust-bar vignette and the base overlay have
   already faded everything to solid black, an explicit line isn't marking a seam between
   two different things anymore, it's just a line sitting on top of one continuous black
   field, serving no purpose. */
@media(max-width:699px){
  .hero-stats{ background-image:none; }
}
/* at 640px+ the stats used to each get a full-width column of the whole content width,
   which left short text stranded in a lot of dead space (looked sparse/unintentional on
   desktop). Switch to content-sized columns with a fixed, deliberate gap instead, so they
   sit together as one tight, centered stat block regardless of how wide the screen gets. */
@media(min-width:640px){
  .hero-stats{ grid-template-columns:repeat(3,max-content); gap:16px 52px; justify-content:center; }
}
/* .hero-stat itself never had its own text-align, so it just inherited the browser
   default (left) inside its max-content grid column, number and label lined up on the
   left edge of the cell instead of centering as a unit, most visible once the label text
   is a different width than the big number above it. */
.hero-stat{ text-align:center; }
/* v201: neon glow on the stat numbers, per Eli's reference screenshot (cyan bloom around
   big stat numbers on a dark bg). Kept the site's own brand blue rather than switching to
   the reference's cyan, layered text-shadow blurs (same color-mix pattern already used for
   --premium/--premium-border) instead of a literal second color, so it reads as "our blue,
   glowing" rather than a foreign accent color showing up once on the page. */
.hero-stat .num{
  font-size:clamp(1.15rem,5.4vw,2.2rem); font-weight:800; color:var(--lime); letter-spacing:-.02em;
  text-shadow:
    0 0 14px color-mix(in oklab, var(--lime) 75%, transparent),
    0 0 34px color-mix(in oklab, var(--lime) 50%, transparent),
    0 0 64px color-mix(in oklab, var(--lime) 30%, transparent);
}
@media(min-width:640px){ .hero-stat .num{ font-size:clamp(2rem,4vw,3.2rem); } }
.hero-stat .label{ font-size:13px; color:var(--muted); margin-top:4px; line-height:1.4; }
@media(min-width:640px){ .hero-stat .label{ font-size:13.5px; } }

/* results: a soft lime glow (this section's own accent, unlike the hero's blue) so
   the page isn't just flat black repeated, without going as bold as the mechanism
   section's full cream break. */
#results{ position:relative; isolation:isolate; overflow:hidden; }
#results::before{
  content:""; position:absolute; inset:auto -10% -15% -10%; height:60%; z-index:-1;
  background:radial-gradient(ellipse 70% 60% at 50% 100%, color-mix(in oklab, var(--lime) 12%, transparent), transparent 70%);
  pointer-events:none;
}
/* bridges the seam at the top of this section, right where the hero's own video/overlay
   has already gone to solid black and the hairline divider sits: without this, that whole
   band was completely unlit, hero fades to black, hits a bare line, stays black until the
   glow above kicks in well below the fold. This one is anchored to the top of #results
   instead and bleeds upward past the section boundary, so the fade-to-black and the next
   section's content connect through a soft glow rather than a flat, dead gap. */
#results::after{
  content:""; position:absolute; inset:-20% -10% auto -10%; height:42%; z-index:-1;
  background:radial-gradient(ellipse 60% 100% at 50% 0%, color-mix(in oklab, var(--lime) 11%, transparent), transparent 72%);
  pointer-events:none;
}
/* v181: paired with the creative-teaser-showcase padding tightened above, mobile-only
   boost so the shortened (but not eliminated) gap between the creative-teaser CTA and
   this section reads as a lit seam instead of flat black. Reaches further up (-20%->-46%)
   and a little stronger (11%->15% mix) than the desktop version, mobile's narrower
   viewport means this ellipse's own width covers less real screen area at the same
   percentage, needed more reach and presence to actually read here. */
@media(max-width:639px){
  #results::after{
    inset:-46% -10% auto -10%;
    background:radial-gradient(ellipse 70% 100% at 50% 0%, color-mix(in oklab, var(--lime) 15%, transparent), transparent 74%);
  }
}

/* hero proof widgets, echoing the sale-notification + revenue-chart demo from Chuck's own video */
.proof-toast-stack{ position:relative; z-index:5; display:flex; flex-direction:column; gap:8px; margin-top:14px; pointer-events:none; }
.proof-toast{
  display:flex; align-items:center; gap:10px; background:#fff; color:#17181a; border-radius:10px;
  padding:10px 14px; font-size:.78rem; font-weight:600; box-shadow:0 10px 30px rgba(0,0,0,.4);
  width:max-content; max-width:230px; animation:toast-cycle 7s ease-in-out infinite;
}
.proof-toast:nth-child(2){ animation-delay:2.3s; }
.proof-toast:nth-child(3){ animation-delay:4.6s; }
/* white on this green was 2.13:1, a real WAVE-caught fail, not a false positive. Swapped
   to the site's own dark ink color instead of inventing a new one, 8.8:1, and it still
   reads as "the Shopify green badge" since the background hue is what carries that. */
.pt-icon{ width:20px; height:20px; border-radius:5px; background:#95bf47; color:var(--lime-ink); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:800; flex-shrink:0; }
/* transform/scale only, never opacity: keeps the toast text fully opaque (full
   contrast) at every point in the loop instead of fading through invisible. */
@keyframes toast-cycle{
  0%,100%{ transform:translateY(6px) scale(.92); }
  15%,30%{ transform:translateY(0) scale(1); }
  45%{ transform:translateY(6px) scale(.92); }
}
@media(prefers-reduced-motion:reduce){ .proof-toast{ animation:none; } }
.proof-revenue-card{
  position:relative; z-index:5; margin-top:12px; background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:14px 16px; max-width:220px; box-shadow:0 14px 40px rgba(0,0,0,.4);
}
/* fade the toasts/revenue card out once the video actually starts playing, so they don't
   sit on top of the player. Only ever triggered by a real click, never on page load, so
   there's no risk of an automated scan catching them mid-fade. */
.proof-toast-stack, .proof-revenue-card{ transition:opacity .35s var(--ease-out), transform .35s var(--ease-out); }
.proof-toast-stack.proof-hide, .proof-revenue-card.proof-hide{ opacity:0; transform:translateY(8px); }
@media(prefers-reduced-motion:reduce){ .proof-toast-stack, .proof-revenue-card{ transition:none; } }
.prc-label{ font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted-2); font-weight:700; }
.prc-num{ font-size:1.3rem; font-weight:800; color:var(--lime); margin-top:4px; }
.prc-spark{ width:100%; height:28px; margin-top:8px; display:block; }
.prc-spark polyline{ fill:none; stroke:var(--lime); stroke-width:2.5; stroke-linecap:round; stroke-linejoin:round; }
@media(min-width:900px){
  .proof-toast-stack{ position:absolute; left:-16px; bottom:18px; margin-top:0; }
  .proof-revenue-card{ position:absolute; right:-16px; bottom:-24px; margin-top:0; }
}
@media(max-width:899px){
  .proof-toast-stack, .proof-revenue-card{ display:none; }
}

/* what we do, Chuck's real 4-part framework: strategy, creative, media, optimization.
   Mobile gets its own horizontal peek-slider (matches the case study/review carousel
   pattern) instead of 4 full-height stacked cards, so you're not scrolling forever to
   clear this section. Tablet and up keep the grid. */
.wwd-grid{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:40px; }
@media(min-width:560px){ .wwd-grid{ grid-template-columns:repeat(2,1fr); } }
@media(min-width:900px){ .wwd-grid{ grid-template-columns:repeat(4,1fr); } }
@media(max-width:559px){
  #approach .wwd-grid{
    display:flex; flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; overscroll-behavior-x:contain;
    scroll-snap-type:x mandatory; gap:14px; margin-top:32px; padding:4px 2px 8px;
    -webkit-overflow-scrolling:touch; scrollbar-width:none;
  }
  #approach .wwd-grid::-webkit-scrollbar{ display:none; }
  #approach .wwd-card{ flex:0 0 80%; min-width:240px; scroll-snap-align:center; }
}
.wwd-swipe-hint{ display:none; }
@media(max-width:559px){ .wwd-swipe-hint{ display:block; } }

/* results stats: the 3-card grid is the tablet/desktop version. On mobile it collapses
   into a single-line ticker that auto-rotates through the same 3 stats, so you land on
   the case study photos right away instead of scrolling through 3 full-width cards first.
   Rotation is transform-only (never opacity), so whichever stat is showing is always at
   full contrast, no risk of a scan catching one mid-fade. */
@media(max-width:639px){ .results-stats{ display:none; } }
/* only 3 stats, but the shared .wwd-grid rule sizes for 4 equal columns at 900px+, so this
   was stretching across 3/4 of the row width, hard left, with a dead gap on the right
   instead of reading as one deliberate centered group. Cap it at 3 columns and center the
   whole block instead of just letting it fill whatever width the grid handed it. */
@media(min-width:900px){
  .results-stats{ grid-template-columns:repeat(3, minmax(220px, 300px)); justify-content:center; max-width:1040px; margin-left:auto; margin-right:auto; }
}
.stat-ticker{ display:none; }
@media(max-width:639px){
  .stat-ticker{
    display:block; position:relative; margin-top:28px; margin-bottom:8px; height:84px; overflow:hidden;
    background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  }
}
.stat-ticker-track{ display:flex; flex-direction:column; transition:transform .6s var(--ease-smooth); }
.stat-ticker-item{
  height:84px; flex:0 0 84px; display:flex; flex-direction:column; align-items:center; justify-content:center;
  text-align:center; padding:0 20px;
}
.stat-ticker-item strong{ font-size:1.25rem; font-weight:800; color:var(--lime); letter-spacing:-.01em; line-height:1.15; }
.stat-ticker-item span{ font-size:12.5px; color:var(--muted); margin-top:3px; line-height:1.35; }
@media(prefers-reduced-motion:reduce){ .stat-ticker-track{ transition:none; } }
.wwd-card{
  position:relative; overflow:hidden; background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius); padding:26px 22px;
  transition:transform .3s var(--ease-out), border-color .3s var(--ease-out), box-shadow .3s var(--ease-out);
}
/* v84: only the homepage's #approach cards carry a .wwd-tip (services.html reuses .wwd-card
   for a plain grid with no tip at all), so the extra bottom padding the tip needs is scoped
   here, not added to the shared .wwd-card rule, that would've left a pointless empty gap at
   the bottom of every services.html card that has nothing to reveal into it. */
#approach .wwd-card{ padding-bottom:90px; }
.wwd-card:hover{
  transform:translateY(-5px);
  border-color:color-mix(in oklab, var(--lime) 45%, var(--border));
  box-shadow:0 18px 40px rgba(0,0,0,.35);
}
/* spotlight hover, v81: v80's position:absolute approach (pulling the hovered card out of
   grid flow to truly center it) turned out broken in practice, Eli's screenshot showed the
   dimming inconsistent and the enlarged card landing on top of card 3 rather than reading
   as centered, plus position isn't a smoothly-animatable property so the state change was
   never going to transition cleanly. Rebuilt using transform only, translateX shifts the
   hovered card toward the row's middle by a fixed amount per column while scale grows it,
   both fully animatable and neither one ever removes anything from grid flow, so there's
   no reflow risk and nothing to pin. It won't land at the literal mathematical center of
   the full row (that point sits on top of a neighbor anyway once you actually look at where
   it lands, per the last attempt), it shifts toward the middle and grows over its nearest
   neighbor, which is what actually reads as "takes the center, in front of the others"
   without the fragility. Bias values only apply at the real 4-column desktop breakpoint,
   scoped together with hover-capability so a narrow resized window can't hit a mismatched
   in-between state. Touch devices keep the plain lift-on-tap, no hover equivalent exists
   for "the other 3 fade because you're not touching them." */
@media (hover:hover) and (pointer:fine){
  .wwd-card{ transition:transform .4s var(--ease-out), border-color .3s var(--ease-out), box-shadow .4s var(--ease-out), opacity .35s var(--ease-out); }
  .wwd-grid:hover .wwd-card{ opacity:.4; }
  .wwd-grid:hover .wwd-card:hover{
    opacity:1; z-index:5; transform:scale(1.15);
    box-shadow:0 32px 70px rgba(0,0,0,.55);
    border-color:color-mix(in oklab, var(--lime) 55%, var(--border));
  }
}
@media(min-width:900px) and (hover:hover) and (pointer:fine){
  .wwd-grid > .wwd-card:nth-child(1):hover{ transform:scale(1.15) translateX(22%); }
  .wwd-grid > .wwd-card:nth-child(2):hover{ transform:scale(1.15) translateX(8%); }
  .wwd-grid > .wwd-card:nth-child(3):hover{ transform:scale(1.15) translateX(-8%); }
  .wwd-grid > .wwd-card:nth-child(4):hover{ transform:scale(1.15) translateX(-22%); }
  /* v130: the results section's 3-stat row (`+63% / 10X / +300%`) reuses `.wwd-grid` for its
     layout, which means it was also inheriting this nth-child bias math, tuned specifically
     for the 4-column What We Do grid above. Applied to a 3-card row instead: card 1 got the
     column-1-of-4 bias (shifts hard right), card 3 got the column-3-of-4 bias (a small shift
     left, correct for a middle-ish column in a 4-wide row but wrong as the actual LAST card
     in a 3-wide row), and card 2, which is already the true center of a 3-card row, still got
     shifted +8% right instead of staying put, exactly Eli's report: "one goes to the right
     other to the left, middle doesn't populate forward to the middle." Rather than invent a
     separate, correct 3-column bias set, overrode all three back to a plain `scale(1.15)`
     with no translateX at all, since a 3-card row growing symmetrically in place doesn't need
     the "shift toward center" trick the 4-icon grid was originally built for, that trick only
     exists because a 4-column layout has no true center column to grow from. */
  .results-stats > .wwd-card:nth-child(1):hover,
  .results-stats > .wwd-card:nth-child(2):hover,
  .results-stats > .wwd-card:nth-child(3):hover{
    transform:scale(1.15);
  }
}
/* v85: the "01/02/03/04" ghost-numeral treatment (previously explained here as replacing
   the old glossy gradient-sphere icons) got called out as redundant, the cards already run
   in a fixed, visible left-to-right/grid order, so a number restating that order added
   nothing. Removed the .wwd-num markup entirely from both index.html's "What We Do" and
   services.html's services grid (same shared pattern, kept consistent in both places same
   as before), titles now lead each card directly. */
/* v89: with the 01-04 numerals gone (see the removal note above), the title was the only
   thing left at the top of the card and read a little flat/small for the lead element.
   Bumped size and weight, and added a short accent underline in the site's one brand
   color rather than coloring the whole word (keeps it legible/high-contrast while still
   giving it the "vibrant" pop Eli asked for, same restrained accent-bar language used
   elsewhere on the site instead of inventing a new treatment). */
.wwd-card h3{
  font-size:1.3rem; font-weight:800; letter-spacing:-.01em; margin-top:0; margin-bottom:14px;
}
.wwd-card h3::after{
  content:""; display:block; width:30px; height:4px; margin-top:10px; border-radius:2px;
  background:var(--lime);
}
.wwd-card p{ margin-top:10px; font-size:.92rem; color:var(--muted); line-height:1.55; }
/* hover-only bonus detail chip: decorative reinforcement of the paragraph above it,
   not new information, so it's marked aria-hidden and never required to read the card.
   Was opacity:0 at rest, which is exactly the pattern flagged elsewhere in this project:
   a real browser scanner reads the declared text color regardless of opacity, so a
   fully-transparent lime-ink-on-lime pairing showed up as a "1:1, fails" contrast error
   4 times over, one per card, even though nothing was ever visibly on screen to fail.
   Fix is the same one used everywhere else on this page: don't hide state with opacity,
   hide it structurally instead. The parent .wwd-card already has overflow:hidden, so
   bottom:-90px alone fully clips this out of view at rest, opacity was redundant. */
.wwd-tip{
  position:absolute; left:22px; right:22px; bottom:-90px; pointer-events:none;
  background:var(--lime); color:var(--lime-ink); font-size:.78rem; font-weight:700; line-height:1.4;
  padding:11px 14px; border-radius:var(--radius-sm); box-shadow:0 12px 26px rgba(0,0,0,.4);
  transition:bottom .4s var(--ease-spring);
}
.wwd-card:hover .wwd-tip{ bottom:22px; }
@media(prefers-reduced-motion:reduce){ .wwd-tip{ transition:none; } }
@media(hover:none){ .wwd-tip{ display:none; } }

/* carousels (shared engine) */
.carousel-wrap{ position:relative; }
.carousel-dots{ display:none; justify-content:center; align-items:center; gap:4px; margin-top:18px; }
.cd-dot{ width:38px; height:38px; padding:0; display:flex; align-items:center; justify-content:center; touch-action:manipulation; }
.cd-dot::before{ content:""; display:block; width:7px; height:7px; border-radius:50%; background:rgba(255,255,255,.22); transition:background .3s var(--ease-out), width .3s var(--ease-out); }
.cd-dot.active::before{ background:var(--lime); width:22px; border-radius:4px; }
.swipe-hint{ display:none; text-align:center; font-size:12px; font-weight:600; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); margin-top:10px; }
.swipe-hint .arr{ display:inline-block; margin-left:6px; animation:nudge 1.6s ease-in-out infinite; }
@keyframes nudge{ 0%,100%{transform:translateX(0);} 50%{transform:translateX(6px);} }
@media(max-width:767px){ .swipe-hint{ display:block; } }

/* compact case strip, one line per card, 3 visible at a time, scrolls at every breakpoint */
.case-strip{
  display:flex; flex-wrap:nowrap; align-items:stretch; overflow-x:auto; overflow-y:hidden; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory; gap:16px; padding:18px 2px 22px; margin-top:40px;
  -webkit-overflow-scrolling:touch; scrollbar-width:none;
}
.case-strip::-webkit-scrollbar{ display:none; }
.carousel-dots{ display:flex; }
/* the next card in line was rendering shorter than the current one until scrolled to,
   "sitting down" and correcting itself once touched. align-items:stretch was already the
   flex default, made it explicit here plus align-self on the card itself, cheap insurance
   against the same class of bug. The likelier real cause: the peeking card's photo had
   loading="lazy", and native lazy-load's viewport check can under-count a horizontally
   adjacent (not vertically below) partially visible element as "not close enough yet,"
   so it doesn't decode until an actual scroll/touch event re-triggers the check, which is
   exactly "fixes itself once you touch it." First two real photos switched to eager below
   so the always-initially-adjacent pair never hits that timing gap. */
.case-chip{
  position:relative; flex:0 0 84%; min-width:260px; scroll-snap-align:center; align-self:stretch;
  background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  overflow:hidden; display:flex; flex-direction:column;
  transition:transform .3s var(--ease-out), border-color .3s var(--ease-out), box-shadow .3s var(--ease-out);
}
/* v128: Eli asked for the case studies to "pop up when i hover over... like how we did with
   the creative and strategy part", pointing at the existing `.wwd-card:hover` treatment
   (lift + lime-tinted border + shadow) used on the homepage's What We Do tabs. Mirrored that
   same base treatment here rather than the more dramatic desktop-only `scale(1.15)` +
   sibling-dimming version `.wwd-grid` also carries: `.case-strip` (this carousel's
   container) has `overflow-y:hidden`, so a big scale-up would risk visibly clipping the
   top/bottom of a lifted card against that overflow, something `.wwd-grid` never has to
   worry about since it isn't a scrolling strip. The lift+border+glow alone is enough to
   read as "this card is interactive," and `z-index:2` on hover keeps the lifted card's
   shadow from getting overlapped by its still-static neighbors. Also bumped
   `.case-strip`'s own top/bottom padding (6px/10px -> 18px/22px) to give the -6px lift and
   its box-shadow actual room to breathe before hitting that overflow:hidden edge. */
.case-chip:hover{
  transform:translateY(-6px);
  border-color:color-mix(in oklab, var(--lime) 45%, var(--border));
  box-shadow:0 18px 40px rgba(0,0,0,.35);
  z-index:2;
}
@media(min-width:640px){ .case-chip{ flex:0 0 calc(50% - 8px); } }
@media(min-width:1024px){ .case-chip{ flex:0 0 calc(33.333% - 11px); } }
/* v225: case-studies.html static grid variant, Chuck's Aug 6 request to carry the named
   homepage case studies (Strung, Kinda Handsome, etc.) over onto the dedicated page too.
   Reuses .case-chip/.cc-* as-is (same card look, hover lift, photo rotation, everything),
   just a different container: the homepage's .case-strip is a horizontal scroll-snap
   carousel because it's fighting for space in one section among many, this page has the
   room to lay all of them out flat at once, wrapping instead of scrolling reads as more
   complete for a page whose whole job is "here are our case studies," not a teaser. */
.case-chip-grid{ display:flex; flex-wrap:wrap; gap:16px; margin-top:28px; }
.case-chip-grid .case-chip{ flex:0 0 100%; }
@media(min-width:640px){ .case-chip-grid .case-chip{ flex:0 0 calc(50% - 8px); } }
@media(min-width:1024px){ .case-chip-grid .case-chip{ flex:0 0 calc(33.333% - 11px); } }
.cc-thumb{ width:100%; aspect-ratio:16/10; background:var(--muted-surface); border-bottom:1px solid var(--border); }
.cc-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
.cc-thumb.placeholder{ display:flex; align-items:center; justify-content:center; }
.cc-thumb.placeholder span{ font-size:12px; color:var(--muted-2); text-transform:uppercase; letter-spacing:.06em; font-weight:700; }
/* v228: icon-by-default, real-photo-on-hover. Eli liked the blue icon tiles built for the 5
   photo-less cards enough to want the same icon shown on the 3 cards that DO have real
   photos (Strung, Kinda Handsome, Premium Homeware), with the real photo only revealing on
   hover, more photos to swap in for the other 5 coming later. The icon sits in its own
   layer ON TOP of the existing photo/rotate layer underneath (z-index:2 vs the photo's
   default stacking), fading out on hover/focus to reveal what's already there rather than
   swapping `src` via JS, keeps this pure CSS like every other reveal on this card.
   `:focus-within` (not just `:hover`) so keyboard users tabbing to the card's link inside
   `.cc-body` also get the reveal, not just mouse users.
   Touch devices get `@media(hover:none)` forcing the icon to stay, not a content-loss bug
   like the gotchas doc warns about, the photo is decorative here, the actual case-study
   info (name/stats/description) is always visible in `.cc-body` regardless of thumb state. */
.cc-hover-swap{ position:relative; }
.cc-hover-swap .cc-icon-overlay{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
  z-index:2; transition:opacity .4s var(--ease-out);
}
.case-chip:hover .cc-hover-swap .cc-icon-overlay,
.case-chip:focus-within .cc-hover-swap .cc-icon-overlay{ opacity:0; }
@media(hover:none){
  .cc-hover-swap .cc-icon-overlay{ opacity:1 !important; }
  /* v229: Eli asked for the real photos to show on mobile too, not just the icon tile.
     Touch has no hover to reveal them the way desktop does, so for the cards that actually
     have real rotating photos underneath (.cc-thumb-rotate: Strung, Kinda Handsome, Premium
     Homeware), fade the icon out permanently on touch instead of forcing it to stay, letting
     the crossfade animation show through on its own, same effect as a permanent "hover".
     The other 5 case-chip cards never use .cc-hover-swap at all (icon-only, no photos shot
     yet), so they're untouched by this and keep showing their icon exactly as before. */
  .cc-thumb-rotate .cc-icon-overlay{ opacity:0 !important; }
}
@media(prefers-reduced-motion:reduce){
  .cc-hover-swap .cc-icon-overlay{ transition:none; }
}
/* v218: 3-photo crossfade for the Strung case-chip thumbnail. Same "stacked absolute layers,
   opacity keyframes" approach as the trust-bar marquee elsewhere in this file, just without
   the horizontal translate. Each image holds for ~4s of a 12s loop, staggered via negative
   animation-delay so all three are already mid-cycle on load (no blank first frame). Kept to
   just this one card on purpose — the case-strip itself is already an auto-scrolling carousel,
   so rotating every card's photo too would stack two independent motions and read as noisy.
   Only applied where there are real, distinct photos worth cycling through (Strung has 3: the
   Rolling Stones bead, the "Anthem" rock-hand charm, and the Def Leppard charm), not fake filler. */
.cc-thumb-rotate{ position:relative; }
.cc-thumb-rotate .cc-rotate-img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
  opacity:0;
  animation:cc-crossfade 12s infinite;
}
.cc-thumb-rotate .cc-rotate-img:nth-child(1){ animation-delay:0s; }
.cc-thumb-rotate .cc-rotate-img:nth-child(2){ animation-delay:-4s; }
.cc-thumb-rotate .cc-rotate-img:nth-child(3){ animation-delay:-8s; }
@keyframes cc-crossfade{
  0%, 27%{ opacity:1; }
  33%, 94%{ opacity:0; }
  100%{ opacity:1; }
}
@media(prefers-reduced-motion:reduce){
  .cc-thumb-rotate .cc-rotate-img{ animation:none; opacity:0; }
  .cc-thumb-rotate .cc-rotate-img:first-child{ opacity:1; }
}
.cc-body{ padding:20px 20px 22px; display:flex; flex-direction:column; flex:1; }
.cc-cat{ font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--lime); }
/* v97: the "next card sits low until you scroll to it, then it settles" report came back
   after an earlier pass already tried align-items:stretch + eager-loading the first two
   photos. Stretch alone assumes every card's UNSTRETCHED natural height already matches
   before the browser reconciles them, but with a real client name or description running
   a few characters longer or shorter, each card's natural pre-stretch height genuinely
   differs (thumb is fixed via aspect-ratio, but the name can wrap 1 or 2 lines and the
   description 2 or 3, both flexible), and mobile Safari/Chrome haven't been reliably
   re-running that stretch recalculation inside a horizontally-scrolling flex row until
   something (a touch/scroll event) forces a relayout, which is exactly "corrects itself
   once you touch it." Rather than lean on the browser to reconcile heights after the fact,
   removed the height difference at the source: name and description are both clamped to a
   fixed number of lines AND given a matching min-height, so every card's natural content
   height is identical from the very first paint, nothing left for any stretch timing bug
   to expose. Checked all 6 real descriptions first, they're all 97-108 characters, so 3
   lines fits every one of them with room to spare, this isn't truncating any real copy. */
.cc-name{
  font-size:1.08rem; font-weight:700; margin-top:8px; line-height:1.3;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden;
  min-height:2.6em;
}
.cc-desc{
  font-size:.92rem; color:var(--muted); margin-top:9px; line-height:1.55;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; overflow:hidden;
  min-height:4.65em;
}
.cc-divider{ border-top:1px solid var(--border); margin:16px 0 14px; }
.cc-stats-row{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.cc-stats-row .num{ font-size:1.35rem; font-weight:800; color:var(--lime); letter-spacing:-.01em; }
.cc-stats-row .lbl{ font-size:.76rem; color:var(--muted-2); text-transform:uppercase; letter-spacing:.04em; margin-top:2px; }

/* v84: Eli wanted the same "break up the black" treatment #results already has bleeding
   into it from the creative-teaser above (an ambient blue wash, this site's one recurring
   accent color, not a new color per section). #approach (the WWD cards) sits between them
   with its own visual interest from the cards themselves, but reviews came right after it
   as another flat black section, no glow of its own, reading as the black stretching on
   again. Same pattern as #results::before, own shape/position so it's this section's glow
   rather than a copy sitting in the exact same spot every time. */
#reviews{ position:relative; isolation:isolate; overflow:hidden; }
#reviews::before{
  content:""; position:absolute; inset:-12% -12% -22% -12%; z-index:-1;
  background:radial-gradient(ellipse 78% 66% at 50% 40%, color-mix(in oklab, var(--lime) 16%, transparent), transparent 76%);
  pointer-events:none;
}

/* reviews slider */
.review-grid{ display:flex; flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; overscroll-behavior-x:contain; scroll-snap-type:x mandatory; gap:16px; padding:6px 2px 10px; margin-top:40px; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
.review-grid::-webkit-scrollbar{ display:none; }
.review-card{ flex:0 0 84%; min-width:250px; scroll-snap-align:center; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:24px; display:flex; flex-direction:column; }
@media(min-width:640px){ .review-card{ flex:0 0 calc(50% - 8px); } }
@media(min-width:1024px){ .review-card{ flex:0 0 calc(33.333% - 11px); } }
.stars{ color:var(--lime); font-size:.9rem; letter-spacing:2px; }
.review-quote{ font-size:1rem; color:var(--fg); margin-top:14px; line-height:1.6; flex:1; }
.review-meta{ margin-top:16px; padding-top:14px; border-top:1px solid var(--border); font-size:.8rem; color:var(--muted); }
.review-meta .name{ color:var(--fg); font-weight:600; }
/* small real Google "G" mark next to "Verified Google Review", since these are actually
   pulled from Google reviews, per Eli's ChatGPT-review pass on this section: a recognized
   brand icon reads as an instant trust cue without anyone having to read the words. Full
   4-color mark (not recolored to the site palette), same reasoning as the footer's other
   trademarked cert badges, a partner/verification mark should look like the real thing. */
.google-review-tag{ display:inline-flex; align-items:center; gap:5px; }
.google-icon{ width:13px; height:13px; flex-shrink:0; }

/* mechanism (.mechanism/.mech-body/.mech-highlight) and the "RCKSTR vs. everyone else"
   comparison table (.compare-wrap/table.compare/.col-us/.col-us-badge/.ic-yes/.ic-no) were
   both removed from the homepage this pass: Eli's read was that a premium-positioned site
   like Pilothouse doesn't need to argue "us vs. them" or explain the problem before getting
   to the work, and the cream-background mechanism section in particular read as a pointless
   break in an otherwise dark, confident page. Rules deleted along with the markup rather than
   left dead in the sheet, neither class is referenced anywhere else in the site. */

.timeline-list{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:36px; }
@media(min-width:860px){ .timeline-list{ grid-template-columns:repeat(3,1fr); } }
.tl-card{ background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:24px 22px; }
.tl-num{ font-size:12px; font-weight:800; color:var(--lime); letter-spacing:.05em; }
.tl-card h3{ font-size:1.05rem; margin-top:10px; }
.tl-card p{ color:var(--muted); font-size:.93rem; margin-top:10px; line-height:1.55; }

/* book: was a real lead-gen intake form (name/email/site/spend/message, posting to a
   Formspree placeholder that was never actually wired up to a real form ID). Eli saw the
   cc94.com reference (a real calendar embed sitting directly next to the FAQ list, no form
   at all) and wants that instead: a reserved spot for Chuck's real Cal.com/Calendly embed,
   paired with the FAQ right next to it rather than as its own separate section further down
   the page. The FAQ's own id="faq" moved onto .book-faq so every existing "#faq" nav link
   sitewide still lands in the right place, it's just inside #book visually now. */
.book-split{ margin-top:44px; display:grid; grid-template-columns:1fr; gap:24px; }
/* v136: was 1fr 1.1fr (FAQ column WIDER than the calendar), backwards for what's actually
   in each column now that a real Calendly embed replaced the placeholder. The FAQ list is
   just text, it reads fine narrow; Calendly's own multi-column day/time picker actively
   needs the room and was rendering visibly squeezed (calendar grid barely fitting, "Select
   a Day" cramped, description text truncating behind "Show more") at the old ratio. Flipped
   to give the calendar meaningfully more width, and only kicks in at a wider breakpoint
   (1000px, up from 860px) so the two-column split doesn't start squeezing the calendar
   again right at the point it first appears. */
@media(min-width:1000px){ .book-split{ grid-template-columns:1.5fr 1fr; align-items:start; } }
/* v142: Eli's screenshot of the real (non-automated-browser) render showed the calendar
   working, but the whole card read as flat/inert against the page, near-black background
   sitting on near-black background, nothing to draw the eye or signal "interact here."
   Lifted it the same way `--premium` already lifts other sections on this site (a faint
   blue tint mixed into the card color, not a new pattern): brighter border, a soft ambient
   glow so the card visually sits forward off the page instead of blending into it. */
.book-calendar{
  border:1px solid color-mix(in oklab, var(--lime) 35%, var(--border));
  border-radius:var(--radius);
  background:color-mix(in oklab, var(--lime) 8%, var(--card));
  padding:28px 20px;
  box-shadow:0 0 44px 2px color-mix(in oklab, var(--lime) 20%, transparent);
  position:relative; overflow:hidden;
}
.book-calendar h3{ font-size:1.35rem; padding-left:8px; }
.book-calendar-meta{ display:flex; align-items:center; gap:8px; margin-top:14px; font-size:.9rem; color:var(--muted); padding-left:8px; }
.book-calendar-meta .dot{ width:5px; height:5px; border-radius:50%; background:var(--lime); flex-shrink:0; }

/* v220: premium booking card (replaces the old inline calendar). Left-aligned, not the
   sitewide centered .eyebrow, this card sits in a two-column split next to left-aligned FAQ
   copy, a centered label would look oddly isolated. Ambient radial glow behind the content
   (same "off-center glow, no new brand color" language as the next-level-html grain/gradient
   pattern used elsewhere) so the card reads as designed, not as a plain bordered box. */
.book-calendar-cta::before{
  content:''; position:absolute; inset:auto -20% -30% -20%; height:70%; z-index:0;
  background:radial-gradient(ellipse 60% 60% at 50% 100%, color-mix(in oklab, var(--lime) 22%, transparent), transparent 70%);
  pointer-events:none;
}
.book-calendar-cta > *{ position:relative; z-index:1; }
.book-calendar-eyebrow{
  font-size:12px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:var(--lime); padding-left:8px; margin-bottom:6px;
}
.book-calendar-cta h3{ padding-left:8px; }
.book-calendar-desc{ margin-top:10px; padding-left:8px; color:var(--muted); font-size:.98rem; line-height:1.6; max-width:38ch; }
.book-calendar-checklist{ list-style:none; margin-top:20px; padding-left:8px; display:flex; flex-direction:column; gap:10px; }
.book-calendar-checklist li{ display:flex; align-items:center; gap:10px; font-size:.92rem; color:var(--fg); font-weight:600; }
.book-calendar-checklist svg{ width:18px; height:18px; flex-shrink:0; color:var(--lime); }
.book-calendar-open{ margin-left:8px; width:calc(100% - 8px); justify-content:center; }
@media(min-width:480px){ .book-calendar-open{ width:auto; } }
/* v136: the Calendly inline widget itself. Was a bare inline style on the div
   (min-width:280px, height:660px) which was both too narrow (Calendly's own picker wants
   real room) and too short (660px cut off the event description and the day grid both
   needed scrolling to see fully). width:100% so it always fills whatever this now-wider
   column gives it rather than a fixed px value, and height bumped way up so the whole
   widget, description text and all, renders without an inner scrollbar.
   v216: that 900/820px height was tuned for the OLD layout, description panel included.
   v193 later added `hide_event_type_details=1` (dropped that whole left panel, calendar
   only), but this height was never re-tuned to match, so the box kept reserving room for
   content that's no longer there. Eli flagged the resulting dead space directly. Measured
   the real Calendly iframe live (headless browser, both 390px and 2000px viewports): the
   no-selection "pick a day" state only ever uses ~500-530px before the time zone row, on
   both breakpoints, the old two-tier 900/820px split wasn't reflecting an actual mobile-
   vs-desktop content difference, just stale numbers. Calendly's inline widget scrolls
   internally if content (e.g. a long list of open time slots) exceeds the box's own
   height, so shrinking this is safe, worst case is an inner scrollbar on a busy day
   rather than any clipped/broken content. Settled on 620px: comfortably covers the day
   picker with real but not excessive breathing room, and still shows 5-6 time slots
   before that inner scroll kicks in once a day's selected. One value now, no breakpoint
   split, since the real content height doesn't meaningfully differ by viewport width. */
.calendly-embed{ width:100%; min-width:280px; height:620px; }

/* v220: booking modal. Same real Calendly embed as before, just mounted here (see the JS
   near the end of index.html) instead of inline on the page. panel max-height:90vh +
   overflow-y:auto is the safety net for short viewports where 620px of calendar plus the
   close button/padding wouldn't otherwise fit, Calendly's widget already scrolls
   internally too so this is a rare double-scroll edge case, not the common path. */
.book-modal{ position:fixed; inset:0; z-index:200; display:none; }
.book-modal.open{ display:block; }
.book-modal-backdrop{
  position:absolute; inset:0; background:rgba(0,0,0,.72);
  opacity:0; transition:opacity .3s var(--ease-smooth);
}
.book-modal.open .book-modal-backdrop{ opacity:1; }
.book-modal-panel{
  position:relative; margin:5vh auto; width:calc(100% - 32px); max-width:640px; max-height:90vh;
  overflow-y:auto; background:var(--card); border:1px solid color-mix(in oklab, var(--lime) 35%, var(--border));
  border-radius:var(--radius); box-shadow:0 24px 70px rgba(0,0,0,.55);
  padding:20px; opacity:0; transform:translateY(16px);
  transition:opacity .3s var(--ease-smooth), transform .35s var(--ease-smooth);
}
.book-modal.open .book-modal-panel{ opacity:1; transform:translateY(0); }
.book-modal-close{
  position:absolute; top:14px; right:14px; z-index:1; width:34px; height:34px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; background:var(--muted-surface);
  border:1px solid var(--border); color:var(--fg); cursor:pointer; transition:border-color .2s var(--ease-out), background .2s var(--ease-out);
}
.book-modal-close:hover{ border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); background:color-mix(in oklab, var(--lime) 10%, var(--muted-surface)); }
.book-modal-close svg{ width:16px; height:16px; }
.book-modal-loading{
  height:620px; display:flex; align-items:center; justify-content:center;
  color:var(--muted-2); font-size:.9rem;
}
@media(prefers-reduced-motion:reduce){
  .book-modal-backdrop, .book-modal-panel{ transition:none; }
}

.book-calendar .btn{ margin-top:20px; }
/* v220: was text-align:center for the old symmetric calendar-widget card. The new booking
   card is left-aligned throughout (see .book-calendar-eyebrow etc. above), a centered note
   under left-aligned content read as misaligned rather than intentional. */
.book-calendar-note{ display:block; font-size:.78rem; color:var(--muted-2); margin-top:12px; margin-left:8px; text-align:left; }
.book-faq h3{ font-size:1.35rem; }

/* faq */
.faq-list{ margin-top:36px; max-width:760px; border-top:1px solid var(--border); }
.faq-item{ border-bottom:1px solid var(--border); }
.faq-item summary{ list-style:none; display:flex; align-items:center; justify-content:space-between; gap:16px; padding:19px 0; cursor:pointer; font-size:1rem; font-weight:600; }
.faq-item summary::-webkit-details-marker{ display:none; }
.faq-item summary::after{ content:"+"; font-size:1.4rem; font-weight:400; color:var(--lime); flex-shrink:0; transition:transform .25s var(--ease-out); }
.faq-item[open] summary::after{ transform:rotate(45deg); }
.faq-item p{ padding-bottom:19px; color:var(--muted); font-size:1rem; line-height:1.6; max-width:640px; }

/* footer: was 3 stacked blocks, each carrying its own 40px margin + border-top, footer-top,
   then cert-row, then the legal line, three dividers' worth of vertical space for what's
   really one closing block. Down to one border-top for the whole bottom half now. */
footer{ border-top:1px solid var(--border); padding:44px 0 30px; }
.footer-top{ display:grid; grid-template-columns:1fr; gap:28px; }
/* v99: was 3 columns (brand, Site, Contact). Eli sent the Pilothouse footer as a reference
   ("lets also make our footer this format, as nice as this") and pointed out the icon-led
   contact rows and the separate "Follow us" list of icon+label social links, distinct from
   the plain link column Contact used to be. Split Contact and added Follow Us as its own
   4th column rather than copying Pilothouse's literal logo/circle-icon styling, matching the
   structure Eli actually pointed at without importing another agency's visual identity. */
/* v101: Eli said the footer read "too vertical" and pointed back at Pilothouse's clean
   single-row layout. Root cause: Contact's real copy (the street address, "Not ready yet?
   Get weekly ad tips") is longer than Site's link labels, so at the old .95fr it wrapped
   mid-word inside the browser's own line-break logic, an uncontrolled 3-line wrap where only
   2 were ever intended, adding real height. Follow Us only ever holds "Instagram"/"YouTube",
   it never needed .8fr. Rebalanced so Contact gets meaningfully more room and Follow Us
   gives that room up. */
/* v102: that fixed Contact's own wrap, but Eli's next screenshot showed the real remaining
   cause of the "too vertical" complaint: the Site column itself, 10-12 links stacked in one
   list, taller than every other column combined. Split it into two side-by-side sub-columns
   (see .footer-site-cols below) so it reads closer to Pilothouse's own two short link
   columns instead of one long one. Site's own share bumped up (it now needs to fit two
   columns side by side) and Contact/Follow Us given back some of the room v101 gave them,
   the explicit <br> fix already controls Contact's wrap regardless of exact width now. */
@media(min-width:768px){ .footer-top{ grid-template-columns:1fr 1.3fr 1.05fr .55fr; } }
/* v101: reverted the v100 YouTube-banner logo swap, Eli asked to go back to the original
   real CDN wordmark. */
.footer-logo img{ height:20px; }
.footer-blurb{ color:var(--muted-2); font-size:.85rem; margin-top:16px; max-width:280px; line-height:1.6; }
.footer-links{ display:flex; flex-direction:column; gap:11px; }
.footer-links .h{ font-size:.75rem; text-transform:uppercase; letter-spacing:.06em; color:var(--muted-2); margin-bottom:6px; }
.footer-links a{ font-size:.9rem; color:var(--muted); transition:color .2s var(--ease-out); }
.footer-links a:hover{ color:var(--lime); }
/* v102: Site's two side-by-side sub-columns, desktop only (see mobile override below, which
   stacks them back into one list to match the rest of the site's centered single-column
   mobile footer). */
.footer-site-cols{ display:flex; gap:22px; }
.footer-site-col{ display:flex; flex-direction:column; gap:11px; }
/* v99: icon-led Contact rows (mail, map pin, arrow) and the new Follow Us list (Instagram,
   YouTube, the two real accounts Eli confirmed, the account-less placeholders for Facebook/
   X/LinkedIn/TikTok in the Pilothouse reference were skipped rather than invented). Icons use
   stroke:currentColor so they pick up the same muted/lime hover states as the link text
   already does via .footer-links a, no separate hover rule needed. */
.footer-contact-row{ display:flex; align-items:flex-start; gap:10px; }
.footer-contact-row .footer-icon{ margin-top:2px; }
span.footer-contact-row{ font-size:.9rem; color:var(--muted); }
.footer-follow-row{ display:flex; align-items:center; gap:10px; }
.footer-icon{ width:17px; height:17px; flex-shrink:0; color:var(--muted-2); }
.footer-bottom{ margin-top:30px; padding-top:24px; border-top:1px solid var(--border); display:flex; flex-direction:column; gap:20px; }
/* Chuck specifically wants this kept, real AI-chatbot referral traffic (ChatGPT,
   Perplexity) already comes through the equivalent row on his live site, same icons,
   same pre-filled prompt, self-hosted here rather than hotlinked from his Wix CDN.
   v166: moved out of .footer-bottom (sitewide, all 206 pages) into the Site column of
   .footer-top, right above the "Site" heading/links, per Eli's ask. No CSS changes
   needed for the move itself, this rule's existing centered-on-mobile/left-on-desktop
   alignment already matches how .footer-links behaves in its new home, and the parent's
   own flex gap now handles spacing above the "Site" label. */
/* v200: pulled out of the footer into its own standalone section (see .ask-ai-section
   below), per Eli's reference screenshot ("last section, not in the footer, more cool").
   Same real icons/links Chuck wants kept, same premium blue-band language already used
   for .portfolio-cta/.post-cta-band, just given real room instead of a small two-line
   row buried under "Site" links. Old .ai-summary-* rules removed sitewide along with the
   markup, nothing references them anymore. */
.ask-ai-section{ background:var(--premium); border-top:1px solid var(--premium-border); border-bottom:1px solid var(--premium-border); text-align:center; }
.ask-ai-section h2.section-title{ color:var(--premium-fg); }
.ask-ai-grid{ display:flex; flex-wrap:wrap; justify-content:center; gap:18px; margin-top:30px; }
.ask-ai-item{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:20px 22px; min-width:104px; border-radius:var(--radius);
  border:1px solid var(--premium-border); background:var(--card);
  transition:transform .2s var(--ease-out), border-color .2s var(--ease-out);
}
.ask-ai-item img{ width:56px; height:56px; border-radius:14px; display:block; }
.ask-ai-item-label{ font-size:.85rem; font-weight:600; color:var(--muted); }
.ask-ai-item:hover, .ask-ai-item:focus-visible{ transform:translateY(-4px); border-color:var(--lime); }
.ask-ai-item:hover .ask-ai-item-label, .ask-ai-item:focus-visible .ask-ai-item-label{ color:var(--fg); }
.footer-legal{ display:flex; flex-direction:column; gap:8px; font-size:.78rem; color:var(--muted-2); }
@media(min-width:768px){ .footer-legal{ flex-direction:row; justify-content:space-between; } }
@media(max-width:759px){
  .footer-top{ text-align:center; justify-items:center; }
  .footer-blurb{ margin-left:auto; margin-right:auto; }
  .footer-links{ align-items:center; }
  /* v102 stacked Site's two columns back into one long list on mobile, matching the rest
     of the single-column mobile footer. v164: Eli's screenshot showed exactly why that
     was wrong, 12 links in one column is the single biggest source of footer height,
     dwarfing every other block, and he asked for something more condensed/horizontal
     instead. Kept the two columns side by side on mobile too, same as desktop, each
     column's own short link labels ("Results", "Services", etc.) have plenty of room at
     even a narrow phone width, this alone roughly halves the Site section's height. */
  .footer-site-cols{ flex-direction:row; gap:32px; justify-content:center; }
  .footer-site-col{ align-items:center; }
  .footer-contact-row{ align-items:center; }
  .footer-legal{ align-items:center; text-align:center; }
}

/* mobile sticky cta, thumb zone reinforcement of the nav CTA */
.mobile-cta{
  position:fixed; bottom:0; left:0; right:0; z-index:50; padding:10px 14px max(12px,var(--safe-bottom));
  background:rgba(0,0,0,.9); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
  border-top:1px solid var(--border);
  /* v193: paired with the homepage script that toggles .cta-offscreen. Chuck's ask: the
     sticky bottom CTA shouldn't be visible while the hero's own "Book a Strategy Call"
     button is still on screen, two identical CTAs stacked in view at once reads
     redundant. Hidden state slides it below the viewport edge; the transition makes the
     reveal read as a deliberate slide-up once the hero button scrolls away, not a pop-in.
     Default (no class, JS never ran) stays visible, so pages without the script, and
     no-JS visitors, keep the always-on bar exactly as before. */
  transition:transform .3s var(--ease-out);
}
.mobile-cta.cta-offscreen{ transform:translateY(110%); }
@media(prefers-reduced-motion:reduce){ .mobile-cta{ transition:none; } }
.mobile-cta a{ height:48px; }
@media(min-width:900px){ .mobile-cta{ display:none; } }

/* reveal: base .reveal has no hidden state so content is always visible without JS
   (accessibility tools, crawlers, and no-JS users always see full content). JS adds
   .reveal-pending only when it can actually animate the reveal back in. Transform-only,
   never opacity: an automated single-pass scan (WAVE's browser executes this script)
   would otherwise catch below-fold text sitting at opacity:0 and flag it as zero
   contrast. Text stays fully opaque at all times, only its position animates. */
.reveal.reveal-pending{ transform:translateY(24px); transition:transform .7s var(--ease-smooth); }
.reveal.reveal-pending.in-view{ transform:translateY(0); }
@media(prefers-reduced-motion:reduce){ .reveal.reveal-pending{ transform:none; transition:none; } }

/* v221: whole-page grain texture REMOVED. Eli's real-device screenshots (viewed through
   WhatsApp's in-app browser) showed solid black rectangular boxes rendering over the trust-
   bar logos and hero content on scroll, not caught by headless/desktop testing since this is
   a real-GPU compositing bug, not a logic bug. Root cause: `mix-blend-mode:overlay` on a
   `position:fixed` full-page layer sitting on top of the pinned sticky hero video and the
   masked trust-bar, both already complex stacking contexts. This exact failure mode has
   already happened once before in this file: `.creative-teaser-fade`'s `mix-blend-mode:
   overlay` (see the v124 comment further down) caused visible glare over real video footage
   for the same underlying reason, and the fix that actually stuck was removing it outright,
   not retuning the opacity. Same call here: removed rather than re-tuned, since a real-GPU
   compositing bug isn't something this sandbox can verify a partial fix against. Value lost
   is minor (a barely-visible texture), the removed risk is a client-facing broken render on
   the exact call Eli's about to demo. */

/* v219: scroll progress bar, next-level-html skill pattern. Fixed thin bar along the very
   top edge, width tied 1:1 to scroll position (no animation/transition on width, it just
   tracks the scroll value directly), so nothing here needs a reduced-motion gate, it's
   feedback, not motion for its own sake. z-index 96, one above the grain layer, so it's
   never dulled by the noise overlay. JS lives in nav-scroll.js (already loaded sitewide)
   so this works identically across all pages without a second script tag anywhere. */
.scroll-bar{
  position:fixed; top:0; left:0; height:2px; width:0; z-index:96; pointer-events:none;
  background:linear-gradient(90deg, var(--lime), color-mix(in oklab, var(--lime) 55%, white));
}

/* v219: kinetic split-word headline, next-level-html skill pattern. Used only on the
   homepage hero H1 (#heroH1), not the generic .reveal class those other hero lines still
   use: a two-word headline is short enough that a per-word staggered slide-up reads as a
   distinct, deliberate beat rather than the same translateY fade every other .reveal
   element gets. Fires once on page load (not on scroll), so it never competes with the
   pinned hero video's scroll-driven effect, the two are sequenced: split-word on load,
   pin engages once the user starts scrolling. */
.split-line{ display:inline-block; overflow:hidden; vertical-align:top; }
.split-word{ display:inline-block; transform:translateY(115%); transition:transform .6s var(--ease-smooth); transition-delay:calc(var(--i,0) * 45ms); }
#heroH1.split-vis .split-word{ transform:translateY(0); }
@media(prefers-reduced-motion:reduce){ .split-word{ transform:none!important; transition:none!important; } }

/* v219: scroll-triggered image wipe (clip-path reveal), next-level-html skill pattern,
   adapted from the skill's raw version for this site's stricter no-JS/no-hidden-content
   rule. Two changes from the skill's default code: (1) the hidden state only applies once
   JS adds .clip-pending, same "starts fully visible, JS opts into the pending state" shape
   as .reveal above, so no-JS visitors, crawlers, and a WAVE scan that runs before JS
   finishes never see a permanently-broken image. (2) the clip only ever hides the bottom
   55%, never 100%: a full-100% clip is a genuine zero-rendered-area state for any tile that
   hasn't scrolled into view yet at scan time, which is exactly the kind of "looks invisible
   to an automated pass" issue already flagged once on this site (see the .reveal comment
   above re: WAVE and opacity:0). Partial clip still reads as a real wipe-in effect on
   scroll, just never fully zeroes the image out. */
.reveal-clip.clip-pending{ clip-path:inset(0 0 55% 0); transition:clip-path 1s var(--ease-smooth); }
.reveal-clip.clip-pending.vis{ clip-path:inset(0 0 0% 0); }
@media(prefers-reduced-motion:reduce){ .reveal-clip.clip-pending{ clip-path:none!important; transition:none!important; } }

/* visually hidden but accessible to screen readers */
.sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }

/* sub-page hero: shorter than the homepage's full-bleed video hero (this is an interior
   page, not the front door), plain dark background with the same premium type treatment
   instead of duplicating the video/overlay machinery on a second page. Centered to match
   the homepage's now-centered hero layout. */
.page-hero{ padding-top:calc(var(--nav-h) + var(--safe-top) + 56px); padding-bottom:48px; text-align:center; }
.page-hero h1{ font-size:clamp(2rem,6.4vw,3.2rem); max-width:760px; margin:0 auto; }
.page-hero .section-sub{ margin-left:auto; margin-right:auto; max-width:620px; }
.page-hero .hero-ctas{ margin-top:30px; display:flex; justify-content:center; }

/* creative portfolio gallery: real ad creatives at their native aspect ratios (a mix of
   portrait 700x1246 video-style thumbnails and square 700x700 static ads), a masonry-ish
   grid via CSS columns so different heights sit naturally without gaps, rather than
   forcing every tile into one fixed aspect ratio and cropping real creative work. */
.portfolio-grid{ column-count:1; column-gap:16px; margin-top:44px; }
@media(min-width:560px){ .portfolio-grid{ column-count:2; } }
@media(min-width:960px){ .portfolio-grid{ column-count:3; } }
.portfolio-tile{
  break-inside:avoid; margin-bottom:16px; border-radius:var(--radius); overflow:hidden;
  border:1px solid var(--border); background:var(--card); position:relative;
  transition:transform .3s var(--ease-out), border-color .3s var(--ease-out);
}
.portfolio-tile:hover{ transform:translateY(-4px); border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); }
.portfolio-tile img, .portfolio-tile video{ width:100%; height:auto; display:block; }

/* partner badges: reuses the homepage's .cert-chip pill treatment so it reads as one
   consistent trust pattern across pages instead of inventing a second style for the
   same idea. */
.partner-badges{ display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:16px; margin-top:40px; }

/* plain-text platform tags on the Services page (Meta Ads Agency, Shopify Ads Agency,
   etc.), distinct from .cert-chip which assumes a white background behind a logo image */
.platform-tag{
  display:inline-flex; align-items:center; background:var(--card); border:1px solid var(--border);
  border-radius:999px; padding:9px 18px; font-size:.82rem; font-weight:700; color:var(--fg);
  transition:border-color .2s var(--ease-out), transform .2s var(--ease-out);
}
/* these became real links out to the platform pages this pass; they were plain, non-
   interactive spans before with zero click affordance, so give them the same
   accent-border hover/press feedback already used on .wwd-card, rather than a bare
   invisible-until-clicked tag. */
a.platform-tag:hover{ border-color:color-mix(in oklab, var(--lime) 55%, var(--border)); }
a.platform-tag:active{ transform:scale(.96); }

/* portfolio CTA band: a simple centered closer, a faded-blue break (via --premium, see
   v82 above) so the page doesn't end on flat black with nothing to visually close it out.
   (Comment previously referenced the old .mechanism section as the matching cream break
   elsewhere on the site, that section was cut entirely back in v67, this is the only one
   of these left, and it's blue now, not cream.) */
.portfolio-cta{ background:var(--premium); border-top:1px solid var(--premium-border); border-bottom:1px solid var(--premium-border); text-align:center; }
.portfolio-cta h2.section-title{ color:var(--premium-fg); margin:0 auto; }
.portfolio-cta p{ color:var(--muted); font-size:1.05rem; margin-top:14px; max-width:560px; margin-left:auto; margin-right:auto; }
.portfolio-cta .hero-ctas{ margin-top:26px; display:flex; justify-content:center; }

/* homepage creative teaser: real pieces pulled from the Creative Portfolio page itself
   (same source images, not new assets), dragged/scrolled through on an ambient blue glow
   instead of a flat color band (the 1AtBat reference used a solid purple block; this
   reuses the same glow language already built for the hero so it reads as one brand
   instead of a bolted-on section). Cards are plain <a> wraps straight to the full
   portfolio page, drag-vs-click is disambiguated in the JS below by movement distance,
   not by intercepting every click. */
/* v112: two follow-ups after v111. First, Eli said the bottom of the section still read as
   "two blues trying to mesh," that was the original `::before` radial glow (restored in
   v111 as part of going back to "the darkness") visibly blending with the card images
   underneath, not actually a bug, just not what he wants anymore. Removed the glow
   entirely this time, flat plain dark background, no blend at all. Second, the vertical
   center-glow column from v111 wasn't what he had in mind, wanted a thin horizontal line
   across the videos instead. Rebuilt it as a full-viewport-width, short horizontal band
   instead of a tall narrow column (see .creative-teaser-track-wrap/.creative-teaser-center-
   glow below), and moved its positioning context to wrap only the track itself (not the
   hint/CTA below it), so `top:50%` centers on the actual card height at every breakpoint
   instead of the showcase's overall height (which includes the hint/CTA and would have
   pushed the line down into that area instead of across the cards). */
/* v120: v119's 40% peak was measured against the wrong reference, it landed at the TOP
   edge of the card row instead of its center, Eli caught it on a real screenshot: "the
   white fade needs to be in the middle of the videos not top." Re-measured directly off
   that screenshot instead of re-deriving from padding/margin math: heading block (eyebrow
   + title + subtext) ends around 22% of the section's total rendered height, the card row
   itself runs from roughly 28% down to 83%, putting its true vertical center at ~55%, not
   40%. Moved the white peak to 55% to match, and pushed the return-to-black point out to
   70% to keep the same fade width around it, everything after that (70%-100%, covering the
   hint text and CTA button) stays flat black same as v119. */
/* v121: Eli asked to make the white "even bigger, lower, cover the videos its like top
   half and middle now, the fade can reach bottom of the videos too", a genuine widening of
   the fade, not just a repositioning. v120's card-row range (28%-83%) is the reference:
   changed the single 55% peak into a held plateau from 45% (into the card row's top half)
   through 75% (most of the way down toward the card row's own bottom at 83%), so white now
   visibly spans top-half-through-lower-card rather than reading as a thin crossing line.
   Still fades back to flat `#000` by 88%, just before the hint text starts around 87-88%,
   so the CTA area underneath stays black and readable, only the card row itself gets the
   bigger white treatment. */
/* v122: three rounds (v119, v120, v121) all tried to line the white fade up with the card
   row by guessing a percentage of the WHOLE section's height, and every round needed
   re-tuning because that percentage isn't fixed, it shifts with how much room the heading
   text and the hint/CTA area take up, which changes across breakpoints (Eli: "looks good
   on mobile I don't love it on desktop"). Root cause of the visible "line" above "Drag to
   explore" on desktop specifically: the 88% fade-to-black completion point landed too close
   to (or inside) the hint text at that breakpoint's proportions, so the text sat directly
   in the transition instead of on solid black, both illegible and reading as a hard edge.
   Fixed properly this time instead of re-guessing another percentage: moved the white fade
   off the whole-section gradient entirely and onto a bounded overlay
   (`.creative-teaser-fade`) sized to `.creative-teaser-track-wrap`, a wrapper containing
   ONLY the track, not the hint/CTA (same wrapper element used for this exact reason back in
   v111/v112's center-glow, removed in v115 along with the glow itself, brought back now).
   Percentages inside that overlay are relative to the card row's own rendered height only,
   so they can't ever drift into the hint/CTA area regardless of breakpoint, and the section
   background itself is flat `#000`, so everything below the card row is unconditionally
   black already, nothing "fades into" black there because there was never anything but
   black to begin with. */
/* v124: Eli's follow-up after v123's opacity reduction: "all the vidoes need to be
   reviewed there is white glare in them all." Pulled the actual video files first to check
   (ffprobe on all 4: creative-1/3/8/22.mp4, all valid h264, correct 270x480 dims, no
   decode errors, playable start to finish, and pulled a real frame from each to compare
   against its poster, content matches what each card claims, e.g. creative-1 is genuinely
   the citrus skincare ad, creative-8 is genuinely "The Token"). So the files themselves are
   fine, nothing to fix there. The glare was `.creative-teaser-fade` itself:
   `mix-blend-mode:overlay` with any positive white value brightens whatever real pixels sit
   underneath it, that's what overlay blending does by definition, there isn't an opacity
   value that both "reads as a fade" and "doesn't visibly wash out real video footage", v122
   and v123 were both still versions of the same problem at different strengths. Removed
   `.creative-teaser-fade` and the `.creative-teaser-track-wrap` div it needed entirely
   rather than tuning a fourth number, an overlay sitting directly on top of real footage was
   never going to be compatible with keeping the videos looking clean. Section is back to
   flat `#000`, no glow/fade effect over the card row at all. */
.creative-teaser{
  position:relative; overflow:hidden; isolation:isolate;
  /* v206: transparent again. v204's version of this (video peeking through) got reverted
     to flat #000 in v205 because it was fighting with the arc-scale effect and the halo
     blob at the time, both of which are gone now, and the hero is a real GSAP-pinned
     section now rather than a CSS sticky trick, Eli asked directly for the hero video to
     show through behind this card row again now that the rest of the effect is built
     properly. Section itself paints nothing, whatever's visible behind the cards is
     `.hero-bg`'s video showing through from the still-pinned section underneath. */
  background:transparent;
}
/* v183: Eli's screenshots showed the black gap before "Results" was still there after
   v181's fix, root cause missed last round: `section{ padding:56px 0; }` (the sitewide
   default, near the top of this file) still applies to THIS section too, `.creative-teaser`
   never overrode it. So the real total was never just `.creative-teaser-showcase`'s own
   bottom padding plus `#results`'s top padding, it was that PLUS this section's own generic
   56px bottom padding stacked underneath, three paddings deep instead of two, which is why
   tightening only the showcase's padding in v181 barely moved the visible gap. This
   section's own bottom padding is redundant now that the showcase wrapper inside it
   already provides real spacing below the row, so it's dropped to 0 here (top padding,
   the space between the hero above and "Real Work" below, is untouched, that space reads
   fine and isn't what was flagged). */
.creative-teaser{ padding-bottom:0; }
/* v125/v126/v144: three earlier rounds of the "ambient blob glow" idea (large blurred
   circles sitting behind the card row, meant to peek through only in the 22px gaps between
   cards) got removed in v171. Root cause of the "big visible box the images bump into" Eli
   flagged on a screenshot at the time: those blobs were 400-460px fixed circles, but the
   actual visible gap they had to show through (22px between cards, plus the showcase's
   40/56px top/bottom padding) was far smaller than the blobs themselves. Most of each
   circle's disc was hidden directly behind the opaque card row (z-index:1 over the blob's
   z-index:0), leaving only the top and bottom ARCS of each circle visible in the padding
   above/below, and with 4 of them spread across the row those arc slivers combined into
   one flat-looking static rectangle that a moving row of cards read as "bumping into."
   v177: Eli confirmed he actually liked the ambient glow and wants it back, just rendered
   right this time, not removed outright. Redesigned rather than restoring the old 4-blob
   version verbatim, since that exact shape is what caused the bug: one single large, very
   softly blurred ellipse instead of four smaller ones.
   v178: Eli's next screenshot showed a flat black band right behind the actual cards, with
   the glow reading elsewhere, not where the row sits. Root cause: v177 centered the ellipse
   at "50% 50%" of `.creative-teaser`, but that element wraps the eyebrow/heading/subtext
   ABOVE the row too, not just the row itself, the heading block alone runs 150-250px tall
   at this font size. 50% of the WHOLE section's height lands somewhere in that heading
   area, well above the row's own true center, so the row itself sat out near the wash's
   already-faded edge, reading as flat black, exactly the "measuring against the wrong box"
   mistake this same file already made once before (the mid-video white line, v119-v122).
   Moved the pseudo-element from `.creative-teaser` to `.creative-teaser-showcase` instead,
   the wrapper that contains ONLY the track/hint/CTA, not the heading, so 50% of ITS height
   lands much closer to the row's actual center (the row is by far the tallest thing in that
   wrapper; the hint text and CTA button below add far less height than the heading above
   did, so the small remaining bias undershoots by tens of pixels instead of hundreds). */
/* v181: Eli's screenshot showed a hard blue edge at the top of this wash, fading normally
   at the bottom, not the even glow both versions above were meant to be. Root cause: this
   pseudo-element used a negative `inset` (bleeding past its own box on purpose, the usual
   way to make a soft wash feel edgeless) on an element that also carries `overflow-x:hidden`
   (`.creative-teaser-showcase`, needed for the real reason in the v138 comment below). Per
   the CSS overflow spec, when one axis is non-visible and the other is left at its default,
   the default axis is computed as `auto`, not `visible`, so overflow-y silently clips here
   too even though only overflow-x was ever set. That clipped the ellipse's bleed at this
   box's true top/bottom edges before the gradient had fully faded, a hard cutoff right at
   the edge, not the gradual fade the negative inset was designed to produce. Bottom
   happened to look fine only because the gradient's own falloff finished fading before it
   reached that edge; top didn't. Fixed by no longer relying on bleed past the box at all:
   `inset:0` keeps this fully inside the box that's actually clipping it, and the gradient's
   own transparent stop now lands well before 100% radius so it's already fully faded out
   before it would need to touch either edge, symmetric top and bottom, with no dependency
   on whether the browser clips the overflow-y or not. */
/* v204: the ambient halo/blob glow (v125-v192, full history above) is removed per Eli's
   direct ask after seeing it live combined with the (also-reverted) transparent section
   background, it read as patchy "blocks" behind the row instead of a clean wash. Section
   is back to flat #000 above with nothing painted behind the cards at all. */
.creative-teaser-showcase::before{ display:none; }
/* v111: the whole gradient-band experiment (v104-v110) is gone now, this wrapper's only
   remaining job is the full-bleed breakout for the track/hint/CTA. */
.creative-teaser-showcase{
  position:relative;
  width:100vw; margin-left:calc(50% - 50vw); margin-right:calc(50% - 50vw);
  padding:40px 0 56px;
  /* v138: this box sits at exactly the true viewport width via the 100vw/negative-margin
     trick above, but its ::before/::after glow blobs are positioned with a % left value
     wide enough (e.g. left:86% + 440px width) to extend past that box's own right edge.
     With no overflow rule here, that extra width was leaking out to the document itself
     (documentElement.scrollWidth ~180px wider than window.innerWidth on a 1512px viewport,
     confirmed live), a real page-level horizontal-scroll bug, not just a visual nit. Fixed
     with overflow-x:hidden here: clips the blobs at the true viewport edge (invisible
     anyway, they're 52px-blurred glow, not sharp content) without touching the track's own
     internal overflow-x:auto scroll, which governs its own children independently. */
  overflow-x:hidden;
}
/* v181: Eli flagged a big flat dead gap on mobile between this section's own CTA button
   and the "Results" heading right after it. The 56px bottom padding here plus
   `.section-compact`'s own 32px top padding on `#results` (a shared class used elsewhere
   too, left untouched to avoid side effects on other sections) added up to a stretch of
   plain solid black with no glow reaching it, the creative-teaser wash fades out inside
   this box's own padding and the results section's lime bridge glow only starts at its own
   top edge, so the space between the two was genuinely bare, not just visually busy.
   Tightened this side of the seam instead of touching the shared class: less raw dead
   space to begin with, on the one side that's safe to change without affecting anything
   else on the page. */
@media(max-width:639px){
  .creative-teaser-showcase{ padding:32px 0 26px; }
}
/* wall-to-wall breakout: Eli sent 1AtBat's reference where the row runs edge to edge in
   the real browser viewport, cards cut off flush against both sides, instead of stopping
   at the site's normal ~1180px .wrap container like every other section. Standard
   full-bleed-child trick, 100vw + negative margins pulls just this one element out to the
   true viewport width regardless of the parent .wrap's max-width/padding, no DOM change
   needed and every other element in the section (heading, hint, CTA) stays normally
   centered. Zero horizontal padding on the track itself so the first/last card actually
   touch the edge instead of leaving a gap. */
.creative-teaser-track{
  display:flex; align-items:center; gap:22px; overflow-x:auto; overflow-y:hidden; overscroll-behavior-x:contain;
  cursor:grab;
  -webkit-overflow-scrolling:touch; scrollbar-width:none; user-select:none;
  width:100vw; margin-left:calc(50% - 50vw); margin-right:calc(50% - 50vw);
  /* v141-v184 (full history preserved in git): this track used to carry a top/bottom inset
     box-shadow to fade the per-card halo glow (and transform:scale() overflow) out before
     `overflow-y:hidden` clipped it at a hard edge. Both of those sources are gone now
     (per-card glow removed v185, arc-scale transform removed v204), so the shadow had
     nothing left to hide, it was just painting two dark bands of its own. Those bands were
     invisible against the section's old flat `#000` background, but now that `.creative-
     teaser` is transparent and the pinned hero video shows through (v206/v212), the same
     shadow read as a hard black line right at the top of the card row, exactly what Eli
     flagged. Removed outright rather than softened, it's vestigial. */
  padding:40px 0;
}
/* v182: paired with the updateArcScale() rewrite in index.html (camelback taper instead of
   a straight ramp to the true edge). That JS change stops enlarged cards from clipping at
   the literal viewport boundary, this adds real breathing room on top of that so cards
   aren't starting their scroll flush against the true edge in the first place, "space
   needs to be expanded" per Eli's ask. Desktop only, this row's own wall-to-wall breakout
   (v138) and the arc-scale effect (v95) are both desktop-only already, mobile keeps its
   original zero-padding edge-to-edge feel. */
@media(min-width:900px){
  .creative-teaser-track{ padding-left:64px; padding-right:64px; }
}
.creative-teaser-track::-webkit-scrollbar{ display:none; }
.creative-teaser-track.dragging{ cursor:grabbing; }
/* the size-variant (lg/sm) experiment read as an uneven, mismatched row once actually
   rendered (some cards towering over others, the tall bracelet piece especially), so
   every card went back to one uniform base width, the --lg/--sm classes stayed in the
   markup unused. Repurposed here instead of deleted: this pass adds a second, different
   kind of size variance, an "arc" driven by each card's live position in the browser
   viewport (bigger near the left/right edges, smaller toward the center), recalculated
   every frame in JS as cards move, rather than a fixed per-card size baked into the
   markup, so it isn't the same effect that got reverted before. --arc-scale is set by
   that JS, defaulting to 1 so cards render normally before the first frame runs (and stay
   at 1 permanently if JS never sets it, e.g. reduced-motion). */
.creative-teaser-card{
  /* v104: bumped from var(--radius) (12px, the shared sitewide token) to a card-specific
     20px, matching how noticeably rounder the reference's cards read compared to ours.
     Scoped to this class only rather than raising --radius itself, that token is shared by
     buttons/inputs/most cards sitewide, changing it would round corners everywhere, not
     just here. */
  /* v110: widths bumped up a step at every breakpoint (150->175, 196->235, 224->268) per
     Eli's "make the videos larger" ask. */
  position:relative; flex:0 0 auto; width:175px; border-radius:20px;
  border:1px solid var(--border); background:var(--card); display:block; z-index:1;
  /* v204: the arc grow/shrink-by-position effect (v83-v186) is removed, the JS that used
     to write --arc-scale/--arc-shift every frame is gone too (see index.html). Cards render
     at one uniform size the whole time now, moving only via the track's own scrollLeft. */
  transition:border-color .3s var(--ease-out);
  will-change:transform;
  /* v121: Eli caught a real rendering bug, corners were reverting to square on cards away
     from center (i.e. whenever --arc-scale isn't exactly 1). Known Chromium/WebKit issue:
     a border-radius + overflow:hidden clip on the same element that also carries a
     transform isn't always respected by the GPU compositing layer the transform promotes
     the element into, the rounded clip can get dropped mid-animation. The v121 fix forced
     the corner through a mask-image instead of relying on overflow:hidden's clip alone,
     which held up for images but not reliably for video: video decode surfaces get their
     own independent compositor layer, and Eli caught square/cut-off corners specifically
     on the video cards (v178). Root cause: overflow:hidden + mask-image were still living
     on THIS element, which also carries the transform above, the exact combination v121
     already flagged as fragile, just not fully eliminated. Real fix (v178): border-radius
     stays here (still needed so this element's own border/background/box-shadow below
     paint with rounded corners), but overflow:hidden and the mask-image moved off this
     transformed element entirely, onto a new inner `.creative-teaser-media` wrapper (see
     below) that holds the img/video and never itself carries a transform. Clipping now
     happens on a plain, non-transformed box, which sidesteps the compositor conflict
     instead of patching around it again. */
  /* v125-v185: this element used to carry its own per-card box-shadow halo, intensity
     tied to --arc-scale/--arc-glow so whichever card was centered lit up brightest. Eli's
     screenshot of the live row called it out directly: individual cards glowing as they
     move reads as ugly, not premium, and he specifically remembered the single ambient
     wash behind the whole section (`.creative-teaser-showcase::before`, still very much
     in place below) reading well on its own before this per-card layer got added on top
     of it. Removed the per-card box-shadow entirely rather than just toning it down, one
     subtle halo for the section is the actual ask, not 18 individually pulsing ones. The
     --arc-scale-driven size/position arc (transform, above) is untouched, that wasn't
     what was flagged. */
}
/* v178: new inner wrapper, holds the img/video and does the actual rounded clipping.
   Deliberately carries NO transform of its own, so the border-radius+overflow:hidden clip
   here isn't subject to the GPU-compositor conflict described in the v121/v178 comment
   above, that conflict only bites when the clipping element and the transformed element
   are the same node. `.creative-teaser-card` still handles layout/motion (transform,
   box-shadow halo, will-change), this handles visuals (clip, mask). position:relative so
   `.creative-teaser-play` (unchanged, still absolutely positioned top:10px/right:10px)
   continues to anchor to the media box itself, same visual spot as before. */
.creative-teaser-media{
  position:relative; width:100%; overflow:hidden; border-radius:20px; display:block;
  -webkit-mask-image:-webkit-radial-gradient(white, black);
  mask-image:radial-gradient(white, black);
}
.creative-teaser-card--lg, .creative-teaser-card--sm{ width:175px; }
@media(min-width:640px){
  .creative-teaser-card, .creative-teaser-card--lg, .creative-teaser-card--sm{ width:235px; }
}
@media(min-width:1024px){
  .creative-teaser-card, .creative-teaser-card--lg, .creative-teaser-card--sm{ width:268px; }
}
.creative-teaser-track:not(.dragging) .creative-teaser-card:hover{
  border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); z-index:2;
}
@media(prefers-reduced-motion:reduce){ .creative-teaser-card{ transform:none !important; transition:none; } }
.creative-teaser-card img{ width:100%; height:100%; aspect-ratio:7/11; object-fit:cover; display:block; pointer-events:none; background:#000; }
/* v123: Eli flagged "one video isnt playing well put a little blue fade behind the videos
   or white". A `<video>` with `preload="none"` has nothing decoded until playback actually
   starts (unlike an `<img>`, which paints as soon as its bytes arrive), so if a video stalls
   or is slow to start, viewers were seeing flat `#000`, a hard black hole where a card
   should be, easy to misread as broken. Gave video cards specifically their own subtle
   backdrop instead of plain black, a soft diagonal blue-into-black fade using the same
   `--lime` blue as the rest of the site, so any gap before a frame paints reads as an
   intentional dark background, not a blank/broken one. Left `<img>` on plain `#000`, images
   paint immediately from their own bytes so this gap doesn't happen there. */
.creative-teaser-card video{
  width:100%; height:100%; aspect-ratio:7/11; object-fit:cover; display:block; pointer-events:none;
  background:linear-gradient(160deg, color-mix(in oklab, var(--lime) 22%, #000) 0%, #000 70%);
}
.creative-teaser-play{
  position:absolute; top:10px; right:10px; width:28px; height:28px; border-radius:50%;
  background:rgba(0,0,0,.55); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  display:flex; align-items:center; justify-content:center; pointer-events:none;
}
.creative-teaser-play svg{ width:11px; height:11px; fill:#fff; margin-left:2px; }
.creative-teaser-hint{
  display:block; text-align:center; font-size:12px; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted-2); margin-top:14px;
}
.creative-teaser-hint .arr{ display:inline-block; margin-left:6px; animation:nudge 1.6s ease-in-out infinite; }

/* blog index: real posts pulled from the live Wix sitemap, most linking out to the live
   rckstrmedia.com/post/<slug> URL until that specific post is fully migrated onto this
   site (a handful are migrated, see the .blog-migrated modifier below), so nothing is a
   thin/fabricated placeholder and no link is ever broken. Card grid, not a plain list,
   since 180+ entries in a single-column list would be an enormous scroll on both
   desktop and mobile. */
.blog-toolbar{ margin-top:36px; display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
.blog-count{ font-size:.85rem; color:var(--muted-2); }
.blog-grid{ display:grid; grid-template-columns:1fr; gap:14px; margin-top:28px; }
@media(min-width:640px){ .blog-grid{ grid-template-columns:repeat(2,1fr); } }
@media(min-width:1024px){ .blog-grid{ grid-template-columns:repeat(3,1fr); } }
.blog-card{
  display:block; background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  padding:20px 20px 22px; transition:transform .25s var(--ease-out), border-color .25s var(--ease-out);
}
.blog-card:hover{ transform:translateY(-3px); border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); }
.blog-card-date{ font-size:.72rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--muted-2); }
.blog-card-title{ font-size:1rem; font-weight:700; margin-top:10px; line-height:1.4; }
/* migrated posts (real content now hosted on this domain) get a small internal badge so
   it's visually clear which ones stay on-site vs which open the live rckstrmedia.com
   post in a new tab. */
.blog-card.blog-migrated{ border-color:color-mix(in oklab, var(--lime) 35%, var(--border)); }
.blog-card-badge{
  display:inline-block; margin-top:10px; font-size:.7rem; font-weight:700; color:var(--lime);
  letter-spacing:.04em; text-transform:uppercase;
}

/* individual migrated blog posts: a real article-prose layout, reusing the site's
   existing .faq-list/.faq-item pattern from the homepage FAQ section for each post's
   own FAQ block rather than inventing a second accordion component. */
.post-hero{ padding-top:calc(var(--nav-h) + var(--safe-top) + 48px); padding-bottom:8px; }
.post-hero .eyebrow{ margin-bottom:10px; }
.post-hero h1{ font-size:clamp(1.9rem,5.6vw,2.8rem); max-width:820px; }
.post-meta{ margin-top:16px; font-size:.85rem; color:var(--muted-2); }
.post-tldr{
  margin-top:32px; background:var(--card); border:1px solid var(--border); border-left:2px solid var(--lime);
  border-radius:0 var(--radius) var(--radius) 0; padding:22px 24px; max-width:720px;
}
.post-tldr .h{ font-size:.75rem; text-transform:uppercase; letter-spacing:.08em; color:var(--lime); font-weight:800; margin-bottom:10px; }
.post-tldr ul{ margin:0; padding-left:18px; }
.post-tldr li{ color:var(--fg); font-size:.95rem; line-height:1.6; margin-bottom:8px; }
.post-tldr li:last-child{ margin-bottom:0; }
.post-body{ max-width:720px; margin-top:36px; }
.post-body h2{ font-size:1.4rem; margin-top:40px; margin-bottom:14px; letter-spacing:-.01em; }
.post-body h3{ font-size:1.1rem; margin-top:28px; margin-bottom:10px; }
.post-body p{ color:var(--muted); font-size:1rem; line-height:1.7; margin-bottom:16px; }
.post-body strong{ color:var(--fg); }
.post-body ul, .post-body ol{ color:var(--muted); font-size:1rem; line-height:1.7; margin:0 0 16px; padding-left:22px; }
.post-body li{ margin-bottom:8px; }
.post-body a{ color:var(--lime); text-decoration:underline; text-underline-offset:2px; }
.post-faq{ max-width:720px; margin-top:8px; }
.post-cta-band{
  margin-top:48px; max-width:720px; background:var(--premium); border:1px solid var(--premium-border);
  border-radius:var(--radius); padding:28px 26px;
}
.post-cta-band h3{ color:var(--premium-fg); font-size:1.15rem; }
.post-cta-band p{ color:var(--muted); margin-top:10px; font-size:.95rem; }
.post-cta-band .btn-primary{ margin-top:18px; }
.post-back{ display:inline-flex; align-items:center; gap:6px; font-size:.85rem; color:var(--muted-2); margin-top:36px; }
.post-back:hover{ color:var(--lime); }

/* v203: AEO audit found zero internal links between the 187 posts and 11 service pages
   anywhere on the site, one of the biggest topical-authority gaps a site can have. This
   is the post side: a small "Keep Reading" block linking 3 topically related posts
   (matched by shared title keywords + category, computed once and baked into the static
   HTML rather than client-side) plus the one service page the post's topic actually
   supports. Kept visually quiet (border-top rule, no card chrome) so it reads as a
   natural next-step, not another CTA competing with .post-cta-band above it. */
.post-related{ max-width:720px; margin-top:40px; padding-top:28px; border-top:1px solid var(--border); }
.post-related h3{ font-size:.95rem; text-transform:uppercase; letter-spacing:.08em; color:var(--muted-2); margin-bottom:14px; }
.post-related-grid{ display:flex; flex-direction:column; gap:10px; }
.post-related-link{ color:var(--fg); font-size:.95rem; line-height:1.5; text-decoration:underline; text-decoration-color:var(--border); text-underline-offset:3px; }
.post-related-link:hover{ text-decoration-color:var(--lime); color:var(--lime); }
.post-related-service{ display:inline-flex; align-items:center; gap:6px; margin-top:18px; font-size:.9rem; font-weight:600; color:var(--lime); }
.post-related-service:hover{ text-decoration:underline; }

/* Service-page side of the same fix: a "From the Blog" section pulling in the posts
   that matched this service, same card language as the rest of the site (border,
   var(--card) bg, hover lift) rather than a plain link list, since this sits as a full
   section rather than a quiet footnote like .post-related above. */
.from-blog-grid{ display:grid; grid-template-columns:1fr; gap:14px; margin-top:30px; max-width:760px; margin-left:auto; margin-right:auto; }
@media(min-width:700px){ .from-blog-grid{ grid-template-columns:1fr 1fr; } }
.from-blog-card{
  display:flex; flex-direction:column; gap:8px; padding:18px 20px; border-radius:var(--radius);
  border:1px solid var(--border); background:var(--card); text-align:left;
  transition:transform .2s var(--ease-out), border-color .2s var(--ease-out);
}
.from-blog-card:hover{ transform:translateY(-3px); border-color:var(--lime); }
.from-blog-cat{ font-size:.72rem; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--lime); }
.from-blog-title{ font-size:.98rem; font-weight:600; color:var(--fg); line-height:1.4; }

/* team page: single real profile card (Chuck's live "Meet The Team" page currently
   lists only himself, no other team members), photo + bio + real social links. */
.team-card{
  display:grid; grid-template-columns:1fr; gap:28px; margin-top:44px; max-width:880px;
}
@media(min-width:700px){ .team-card{ grid-template-columns:220px 1fr; } }
.team-photo{ border-radius:var(--radius); overflow:hidden; border:1px solid var(--border); aspect-ratio:5/6; background:var(--card); }
.team-photo img{ width:100%; height:100%; object-fit:cover; display:block; }
.team-info h2{ font-size:1.5rem; }
.team-role{ color:var(--lime); font-size:.85rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; margin-top:6px; }
.team-bio{ color:var(--muted); font-size:.98rem; line-height:1.7; margin-top:18px; }
.team-social{ display:flex; gap:14px; margin-top:22px; }
.team-social a{
  width:38px; height:38px; border-radius:50%; background:var(--card); border:1px solid var(--border);
  display:flex; align-items:center; justify-content:center; transition:transform .2s var(--ease-out), border-color .2s var(--ease-out);
}
.team-social a:hover{ transform:translateY(-2px); border-color:color-mix(in oklab, var(--lime) 45%, var(--border)); }
.team-social svg{ width:18px; height:18px; color:var(--muted); }
.team-social a:hover svg{ color:var(--lime); }

/* case studies page: real stat cards from rckstrmedia.com/case-studies, reusing the
   homepage's .wwd-card visual treatment for consistency. */
.cs-grid{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:44px; }
@media(min-width:640px){ .cs-grid{ grid-template-columns:repeat(2,1fr); } }
@media(min-width:1024px){ .cs-grid{ grid-template-columns:repeat(3,1fr); } }
.cs-card{ background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:26px 22px; }
.cs-card .platform{ font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted-2); }
.cs-card .stat{ font-size:1.7rem; font-weight:800; color:var(--lime); margin-top:10px; letter-spacing:-.01em; line-height:1.15; }
.cs-card .desc{ font-size:.9rem; color:var(--muted); margin-top:10px; line-height:1.55; }
.cs-mini-grid{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:20px; }
@media(min-width:640px){ .cs-mini-grid{ grid-template-columns:repeat(2,1fr); } }
@media(min-width:1024px){ .cs-mini-grid{ grid-template-columns:repeat(3,1fr); } }
.cs-mini{ background:var(--muted-surface); border:1px solid var(--border); border-radius:var(--radius-sm); padding:18px 20px; }
.cs-mini .h{ font-size:.95rem; font-weight:700; }
.cs-mini p{ font-size:.85rem; color:var(--muted); margin-top:8px; line-height:1.5; }

/* pricing page: real 2-tier structure from rckstrmedia.com/pricing (both tiers are
   "$Custom", no published rate card, matches the existing homepage FAQ answer). */
.pricing-grid{ display:grid; grid-template-columns:1fr; gap:20px; margin-top:44px; max-width:900px; }
@media(min-width:768px){ .pricing-grid{ grid-template-columns:repeat(2,1fr); } }
/* single real tier (Growth Partner) as of the v53 pricing-copy update, centers one card
   instead of stretching it across the old 2-column grid meant for two tiers */
.pricing-grid.single{ max-width:560px; margin-left:auto; margin-right:auto; }
@media(min-width:768px){ .pricing-grid.single{ grid-template-columns:1fr; } }
.pricing-card{ background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:32px 28px; display:flex; flex-direction:column; }
.pricing-card h3{ font-size:1.3rem; }
.pricing-card .price{ font-size:1.1rem; font-weight:800; color:var(--lime); margin-top:8px; }
.pricing-card .lead{ color:var(--muted); font-size:.92rem; margin-top:14px; line-height:1.6; }
.pricing-list{ margin-top:20px; flex:1; }
.pricing-list li{ display:flex; align-items:flex-start; gap:10px; font-size:.9rem; color:var(--fg); margin-bottom:12px; line-height:1.5; }
.pricing-list li::before{ content:"\2713"; color:var(--lime); font-weight:800; flex-shrink:0; }
.pricing-card .note{ font-size:.8rem; color:var(--muted-2); margin-top:16px; }
.pricing-card .btn{ margin-top:24px; align-self:flex-start; }

/* social-media-ads-1.html: real content migrated from rckstrmedia.com/social-media-ads-1
   (Chuck's live "For DTC Brands" funnel page, fed by ad traffic and the site's own Ask AI
   buttons). Two of the old page's sections (a "who we work with" graphic and a "us vs.
   traditional agencies vs. in-house" comparison table) only existed as baked Wix images
   with no accessible text, downloaded and transcribed by hand rather than fabricated or
   left out, then rebuilt as real HTML instead of hot-linking someone else's CDN images.
   Reviews reuse a plain grid instead of the homepage's scroll-carousel, since the carousel
   dots/swipe behavior is homepage-only JS not worth porting for a 3-card static list. */
.social1-review-grid{ display:grid; grid-template-columns:1fr; gap:16px; }
@media(min-width:768px){ .social1-review-grid{ grid-template-columns:repeat(3,1fr); } }
.social1-compare-cols{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:32px; max-width:820px; margin-left:auto; margin-right:auto; }
@media(min-width:640px){ .social1-compare-cols{ grid-template-columns:1fr 1fr; } }
.social1-compare-col{ background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:24px; }
.social1-compare-head{ font-weight:800; font-size:1rem; margin-bottom:14px; }
.social1-compare-yes .social1-compare-head{ color:var(--lime); }
.social1-compare-no .social1-compare-head{ color:var(--muted); }
.social1-compare-col ul{ display:flex; flex-direction:column; gap:12px; }
.social1-compare-col li{ font-size:.92rem; color:var(--fg); line-height:1.5; padding-left:20px; position:relative; }
.social1-compare-yes li::before{ content:"\2713"; position:absolute; left:0; color:var(--lime); font-weight:800; }
.social1-compare-no li::before{ content:"\2715"; position:absolute; left:0; color:var(--muted-2); font-weight:800; }
.social1-table-wrap{ overflow-x:auto; margin-top:32px; -webkit-overflow-scrolling:touch; }
.social1-table{ width:100%; min-width:560px; border-collapse:collapse; font-size:.88rem; }
.social1-table th, .social1-table td{ padding:12px 14px; border-bottom:1px solid var(--border); text-align:left; }
.social1-table thead th{ color:var(--muted-2); font-size:.75rem; text-transform:uppercase; letter-spacing:.04em; font-weight:700; }
.social1-table tbody th{ color:var(--muted); font-weight:600; white-space:nowrap; }
.social1-table tbody td:first-of-type{ color:var(--lime); font-weight:700; }

/* v226: newsletter.html signup form. Chuck's Aug 6 email asked to recreate the newsletter
   page from the old Wix site; this site had no native <form> anywhere yet (the CSP's
   `connect-src` already pre-allowed formspree.io for exactly this eventuality, see the
   long comment on that rule in _headers), so this is the first real form styling pass.
   Deliberately plain: a single email field, one checkbox, one button, real Netlify Forms
   markup (`data-netlify`, honeypot, hidden form-name input) so submissions land in
   Netlify's dashboard without any backend of our own, matching what Chuck described
   wanting to wire up on his end via the Netlify/make.com integration. */
.newsletter-form{ max-width:480px; margin:28px auto 0; display:flex; flex-direction:column; gap:16px; text-align:left; }
.newsletter-field label{ display:block; font-size:13px; font-weight:600; color:var(--muted); margin-bottom:6px; }
.newsletter-field input[type="email"]{
  width:100%; background:var(--input); border:1px solid var(--border); border-radius:var(--radius-sm);
  padding:14px 16px; font-size:16px; color:var(--fg); font-family:inherit;
}
.newsletter-field input[type="email"]:focus-visible{ outline:2px solid var(--lime); outline-offset:2px; border-color:var(--lime); }
.newsletter-checkbox{ display:flex; align-items:flex-start; gap:10px; font-size:14px; color:var(--muted); line-height:1.4; cursor:pointer; }
.newsletter-checkbox input{ margin-top:3px; width:16px; height:16px; accent-color:var(--lime); flex-shrink:0; }
/* honeypot: off-screen, not display:none/visibility:hidden (some bots skip filling fields
   detected as invisible via computed style, positioning off-canvas is more reliable). Real
   users on a screen reader never reach it either since it's also aria-hidden. */
.newsletter-hp{ position:absolute; left:-9999px; top:-9999px; }
.newsletter-success{
  display:none; text-align:center; padding:24px; background:var(--premium); border:1px solid var(--premium-border);
  border-radius:var(--radius); margin-top:28px; max-width:480px; margin-left:auto; margin-right:auto;
}
.newsletter-success.show{ display:block; }
.newsletter-success p{ color:var(--fg); font-size:15px; line-height:1.5; }
.newsletter-success a{ color:var(--lime); font-weight:700; }
.newsletter-form.hide{ display:none; }
.newsletter-stats{ display:flex; flex-wrap:wrap; justify-content:center; gap:32px 48px; margin:36px 0 0; }
.newsletter-stats .hero-stat{ margin:0; }
