/**
 * Unlim Code — custom styles
 *
 * Everything hand-written for Unlim Code lives here. It is loaded AFTER
 * assets/conicorn.shared.*.css, which is a generated export protected by an
 * SRI integrity hash — editing that file makes the browser refuse to load it,
 * so never touch it. Add overrides here instead.
 *
 * Values below reuse the export's design tokens (--_color---*, --font-size--*,
 * --_radius---*, --_spacing---*) so this stays visually in sync with the rest
 * of the site.
 */

:root {
  /* .navbar-wraper's own padding-top (export default: 2rem) — the single
     source of truth for how far the navbar sits from the safe top edge.
     Reduced on mobile below (max-width:991px) so the navbar sits closer to
     the top there, as on /template; --unlim-nav-h and the safe-area "top"
     offset below both derive from this one value, so they never drift out
     of sync with whatever this is set to at a given width. */
  --unlim-nav-pad-top: 2rem;
  /* The navbar is position:fixed, (--unlim-nav-pad-top + 3.5rem) tall
     (.navbar-wraper's own padding-top + the 3.5rem navbar-brand-link row).
     That assumes .navbar's own "top" offset is 0 — true whenever
     env(safe-area-inset-top) <= --unlim-nav-pad-top (the .navbar-wraper >
     .navbar rule below only shifts it down past that). On notch/Dynamic-
     Island phones (env(safe-area-inset-top) is only nonzero on the homepage,
     via viewport-fit=cover — see components/head.html) the inset can be
     bigger than --unlim-nav-pad-top, so the navbar actually renders lower
     and a flat reserve would leave hero content (the eyebrow) peeking out
     from under it. max() keeps the plain reserve everywhere this doesn't
     apply (env()=0 off the homepage, or <= --unlim-nav-pad-top on non-notch
     phones) and grows to match the navbar's real bottom edge otherwise. */
  --unlim-nav-h: max(calc(var(--unlim-nav-pad-top) + 3.5rem), calc(env(safe-area-inset-top, 0px) + 3.5rem));
}

/* Mobile/tablet — the navbar can sit noticeably closer to the top than the
   export's flat 2rem (~1rem above the safe/visual top edge reads better at
   this size); desktop keeps the export's own 2rem, untouched. */
@media screen and (max-width: 991px) {
  :root {
    --unlim-nav-pad-top: 1rem;
  }
}

/* Local variable Montserrat — used only by the preloader wordmark below.
   Not a Google Fonts / @import — a local file already in assets/fonts/.
   Rest of the site keeps using Geist; nothing else references this. */
@font-face {
  font-family: 'Montserrat';
  src: url('/assets/fonts/Montserrat-Variable.ttf') format('truetype');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Homepage preloader — must sit above the fixed navbar, cookie banner and
   chat launcher (the export's own z-index:100 doesn't). Entrance/exit is
   the native Webflow IX3 timeline (interaction i-da903e25, timeline
   t-1bef9bdc, wf:load) — no companion JS.
   opacity:1 is still set here, in render-blocking CSS, because IX3's own
   actions never touch opacity: the export's own opacity:0 default
   (assets/conicorn.shared*.css) would otherwise leave .preloader invisible
   forever.
   The logo/letters' translateY(2em) starting point used to be pinned here
   too, but IX3's own actions animate from y:2em to y:null (revert to
   whatever the CSS cascade gives with no override) — with a permanent
   translateY(2em) still in this stylesheet, "no override" resolved right
   back to 2em, so the reveal never reached y:0. IX3 applies its own from
   value at runtime, so this stylesheet doesn't need to pre-set it. */
.preloader {
  z-index: 9999;
  opacity: 1;
  /* The export sizes this with height:100vh (assets/conicorn.shared*.css),
     which on mobile is the LARGEST possible viewport (toolbar collapsed).
     IX3's exit transform is translate3d(0,-100%,0) — relative to the
     preloader's OWN box height, not the current visible viewport. If the
     address bar is still showing when that height is measured, 100vh is
     taller than what's actually visible, and moving up by "the whole box"
     doesn't clear the sliver of extra height the toolbar was hiding — a
     thin black strip stays pinned at the top after the exit finishes.
     100dvh tracks the CURRENT real visible viewport continuously (browsers
     without dvh support just keep the export's 100vh above), so the
     translate always clears exactly what's visible, regardless of toolbar
     state. Layered after the export's rule via the cascade — no !important
     needed since this file loads after it (components/head.html). */
  height: 100dvh;
}
/* Initial-flash fix: register()'s classList.add("w-mod-ix3") (which lifts
   the export's own visibility gate in components/head.html) and GSAP's own
   first render of the preload timeline's y:2em "from" state land on two
   different frames — GSAP only renders on its own next ticker tick, so for
   one frame the logo/wordmark show at their natural, fully-visible resting
   position before IX3 pushes them down and starts the reveal. Hide just
   these two (not the vendor gate's whole list) until src/pages/index.html's
   script confirms that render already happened, then lift it — no
   transform involved, so it can't reintroduce the y:2em/CSS-cascade
   conflict removed above. */
.preloader .preload-logo,
.preloader .preload-text {
  visibility: hidden !important;
}
html.preload-ix3-rendered .preloader .preload-logo,
html.preload-ix3-rendered .preloader .preload-text {
  visibility: visible !important;
}
/* Preloader logo size — adjust this value only */
.preloader .preload-logo {
  width: clamp(1.5em, 8vw, 3em);
}
.preload-text {
  display: flex;
  align-items: baseline;
  /* "UNLIM CODE" must never break mid-word — white-space is inherited, so
     this alone covers .gsap_split_word/.gsap_split_letter below (neither
     sets its own white-space). Belt-and-suspenders alongside the sizing
     fix on the two rules below: even if a future edit changes the copy or
     the font metrics, this guarantees no wrap regardless. */
  white-space: nowrap;
}

/* Both letter rules below share this font-size formula (only weight/
   letter-spacing/color differ, unchanged). The outer clamp(_, 5vw, 80px)
   is the original export value, untouched above a 366px viewport (5vw only
   overtakes the 40px floor once vw hits 800px, so nothing here changes
   until then). Below 366px, the flat 40px floor doesn't leave "UNLIM CODE"
   enough width next to the logo — measured: at 320px the logo (~65px) +
   gap (~12px) leaves 243px for text, but "UNLIM CODE" needs 289px at 40px,
   so the last letter of each word wrapped to a second line. The inner
   clamp(32px, calc(17vw - 22px), 40px) replaces that flat floor with one
   that ramps down to ~32px by 320px (comfortably under the 243px budget)
   and rejoins the original 40px by 366px — reusing the outer clamp's own
   mechanism (available-width sizing) rather than a device-specific rule,
   and provably identical to the original for every width the bug didn't
   affect. */
.preload-text > .gsap_split_word:first-child .gsap_split_letter:not(:has(.gsap_split_word)) {
  font-family: 'Montserrat', sans-serif;
  font-weight: 600;
  font-size: clamp(clamp(32px, calc(17vw - 22px), 40px), 5vw, 80px);
  font-synthesis: none;
  letter-spacing: 0.1em;
  color: #f6f6f6;
}
.preload-text > .gsap_split_word:last-child .gsap_split_letter:not(:has(.gsap_split_word)) {
  font-family: 'Montserrat', sans-serif;
  font-weight: 400;
  font-size: clamp(clamp(32px, calc(17vw - 22px), 40px), 5vw, 80px);
  font-synthesis: none;
  letter-spacing: 0.05em;
  color: #f6f6f6bf;
}

/* .navbar-wraper's own padding-top — export default 2rem, reduced to 1rem on
   mobile/tablet by the --unlim-nav-pad-top override above. */
.navbar-wraper {
  padding-top: var(--unlim-nav-pad-top);
}

/* Clears the Dynamic Island/notch once viewport-fit=cover is set (homepage
   only — see src/pages/index.html's @page front matter); env() is 0
   everywhere else, so this is a no-op elsewhere. Offsets .navbar itself
   (already position:relative via .w-nav) rather than padding .navbar-wraper,
   so .navbar-wraper's own padding-top stays exactly what it's set to above —
   references the same --unlim-nav-pad-top so the two never drift apart. */
.navbar-wraper > .navbar {
  top: max(0px, calc(env(safe-area-inset-top, 0px) - var(--unlim-nav-pad-top)));
}

/* Narrow phones (<=479px) — the navbar's logo + language switcher + Menu
   button + "Start a Project" CTA no longer fit in one row without clipping.
   Drop just the CTA (its own links to the same destination stay reachable
   via the nav menu and elsewhere on every page); scoped to .navbar-actions'
   own direct child so it can't match any other .button on the page. The
   export also carries its own <=479px `.navbar-brand-link{display:none}`
   rule (seemingly meant for a nav variant this site doesn't use) that hides
   the logo outright at this width — restore it back to visible. */
@media screen and (max-width: 479px) {
  .navbar-actions > .button {
    display: none;
  }
  .navbar-brand-link {
    display: flex;
  }

  /* Without the CTA, .navbar-actions' only remaining content (the language
     switcher + Menu button, ~30px tall) is much shorter than the export's
     own padding was sized for, and its .25rem top/bottom/right padding
     (export; only the left was ever generous, at 1.5rem) leaves almost no
     breathing room on the other three sides — together the pill reads
     noticeably shorter and more cramped than the logo circle beside it.
     min-height reuses .navbar-brand-link's own 3.5rem (not a new number) so
     the two read as one row again; vertical padding stays modest (30px
     content + 1rem padding = 46px, under the 56px floor) so min-height, not
     padding, is what actually sets the height — landing on an exact match
     rather than overshooting it. Horizontal padding (1.25rem) is the one
     doing the "not cramped against the edge" work; the existing
     align-items:center (export, .navbar-actions) centers the switcher/Menu
     within the taller box for free. Checked at 320px: logo + switcher +
     Menu + this padding is ~250px of a 320px viewport, no overflow. */
  .navbar-actions {
    min-height: 3.5rem;
    padding: 0.5rem 1.25rem;
  }
}

/* Nav active state is driven by our own js/nav-menu.js (single source of
   truth: pathname + which homepage section is in view), not by Webflow's
   native .w--current scrollspy — that module can mark more than one link
   current at once (e.g. a static match on "/" plus a dynamic scroll match
   on "Services"). Neutralizing .w--current here makes double-highlighting
   visually impossible regardless of what Webflow's own state does. */
.nav-link.w--current {
  background-color: transparent;
  color: var(--_color---white--01);
}
.nav-link.is-current {
  background-color: var(--_color---white--06);
  color: var(--_color---white--01);
}

/* Case card copy (title/description/tags) is filled in per-language by
   js/case-cards-i18n.js; until that finishes for a given card, the static
   English fallback baked into components/case-cards.html would flash
   before the real translation lands. Kept independent of the separate
   i18n-pending gate (js/i18n.js) — case cards resolve on their own timeline,
   sometimes after the rest of the page has already unlocked. The image and
   CTA fields are excluded: case-cards-i18n.js only touches their alt/
   aria-label attributes, not visible content, so hiding them would just add
   a new flicker (the image popping in) that didn't exist before. */
[data-card-field="title"],
[data-card-field="description"],
[data-card-field="tag1_value"],
[data-card-field="tag1_label"],
[data-card-field="tag2_value"],
[data-card-field="tag2_label"],
[data-card-field="tag3_value"],
[data-card-field="tag3_label"] {
  visibility: hidden;
}
[data-case-slug][data-cards-ready] [data-card-field] {
  visibility: visible;
}

/* ══════════════════════════════════════════════════════════════════════════
   Homepage — Hero geometry (matches the Framer reference's model)
   .hero-section-wrap keeps the export's `overflow:hidden` and effectively
   its `height:100vh` (now `min-height:100vh` — see below), and still centers
   its one in-flow child (.padding-global, which flex-grows to fill it).
   .logo-marquee-section is already a SIBLING of
   .hero-section-wrap (not nested inside it), placed right after it closes —
   ordinary document flow below the 100vh box, exactly like Framer. Nothing
   to move there; it was never inside the box being sized here.
   The actual vertical centering of the visible content (eyebrow/h1/sub/CTAs)
   happens one level in, on .container-base.hero (`display:flex;
   justify-content:center; align-items:center`, height driven by `flex:1` —
   see below) — so that's where navbar clearance belongs, not on the outer
   100vh box. Reserving the
   navbar's full height as top padding there, then still centering normally,
   is what actually produces "content centers in the space BELOW the navbar"
   (centerY = 50vh + navbarHeight/2) — no manual half-navbar arithmetic, it
   falls out of the flex geometry on its own.

   .hero-bg is the export's Background component (template/index.html's
   .framer-1df2f6f). In the template this is position:absolute against the
   SECTION that holds both the hero and the logo marquee — i.e. it's exactly
   as tall as hero+marquee combined, extending under the marquee (a normal-
   flow, bottom-docked sibling) rather than stopping where the marquee
   starts. .hero-bg now matches that: its containing block is section.hero
   (below), not .hero-section-wrap, so inset:0% sizes it to the full
   section, not just the hero's own 100vh box. Two earlier passes got this
   wrong in different directions: one shrank .hero-bg to a compact band
   (inset by --unlim-nav-h top+bottom) to dodge the navbar, needing a pile
   of workarounds to compensate; a later one restored it to full-size but
   only as tall as .hero-section-wrap (excluding the marquee), which meant
   .hero-bg-video-gradient-bottom's percentages (14.7748% etc., the
   template's own, meant to divide the FULL hero+marquee height) landed in
   the wrong place, and the marquee's own transparent top had nothing of
   the Background layer left to reveal (patched, at the time, with a flat
   background-color on section.hero — also gone now, see below). Both are
   gone: only the VIDEO inside .hero-bg shrinks and centers per breakpoint
   (matching the template's own .framer-11lkxjs-container), .hero-bg itself
   is simply left at its full, correctly-scoped size.
   ══════════════════════════════════════════════════════════════════════════ */

.hero-section-wrap {
  /* No position here (the export never sets one either) — .hero-bg's
     containing block needs to skip past this element to section.hero
     itself (position:relative, export, below), which is exactly what
     leaving this static achieves. Giving this position:relative was an
     earlier pass's way of deliberately SHRINKING .hero-bg's containing
     block to exclude the marquee (see above) — the opposite of what's
     wanted now. Nothing else here depends on this being positioned: its
     one in-flow child (.padding-global) is plain flow, and section.hero's
     own overflow:hidden (export) still clips .hero-bg exactly the same
     either way, since .hero-bg is sized to section.hero's own box now. */
  height: auto;
  min-height: 100vh;
}

/* .hero-bg (export: position:absolute; inset:0%; width/height:100%) is left
   completely alone geometrically — no override; its containing block is
   now section.hero (see above), so inset:0% covers section.hero's full
   height on its own, no extra sizing rule needed here. background-color
   here is the template's own plane (.framer-1df2f6f,
   background-color:#b7b7b7 — the exact value its own token resolves to,
   not our grey-05/#b8b8b8, which is a different, if near-identical, value):
   it's .hero-bg's OWN base, painted at the very back of .hero-bg's own
   stacking context (position:absolute + z-index:-1 still creates one —
   CSS2.1 Appendix E — so this background sits behind all of .hero-bg's
   children: video, vignette, dark overlay), never in front of any of them.
   .section.hero's own grey-01 (export, no override — see .logo-marquee-
   section below) stays the fallback for whenever .hero-bg somehow doesn't
   reach, same as the template — in practice never, now that the two boxes
   match exactly. */
.hero-bg {
  background-color: #b7b7b7;
}

/* .container-base.hero {height:100%} (export) needs .padding-global's height
   to be "definite" for percentage resolution — but .padding-global's own
   height, though visually 100% of .hero-section-wrap (from its flex:1 in a
   min-height:100vh flex column — the export's own model, untouched), isn't
   treated as definite for a PERCENTAGE-height grandchild by the flex
   spec once the flex container's own size comes from min-height rather than
   an explicit height. Confirmed empirically: .container-base.hero collapsed
   to its own content height (~647px of an available 844px) instead of
   filling it, so .hero-layout rendered top-aligned inside that shrunk box
   instead of vertically centered — the "hero content collects near the
   top" regression. Swapping the percentage-height link for a second level
   of flex-grow (the same mechanism that already reliably fills
   .padding-global itself, one level up) sidesteps the ambiguity entirely
   instead of trying to force the percentage case to resolve. */
.hero-section-wrap > .padding-global {
  display: flex;
  flex-direction: column;
}
.container-base.hero {
  flex: 1;
  /* Exactly the navbar's own reserved height — no added gap on top of it.
     Centering (see above) already gives a visual gap whenever the viewport
     is taller than the content; on short phones that gap shrinks toward 0
     but the eyebrow still clears the navbar exactly, which is the point of
     this padding. */
  padding-top: var(--unlim-nav-h);
}

/* Narrow phones (<=479px) run out of horizontal room for the export's
   3rem/48px hero H1 well before they run out of vertical room — clamp scales
   it down smoothly with viewport width instead of a second flat value, so
   there's no jump at the 479/480 boundary (it resolves to ~3rem right at
   479px, matching the unchanged value at 480px+). Scoped to the hero heading
   only — every other H1 on the site keeps the export's own sizing. */
@media screen and (max-width: 479px) {
  .hero-heading {
    font-size: clamp(2.25rem, 10vw, 3rem);
  }
}

/* Below 1200px — the template's OWN breakpoint for this component (its
   .framer-11lkxjs-container video wrapper is height:100% at >=1200px and
   height:70% below it, identical for tablet AND phone: confirmed in
   template/index.html in both the 800-1199.98px band and the <=799.98px
   band — one shared value, not a further tablet/mobile split). The export
   re-implemented this at a different breakpoint (991px) with a different
   unit (60vh instead of 70%) — matched to the template exactly here
   instead, replacing that export tier. 70% is of .hero-bg's own height,
   which (see above) is section.hero's full height, hero+marquee combined —
   matching the template, where that same 70% is likewise a fraction of the
   full Background plane, not of the hero viewport alone.
   flex:none so the explicit height (not flex-grow) drives the video's
   main-axis size in .hero-bg's column-direction flex layout; .hero-bg's own
   justify-content:center then centers that shorter box vertically — the
   same mechanism the template uses, just on the opposite axis (its
   .framer-1df2f6f is row-direction, so centering there runs on the
   cross-axis via align-items — our Background wrapper is column-direction,
   so centering runs on the main axis via justify-content instead). Width is
   untouched: .hero-bg sets no align-items, so the default `stretch` already
   fills 100% of .hero-bg's width, same as the template's explicit
   width:100%. object-fit:cover on the video element itself (export's
   .w-background-video > video) is untouched, so it still crops to fill
   without distorting. */
@media screen and (max-width: 1199px) {
  .hero-bg {
    justify-content: center;
  }
  .hero-bg-video {
    flex: none;
    height: 70%;
  }
}

/* .hero-bg-video-gradient-top is never used: the template's vignette
   (.framer-4gsoix) is ONE full-size element with a single 3-stop gradient,
   but the export split it into two separate bands (top + bottom, each a
   partial 2-stop fade) instead. Rather than reproduce two partial elements,
   the full 3-stop gradient lives entirely on -bottom below; -top stays
   permanently hidden (the template hides its own equivalent above 1200px
   too, via .hidden-72rtr7 — see the comment below). */
.hero-bg-video-gradient-top {
  display: none;
}

/* .hero-bg-video-gradient-bottom carries the template's full vignette
   (.framer-4gsoix) verbatim — same 3 stops, same percentages, full-size
   (inset:0, matching the template's own height:100%;width:100%;top:0%;
   left:0%). Those percentages are computed against the template's OWN
   full-size Background plane — hero+marquee combined — which is exactly
   what .hero-bg's box is now (see above): they apply as-is, no rescaling.
   An earlier pass had .hero-bg full-size but only as tall as
   .hero-section-wrap (excluding the marquee), and even earlier than that,
   tried these same stops directly against .section.hero's total height —
   both the wrong box for this gradient one way or the other; this is the
   one that actually matches where the template computes them.
   Hidden >=1200px (matching the template's own .hidden-72rtr7, gated at
   exactly `@media (min-width:1200px)` in template/index.html), visible
   below it. Layers above the video and below .hero-bg-overlay purely by DOM
   order: both -bottom and -overlay are position:absolute, z-index:auto
   children of .hero-bg, so among themselves they stack by source order
   (CSS2.1 Appendix E) — -overlay comes after -bottom in the markup. */
.hero-bg-video-gradient-bottom {
  display: none;
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    #d9d9d9 0% 14.7748%,
    transparent 20.9009% 76.7568%,
    #b8b8b8 86.1262% 97.2973%
  );
}
@media screen and (max-width: 1199px) {
  .hero-bg-video-gradient-bottom {
    display: block;
  }
}

/* .hero-bg-overlay (export: position:absolute; inset:0%, untouched) needs
   only its gradient's bottom stop corrected: the export's #0000001a (alpha
   26/255 ~ 0.102) is a rounding drift from the template's own #00000014
   (alpha 20/255 ~ 0.078, confirmed directly against template/index.html's
   own overlay rule); the top stop (#0006, alpha 0.4) already matches both
   and is left as-is. */
.hero-bg-overlay {
  background-image: linear-gradient(#0006, #00000014);
}

/* ══════════════════════════════════════════════════════════════════════════
   Homepage — First Marquee (Service Ticker)
   Graphic weight 700 and a delicate dot separator, centered within the
   untouched 5rem flex gap.
   .logo-marquee-section itself is UNTOUCHED — no override, no background-
   color. It keeps the export's own rule verbatim (position:relative;
   z-index:2; background-image: linear-gradient(180deg, transparent,
   grey-05)), which already matches the original template's own
   .framer-1yln4xl (background: linear-gradient(180deg, #fff0 0%, #b7b7b7
   100%); height:min-content; z-index:2; position:relative — only the
   breakpoint-only horizontal padding differs between them, untouched
   either way).
   That transparent top is meaningful, in the template and now here too:
   .hero-bg's containing block is section.hero (see above), so it extends
   under this section's own box, the same way the template's
   .framer-1df2f6f (Background) extends under .framer-1yln4xl (Logo
   Marquee) — both position:absolute against a shared ancestor section,
   both taller than the in-flow content stacked in front of them. The
   marquee's transparent stop reveals THAT — z-index:2 on
   .logo-marquee-section vs. z-index:-1 on .hero-bg puts the marquee on top
   regardless of DOM order — already faded close to solid by the vignette's
   own bottom stop (86-97%, now correctly a fraction of the SAME hero+
   marquee height this section itself is part of), so nothing further
   needs to patch the seam: no background-color here, and no fallback color
   needed on section.hero either (export's own grey-01 is back, below —
   .hero-bg's opaque plane now reaches everywhere section.hero does, so
   that raw color is never actually visible). */
.logo-marquee-item {
  display: flex;
  align-items: center;
  white-space: nowrap;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.9);
}

.logo-marquee-dot {
  margin-left: 2.5rem;
  margin-right: -2.5rem;
  font-size: 1.4em;
  font-weight: 700;
  line-height: 1;
  color: #ffffff;
  pointer-events: none;
  user-select: none;
}

/* Statics marquee (001 WHO WE ARE) — the shared marquee keyframe (src/pages/
   index.html, .unlim-marquee-ltr/-rtl) runs unconditionally the instant its
   class is present, i.e. from initial page parse — well before the
   preloader even lifts. By the time a user actually scrolls this section
   into view, the phrase is already mid-loop instead of at its start.
   Paused by default and scoped to just this one marquee, not the shared
   class generally — the logo and integrations marquees keep their current
   always-running behavior, unchanged. A ScrollTrigger (see that init
   script) adds .marquee-playing the first time #about-section is actually
   seen. */
.about-bottom .unlim-marquee-ltr {
  animation-play-state: paused;
}
.about-bottom .unlim-marquee-ltr.marquee-playing {
  animation-play-state: running;
}

/* ══════════════════════════════════════════════════════════════════════════
   Homepage — About, top block (no video panel — see the audit; Framer has
   one here, this site deliberately doesn't add it back in this pass)
   Without a video filling the space, .about-top (eyebrow + heading +
   subheading) read as too short once .about-bottom's stat marquee followed
   right after it — the next section felt like it arrived too fast. Give it
   real breathing room, scaled to viewport height rather than a flat desktop
   number, so it doesn't force artificial height onto short or narrow
   viewports where it wouldn't read well. Left untouched below 768px —
   mobile's existing (already-correct) compact composition is unaffected.
   .about-top has no explicit height in the vendor CSS today (`justify-
   content: flex-start`, sized to its content) — pairing the taller box with
   `justify-content: center` is what actually centers the content in the new
   space; below the min-height (content already taller than the clamp), it's
   a no-op, identical to the original flex-start behavior.
   ══════════════════════════════════════════════════════════════════════════ */
@media screen and (min-width: 768px) {
  .about-top {
    justify-content: center;
    min-height: clamp(280px, 40vh, 460px);
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   Secondary pages — shared scaffolding
   Every non-home page (contact, services, case-studies, legal) is built from
   these primitives + the export's design tokens, so they read as one site.
   Composition mirrors the Framer template pages; values come from the tokens.
   ══════════════════════════════════════════════════════════════════════════ */

/* Vertical rhythm that clears the fixed 88px navbar (template top pad ~164px). */
.sec-pad {
  padding-top: calc(var(--unlim-nav-h) + var(--_spacing---medium));
  padding-bottom: var(--_spacing---xmedium);
}
.sec-pad-tight {
  padding-top: var(--_spacing---large);
  padding-bottom: var(--_spacing---large);
}

/* Page head: eyebrow + heading + intro. Left by default, centered on listings. */
.page-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.5rem;
}
.page-head.is-center {
  align-items: center;
  text-align: center;
}
.page-head .heading-style-h1,
.page-head .heading-style-h2 {
  margin: 0;
}
.page-head-intro {
  max-width: 40rem;
  color: var(--_color---black--03);
  font-size: 1.25rem;
  line-height: 1.4;
}
.page-head.is-center .page-head-intro {
  margin-left: auto;
  margin-right: auto;
}

/* Service/case detail hero (svcd-header/svcd-heading-wrap/svcd-h1/svcd-sub).
   Centralized here from identical page-local copies duplicated across all
   12 service/case detail pages (src/pages/services/*, src/pages/case-studies/*)
   and now also used by the case-studies listing hero. */
.svcd-header { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; width: 100%; }
.svcd-heading-wrap { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.svcd-h1 { margin: 0; line-height: 1.1em; text-align: center; }
.svcd-sub { margin: 0; max-width: 40rem; font-size: var(--font-size--body-l); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); text-align: center; color: var(--_color---grey--03); }

/* Service + case-study detail pages (svcd-gradient/svcd-container/svcd-info/
   svcd-solutions/etc.) — centralized from identical page-local <style>
   blocks duplicated across all 12 detail pages (src/pages/services/*,
   src/pages/case-studies/*); each page's <style> block is now removed
   entirely, nothing page-local remains on either. Case study pages have
   two extra .svcd-block sections of their own (Approach, and a closing
   partnership block after Results) — same classes, no extra CSS needed.
   Layout/type scale/spacing replicate the Conicorn case-study template,
   measured from the live Framer page at 1440px and 514px. Colors map 1:1
   onto the site's existing Webflow tokens (same Geist family, grey ramp). */
.svcd-gradient { background: linear-gradient(180deg, #fff 0%, #b7b7b7 100%); }
.svcd-container { display: flex; flex-direction: column; align-items: center; gap: 3rem; }
.svcd-hero-image-wrap { position: relative; width: 100%; border-radius: var(--_radius---radius-7xl); overflow: hidden; }
.svcd-hero-image-wrap img { display: block; width: 100%; height: auto; aspect-ratio: 1040 / 560; object-fit: cover; }
.svcd-image-overlay { position: absolute; inset: 0; z-index: 2; pointer-events: none; border-radius: inherit; box-shadow: inset 0 0 40px rgba(255,255,255,.4), inset 0 0 12px #fff; }
.svcd-main { display: flex; flex-direction: column; align-items: center; width: 100%; }
.svcd-inner { width: 100%; max-width: 40rem; }
/* Info panel: 2-col row (blocks 1+2) then two full-width rows (blocks 3, 4).
   Was a 3-col row (1+2+3) plus a separate block 4 below. Separators are
   generic (border/padding on every child but the last), not per-page
   nth-child rules, so the group count can change without touching CSS. */
.svcd-info { display: flex; flex-direction: column; gap: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--_color---black--06); }
.svcd-info > *:not(:last-child) { padding-bottom: 1.5rem; border-bottom: 1px solid var(--_color---black--06); }
.svcd-info-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.svcd-info-item { display: flex; flex-direction: column; gap: .5rem; }
.svcd-info-name { margin: 0; font-size: var(--font-size--body-m); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); font-weight: var(--font-weight--semibold); color: var(--_color---grey--02); }
.svcd-info-value { margin: 0; font-size: var(--font-size--body-m); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--03); }
.svcd-content { display: flex; flex-direction: column; gap: 4rem; padding-top: 1.5rem; }
.svcd-block { display: flex; flex-direction: column; gap: 1.5rem; }
.svcd-label { margin: 0; font-size: 2rem; line-height: 1.1em; font-weight: var(--font-weight--medium); letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--01); }
.svcd-text p { margin: 0; font-size: var(--font-size--body-m); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--01); }
.svcd-text p + p { margin-top: .5rem; }
.svcd-text strong { font-weight: var(--font-weight--semibold); }
.svcd-list { margin: 1.5rem 0 0; padding: 0; list-style: none; position: relative; }
.svcd-list li { position: relative; padding-inline-start: 2ch; font-size: var(--font-size--body-m); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--01); }
.svcd-list li::before { content: "\2022"; position: absolute; inset-inline-start: 0; }
.svcd-list li + li { margin-top: .5rem; }
.svcd-challenge-image { width: 100%; border-radius: var(--_radius---radius-5xl); overflow: hidden; }
.svcd-challenge-image img { display: block; width: 100%; height: auto; aspect-ratio: 640 / 360; object-fit: cover; }
.svcd-solutions { display: flex; flex-direction: column; gap: 2.5rem; }
.svcd-sol-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem 1.5rem; }
.svcd-sol-card { display: flex; flex-direction: column; gap: 1.25rem; }
.svcd-sol-image { width: 100%; border-radius: 4rem; overflow: hidden; } /* 64px per template; no matching radius token */
.svcd-sol-image img { display: block; width: 100%; height: auto; aspect-ratio: 308 / 240; object-fit: cover; }
.svcd-sol-title { margin: 0; font-size: var(--font-size--body-m); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); font-weight: var(--font-weight--semibold); color: var(--_color---grey--01); }
.svcd-sol-card .svcd-list { margin-top: 0; }
.svcd-stats { display: flex; gap: .5rem; }
.svcd-stat { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: .25rem; padding: 1rem 1.5rem; border-radius: 1.5rem; background: rgba(255,255,255,.4); }
.svcd-stat-num { margin: 0; font-size: 1.5rem; line-height: 1.1em; font-weight: var(--font-weight--medium); letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--01); }
.svcd-stat-label { margin: 0; font-size: var(--font-size--body-s); line-height: 1.35em; letter-spacing: var(--letter-spacing--ls-s); color: var(--_color---grey--01); }
/* Content→footer seam, scoped the same way the homepage's own
   .section.faq + .footer .footer-main override is (see the Footer section
   below): the sibling combinator is a free, HTML-free scope to "whichever
   page's gradient section sits right before the shared footer" — no data
   attribute, no other route touched. Same gradient value each of the 5
   service pages' own page-local copy of this rule already had. */
.svcd-gradient + .footer .footer-main { background-image: linear-gradient(180deg, #b7b7b7 0%, rgba(184, 184, 184, 0) 100%); }
@media (max-width: 767px) {
  .svcd-sub { font-size: var(--font-size--body-m); }
  .svcd-info-row { grid-template-columns: 1fr; }
  .svcd-hero-image-wrap { border-radius: 4rem; }
  .svcd-challenge-image { border-radius: 4rem; }
  .svcd-sol-grid { grid-template-columns: 1fr; }
  .svcd-stats { flex-direction: column; }
}

/* ══════════════════════════════════════════════════════════════════════════
   Contact — dark two-column composition (template/contact)
   LEFT: eyebrow + H1 + subheading + contact info + rotating badge.
   RIGHT: the shared form (components/contact-form.html) in a dark card.
   The section is dark and flows straight into the dark footer bottom bar.
   ══════════════════════════════════════════════════════════════════════════ */

/* Wraps contact-sec + footer-plain.html in one positioning root so a single
   video (.contact-bg, moved here from inside .contact-sec) can cover both
   without a second, independently-cropped copy in the footer — see the
   html[data-i18n-page="contact"] rules below that hide the footer's own
   .footer-bg-video and make .footer transparent on this page only. The dark
   fallback (shown while the video loads) lives here instead of on .contact-sec,
   since it now needs to cover both sections. */
.contact-footer-scene {
  position: relative;
  display: flex;
  flex-direction: column;
  background-color: var(--_color---grey--01);
}
.contact-sec {
  position: relative;
  overflow: hidden;
  color: var(--_color---white--01);
}
/* Animated background that reads as one scene with the dark footer below it.
   Same footer video, darkened by a gradient that deepens toward the bottom so
   there is no hard grey seam where the section meets the footer. */
.contact-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.contact-bg video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Full opacity, same as the footer video — so the contact scene and the footer
     read as ONE continuous animated background (the previous 0.45 + heavy dark
     gradient made the section look like a different, flatter background). */
  opacity: 1;
}
.contact-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Gentle darken at the top (keeps the heading/lead readable over the video)
     that fades to fully transparent at the bottom, so where the section meets
     the footer the treatment is identical (raw video, no overlay) — no seam. */
  background: linear-gradient(180deg,
    rgba(17, 17, 17, 0.55) 0%,
    rgba(17, 17, 17, 0.3) 50%,
    rgba(17, 17, 17, 0) 100%);
}
/* Keep the content above the video layer. */
.contact-sec > .padding-global {
  position: relative;
  z-index: 1;
}
/* Wider content column (template: ~1200px) than the export's default
   .container-medium (65rem/1040px) — scoped to this page only, since
   .container-medium is shared sitewide. */
html[data-i18n-page="contact"] .container-medium {
  max-width: 75rem;
}
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: start;
}
.contact-left {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
}
.contact-lead {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.contact-lead .heading-style-h1 {
  margin: 0;
  color: var(--_color---white--01);
}
.contact-lead-sub {
  max-width: 30rem;
  margin: 0;
  color: var(--_color---white--02);
  font-size: 1.25rem;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.03em;
}
.contact-info {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.contact-info-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.contact-info-label {
  font-size: var(--font-size--body-s);
  letter-spacing: -0.02em;
  color: var(--_color---white--04);
}
.contact-info-value,
.contact-info-value a {
  font-size: var(--font-size--body-m);
  color: var(--_color---white--01);
  text-decoration: none;
}
.contact-info-value a:hover {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
/* Rotating circle logo, embedded in the form's heading area (template/contact)
   rather than floating separately. Reuses the export's curve-text asset. */
.contact-card-head {
  display: flex;
  align-items: center;
  gap: 1.1rem;
}
/* Static circle badge, embedded in the form heading (as in the template).
   No infinite rotation — the template does not spin it, and the design bible
   forbids endless looping animation. */
.contact-card-badge {
  flex: none;
  width: 4rem;
  height: 4rem;
  opacity: 0.9;
}

/* Right column: the form, on a dark card. The fields inside come from
   components/contact-form.html and are already styled dark (.form_input). */
.contact-card {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
  max-width: 680px;
  padding: 3rem 3.75rem 3rem;
  border: 1px solid var(--_color---white--06);
  border-radius: 7.5rem;
  background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(255, 255, 255, 0.2));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  box-shadow:
    inset 0 0 4px rgba(255, 255, 255, 0.8),
    inset 0 0 40px rgba(255, 255, 255, 1);
}
.contact-card-heading {
  margin: 0;
  font-size: var(--font-size--title-l);
  font-weight: var(--font-weight--medium);
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--_color---white--01);
}
/* The form fills the card; the standalone-card min-height isn't wanted here. */
.contact-card .cta-form {
  min-height: 0;
}
/* Form controls — scoped to the contact page's own form instance (the
   shared component also appears in the footer CTA card, which keeps its
   existing look). Border/typography/textarea min-height are the export's
   own — only background/padding/radius move closer to the template here. */
.contact-card .cta-form .form_input {
  background-color: rgba(0, 0, 0, 0.2);
  padding: 0.75rem;
  border-radius: 1rem;
}
.contact-card .cta-form .form-button {
  margin-top: 2rem;
}

/* Sticky footer, scoped to the contact page (data-i18n-page keeps the shared
   .page-wrapper untouched elsewhere): the wrapper is a full-height flex column,
   the contact section stretches to fill, the footer keeps its natural height —
   so a short page never inflates the footer. */
html[data-i18n-page="contact"] .page-wrapper {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
html[data-i18n-page="contact"] .main-wrapper {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
}
html[data-i18n-page="contact"] .contact-footer-scene {
  flex: 1 0 auto;
}
html[data-i18n-page="contact"] .contact-sec {
  flex: 1 0 auto;
}

/* .contact-footer-scene's own video (.contact-bg) now covers the footer too,
   so the footer's normal per-page video (components/footer-bottom.html,
   unchanged and still used as-is by every other route) would be a second,
   independently-cropped copy playing on top of it — hide it, and let .footer's
   own background go transparent so the shared scene video shows through. */
html[data-i18n-page="contact"] .footer {
  background-color: transparent;
}
html[data-i18n-page="contact"] .footer-bg-video {
  display: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   Legal pages — Privacy / Terms (template/privacy-policy, template/terms)
   Header (H1 72px + intro 20px) then numbered sections (H3 32px) in a single
   readable centered column, matching the template's legal layout.
   ══════════════════════════════════════════════════════════════════════════ */

/* Template legal pages sit on a soft white→light-grey wash, not flat white.
   A fixed, viewport-height gradient reproduces that gentle top-to-bottom fade;
   the dark footer paints over its own area, so only the content shows the wash. */
html[data-i18n-page="privacy"] body,
html[data-i18n-page="terms"] body {
  background-color: #ffffff;
  background-image: linear-gradient(180deg, #ffffff 0%, var(--_color---grey--08) 100%);
  background-attachment: fixed;
  background-repeat: no-repeat;
}

/* The one contact form's consent checkbox (components/contact-form.html), sat
   right before the submit button. Same dark-card context in both places the
   form appears (footer CTA + standalone contact page), so white-based colors
   are safe unconditionally. */
.form-consent {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  cursor: pointer;
  margin-top: 0.25rem;
}
/* A real <input type="checkbox"> throughout — appearance:none strips only
   the browser's own box/tick rendering, everything else (state, keyboard
   toggling, :checked, form submission, required validation) stays native.
   Matches .form_input's own visual language one section up: same border
   (white--06), same dark fill (black--06), same focus treatment (border
   lightens to white--03) — just a smaller, squarer radius than the pill-ish
   1.125rem text inputs use, sized for an 18px box instead of a 48px field.
   The checkmark reuses the exact path every other check-icon on the site
   draws (e.g. .capabilities-check-icon), so it reads as the same mark, not
   a new one — traced from that shared 16x16 viewBox path, restroked here at
   the checkbox's own 18px box. */
.form-consent-input {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.15rem;
  border: 1px solid var(--_color---white--06);
  border-radius: 0.375rem;
  background-color: var(--_color---black--06);
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}
.form-consent-input:hover {
  border-color: var(--_color---white--03);
}
.form-consent-input:focus-visible {
  outline: 2px solid var(--_color---white--03);
  outline-offset: 2px;
}
.form-consent-input:checked {
  background-color: var(--_color---white--01);
  border-color: var(--_color---white--01);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M3.33334 8.00008L6.66667 11.3334L13.3333 4.66675' stroke='%231a1a1a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0.7rem;
}
.form-consent-label {
  font-size: var(--font-size--body-s);
  font-weight: 400;
  line-height: 1.45;
  color: var(--_color---white--03);
}
.form-consent-label a {
  color: var(--_color---white--01);
  text-decoration: underline;
}
.form-consent-label a:hover {
  color: var(--_color---white--02);
}

.form-success-message-inner {
  row-gap: 0.75rem;
}

/* Cookie consent banner — fixed bottom card, sitewide (included once in
   components/scripts.html so it renders on every page). Kept intentionally
   separate from GA4/Yandex Metrika loading in components/head.html: this is
   UI + a stored preference only, not a script-loading gate. */
.cookie-consent {
  position: fixed;
  left: 1rem;
  right: 1rem;
  /* env() is 0 without viewport-fit=cover (homepage only), so this stays
     exactly 1rem on every other page. */
  bottom: calc(1rem + env(safe-area-inset-bottom));
  z-index: 999;
  /* Own compositing layer — see the .navbar-wraper comment above; same iOS
     Safari fixed-element flicker, same fix. */
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
  will-change: transform;
  isolation: isolate;
  display: flex;
  justify-content: center;
  pointer-events: none;
}
.cookie-consent[hidden] {
  display: none;
}
.cookie-consent-card {
  pointer-events: auto;
  width: 100%;
  max-width: 40rem;
  border: 1px solid var(--_color---white--06);
  border-radius: var(--_radius---radius-2xl);
  background-color: var(--_color---black--02);
  background-image: linear-gradient(#0006, #ffffff14);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
  padding: 1.5rem;
}
.cookie-consent-text {
  margin: 0 0 1rem;
  font-size: var(--font-size--body-s);
  line-height: 1.5;
  color: var(--_color---white--02);
}
.cookie-consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.cookie-btn {
  border-radius: var(--_radius---round);
  padding: 0.6rem 1.25rem;
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
  cursor: pointer;
  transition: all 0.2s;
  border: 1px solid var(--_color---white--06);
  background: transparent;
  color: var(--_color---white--01);
}
.cookie-btn:hover {
  border-color: var(--_color---white--03);
}
.cookie-btn-solid {
  border-color: transparent;
  background-color: var(--_color---white--01);
  color: var(--_color---black--01);
}
.cookie-btn-solid:hover {
  background-color: var(--_color---white--02);
}

/* Narrow phones (<=479px) — the 3 action buttons wrap to their own lines at
   the export's default padding/gap/font-size, since RU's intrinsic widths
   ("Настройки" / "Отклонить" / "Принять") don't fit one row at 320px.
   Force them into 3 equal columns and trim padding/gap/font-size just enough
   to fit RU (the longest of the 3 languages) in one row with room to spare.
   `.cookie-consent-card >` scopes this to the main 3-button row only — the
   nested "Save preferences" row inside .cookie-consent-settings is a
   grandchild, not a direct child, so it keeps its own single full-width
   button untouched. white-space stays the default `normal` (no nowrap
   anywhere in this component), so a label that's still tight after this
   wraps onto a second line instead of clipping or ellipsizing. */
@media screen and (max-width: 479px) {
  .cookie-consent-card {
    padding: 1.25rem;
  }
  .cookie-consent-card > .cookie-consent-actions {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.4rem;
  }
  .cookie-consent-card > .cookie-consent-actions .cookie-btn {
    padding: 0.6rem 0.4rem;
    font-size: var(--font-size--body-xs);
    text-align: center;
  }
}

.cookie-consent-settings {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--_color---white--06);
}
.cookie-consent-settings[hidden] {
  display: none;
}
.cookie-category {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  cursor: pointer;
}
.cookie-category-title {
  display: block;
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
  color: var(--_color---white--01);
}
.cookie-category-desc {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--_color---white--03);
}
.cookie-category input[type="checkbox"] {
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.15rem;
  accent-color: var(--_color---white--01);
  cursor: pointer;
}
.cookie-category input[type="checkbox"]:disabled {
  cursor: default;
  opacity: 0.6;
}

/* The template's legal pages sit in a single CENTERED reading column (~40rem),
   not left-aligned against the container edge. Center the head and body, keep
   the text itself left-aligned inside the column. */
.legal-head {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  max-width: 42rem;
  margin-left: auto;
  margin-right: auto;
}
.legal-head .heading-style-h2 {
  margin: 0;
}
.legal-intro {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  max-width: 42rem;
  margin: 0;
  color: var(--_color---black--03);
  font-size: 1.25rem;
  line-height: 1.45;
}
.legal-intro p {
  margin: 0;
}
/* "Last updated …" line under the heading, as in the template legal pages. */
.legal-updated {
  font-size: var(--font-size--body-s);
  color: var(--_color---black--04);
}
.legal-body {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
  max-width: 42rem;
  margin-top: var(--_spacing---small);
  margin-left: auto;
  margin-right: auto;
}
.legal-block {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.legal-block h2 {
  margin: 0;
  font-size: var(--font-size--h6);
  font-weight: var(--font-weight--medium);
  letter-spacing: -0.03em;
  line-height: 1.15;
  color: var(--_color---grey--01);
}
/* Auto-number the sections from a CSS counter so the "1." … "5." prefixes
   stay correct in any language without living in the translatable text. */
.legal-body.is-numbered {
  counter-reset: legal;
}
.legal-body.is-numbered .legal-block h2::before {
  counter-increment: legal;
  content: counter(legal) ". ";
}
.legal-block p {
  margin: 0;
  color: var(--_color---black--02);
  line-height: 1.6;
}
.legal-block ul {
  margin: 0;
  padding-left: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  color: var(--_color---black--02);
}
.legal-note {
  font-size: var(--font-size--body-s);
  color: var(--_color---black--04);
}
/* A numbered section's body (injected via i18n html): stacks its paragraphs,
   sub-headings and lists with the same rhythm. .legal-block p / ul above still
   apply (they match descendants), so only the wrapper + sub-heading are new. */
.legal-block-body {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.legal-sub {
  margin: 0.35rem 0 0;
  font-size: 1.125rem;
  font-weight: var(--font-weight--medium);
  letter-spacing: -0.01em;
  line-height: 1.2;
  color: var(--_color---grey--01);
}

/* ══════════════════════════════════════════════════════════════════════════
   Services — landing page (no template page exists; synthesized in Conicorn
   style from the design system). Hero + service cards + process + mid CTA.
   Service copy reuses the existing capabilities.* / process.* i18n keys so it
   never drifts from the homepage.
   ══════════════════════════════════════════════════════════════════════════ */

/* Stacks major page blocks with a generous, consistent rhythm. */
.page-stack {
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---large);
}

/* Service cards grid. The card with a detail page is an <a class="is-link">;
   the rest are plain <div>s, so nothing links to a dead end. */
.svc-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}
.svc-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
  min-height: 100%;
  padding: 2.5rem;
  border-radius: var(--_radius---radius-l);
  background-color: var(--_color---grey--08);
  text-decoration: none;
  color: inherit;
}
.svc-card.is-link {
  transition: background-color 0.25s ease, transform 0.25s ease;
}
.svc-card.is-link:hover {
  background-color: var(--_color---grey--07);
  transform: translateY(-2px);
}
.svc-card-num {
  font-size: var(--font-size--body-s);
  color: var(--_color---black--04);
  letter-spacing: 0.02em;
}
.svc-card-title {
  margin: 0;
  font-size: var(--font-size--title-m);
  font-weight: var(--font-weight--medium);
  line-height: 1.15;
}
.svc-card-desc {
  margin: 0;
  color: var(--_color---black--02);
  line-height: 1.5;
}
.svc-card-more {
  margin-top: auto;
  padding-top: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
}
.svc-card-more svg {
  width: 0.9rem;
  height: 0.9rem;
}

/* Service areas — the rich landing-page grid (10 areas, each with a business
   outcome, capability list and a representative stat). Two columns on desktop.
   Only the CRM card is an <a class="is-link">; the rest are plain <div>s, so no
   card links to a dead end. */
.svc-areas {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem;
}
.svc-area {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-height: 100%;
  padding: 2.5rem;
  border: 1px solid var(--_color---black--06);
  border-radius: var(--_radius---radius-l);
  background-color: var(--_color---grey--08);
  color: inherit;
  text-decoration: none;
}
.svc-area.is-link {
  transition: background-color 0.25s ease, transform 0.25s ease, border-color 0.25s ease;
}
.svc-area.is-link:hover {
  background-color: var(--_color---grey--07);
  border-color: var(--_color---black--05);
  transform: translateY(-2px);
}
.svc-area-top {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
}
.svc-area-num {
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
  color: var(--_color---black--04);
  letter-spacing: 0.02em;
}
.svc-area-title {
  margin: 0;
  font-size: var(--font-size--title-m);
  font-weight: var(--font-weight--medium);
  line-height: 1.15;
}
.svc-area-outcome {
  margin: 0;
  color: var(--_color---black--02);
  font-size: 1.0625rem;
  line-height: 1.5;
}
.svc-area-caps {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.svc-area-caps li {
  position: relative;
  padding-left: 1.15rem;
  color: var(--_color---black--03);
  font-size: var(--font-size--body-s);
  line-height: 1.45;
}
.svc-area-caps li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: var(--_radius---round);
  background-color: var(--_color---grey--01);
}
.svc-area-foot {
  margin-top: auto;
  padding-top: 1.25rem;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  border-top: 1px solid var(--_color---black--06);
}
.svc-area-stat {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.svc-area-stat-num {
  font-size: var(--font-size--title-l);
  font-weight: var(--font-weight--medium);
  line-height: 1;
  letter-spacing: -0.02em;
}
.svc-area-stat-label {
  font-size: var(--font-size--body-s);
  color: var(--_color---black--04);
  line-height: 1.3;
}
.svc-area-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex: none;
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
}
.svc-area-link svg {
  width: 0.9rem;
  height: 0.9rem;
}

/* Process: numbered steps, divider rules — echoes the homepage process. */
.svc-process {
  display: flex;
  flex-direction: column;
}
.svc-step {
  display: grid;
  grid-template-columns: 3rem 1fr;
  gap: 1.5rem;
  align-items: start;
  padding: 1.75rem 0;
  border-top: 1px solid var(--_color---black--06);
}
.svc-step:last-child {
  border-bottom: 1px solid var(--_color---black--06);
}
.svc-step-num {
  font-size: var(--font-size--title-l);
  font-weight: var(--font-weight--medium);
  line-height: 1;
  color: var(--_color---black--04);
}
.svc-step-body {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.svc-step-title {
  font-size: var(--font-size--title-m);
  font-weight: var(--font-weight--medium);
}
.svc-step-desc {
  color: var(--_color---black--02);
  line-height: 1.5;
}

/* Mid-page CTA band — dark, echoes the site's dark surfaces. */
.svc-cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 1.5rem;
  padding: var(--_spacing---medium) var(--_spacing---small);
  border-radius: var(--_radius---radius-2xl);
  background-color: var(--_color---grey--01);
  color: var(--_color---white--01);
}
.svc-cta h2 {
  margin: 0;
  max-width: 30rem;
}
.svc-cta p {
  margin: 0;
  max-width: 34rem;
  color: var(--_color---white--02);
  font-size: 1.125rem;
}
/* Reused pill button on light or dark surfaces (matches .cs-viewall). */
.unlim-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 1.6rem;
  border-radius: var(--_radius---round);
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
  text-decoration: none;
  transition: transform 0.25s ease, background-color 0.25s ease;
}
.unlim-btn.is-light {
  background-color: var(--_color---white--01);
  color: var(--_color---grey--01);
}
.unlim-btn.is-dark {
  background-color: var(--_color---grey--01);
  color: var(--_color---white--01);
}

/* ══════════════════════════════════════════════════════════════════════════
   CRM service page — editorial / case-study layout
   Replaces the homepage-style .value-card icon grids and the process zigzag with
   editorial numbered rows, so the page reads like an internal case-study article
   rather than a second homepage. Reuses the existing pages.crm.* i18n keys.
   Light surface (dark hero above), so text is dark-on-light like the other
   secondary-page content.
   ══════════════════════════════════════════════════════════════════════════ */
.crm-editorial {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
}
.crm-editorial-head {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 46rem;
}
.crm-editorial-head .heading-style-h2 {
  margin: 0;
}
.crm-editorial-head .subheading-text {
  margin: 0;
}

/* Shared numbered-row scaffold for the problem list and the "what we build"
   feature list — a large index number beside an editorial title + paragraph,
   separated by hairline rules (the case-study rhythm, not a card grid). */
.crm-problems {
  list-style: none;
  margin: 0;
  padding: 0;
}
.crm-problems,
.crm-build {
  display: flex;
  flex-direction: column;
}
.crm-problem,
.crm-build-row {
  display: grid;
  grid-template-columns: 5rem 1fr;
  gap: 1.5rem;
  align-items: start;
  padding: 1.875rem 0;
  border-top: 1px solid var(--_color---black--06);
}
.crm-problems .crm-problem:last-child,
.crm-build .crm-build-row:last-child {
  border-bottom: 1px solid var(--_color---black--06);
}
.crm-problem-num,
.crm-build-num {
  font-size: var(--font-size--title-l);
  font-weight: var(--font-weight--medium);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--_color---black--04);
}
.crm-problem-body,
.crm-build-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.crm-problem-title,
.crm-build-title {
  margin: 0;
  font-size: var(--font-size--title-m);
  font-weight: var(--font-weight--medium);
  line-height: 1.15;
}
.crm-problem-desc,
.crm-build-desc {
  margin: 0;
  max-width: 44rem;
  color: var(--_color---black--02);
  font-size: 1.0625rem;
  line-height: 1.55;
}

/* Related services — a compact link list, not a third card grid. */
.crm-related {
  display: flex;
  flex-direction: column;
}
.crm-related-item {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.4rem 1.5rem;
  align-items: baseline;
  padding: 1.5rem 0;
  border-top: 1px solid var(--_color---black--06);
  text-decoration: none;
  color: inherit;
  transition: padding-left 0.2s ease, background-color 0.2s ease;
}
.crm-related-item:last-child {
  border-bottom: 1px solid var(--_color---black--06);
}
.crm-related-item:hover {
  padding-left: 0.6rem;
}
.crm-related-title {
  grid-column: 1;
  margin: 0;
  font-size: var(--font-size--title-m);
  font-weight: var(--font-weight--medium);
  line-height: 1.15;
}
.crm-related-desc {
  grid-column: 1;
  margin: 0;
  max-width: 38rem;
  color: var(--_color---black--02);
  line-height: 1.5;
}
.crm-related-arrow {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  font-size: 1.25rem;
  color: var(--_color---black--04);
}

@media screen and (max-width: 767px) {
  .crm-problem,
  .crm-build-row {
    grid-template-columns: 3.25rem 1fr;
    gap: 1rem;
  }
  .crm-problem-num,
  .crm-build-num {
    font-size: var(--font-size--title-m);
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   Case studies — listing page
   Reuses components/case-cards.html, so the cards never drift from the
   homepage. Only the layout differs: a plain stack instead of a coverflow.
   ══════════════════════════════════════════════════════════════════════════ */

.cs-list {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  margin-top: var(--_spacing---medium);
}

/* Undo the coverflow positioning from .cs-item — there is no carousel here. */
.cs-list .cs-item {
  position: static;
  width: auto;
  height: auto;
  margin-left: 0;
  transform: none;
  opacity: 1;
  cursor: default;
  transition: none;
}

/* ── Read-case-study CTA (components/case-cards.html) ──────────────────────
   Lives in each card's content area. On the homepage coverflow the export's
   rule `.cs-item[data-state='prev'|'next'|'hidden'] :is(a,button){pointer-
   events:none}` already makes it fire only on the ACTIVE card — a click on a
   side card falls through to the carousel and just activates it. On the
   listing page (.cs-list) and the mobile rail every card's button is live. */
.casestudy-card-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  align-self: flex-start;
  margin-top: 1.1rem;
  padding: 0.55rem 1.15rem;
  border-radius: var(--_radius---round);
  background-color: var(--_color---grey--01);
  color: var(--_color---white--01);
  font-size: var(--font-size--body-s);
  font-weight: var(--font-weight--medium);
  line-height: 1;
  text-decoration: none;
  transition: transform 0.2s ease, background-color 0.2s ease;
}
.casestudy-card-cta:hover {
  transform: translateY(-1px);
  background-color: var(--_color---grey--03);
}
.casestudy-card-cta-icon {
  display: inline-flex;
  width: 0.85rem;
  height: 0.85rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   Case studies — coverflow
   Replaces the old Webflow w-slider on the homepage. Structure follows the
   Conicorn template's homepage block (a centred active card with the previous
   and next cards peeking behind it), but the card itself reuses the existing
   .casestudy-card component, so it inherits the site's look and its existing
   991 / 767 / 479 breakpoints for free.
   Driven by js/case-studies-carousel.js. No jQuery, no Webflow runtime.
   ══════════════════════════════════════════════════════════════════════════ */

.cs-coverflow {
  --cs-card-w: min(58rem, 76vw);
  --cs-card-h: 29rem;
  --cs-peek: 64%;          /* side-card offset, relative to card width */
  --cs-side-scale: 0.84;
  --cs-side-opacity: 0.45;

  position: relative;
  z-index: 1;
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

.cs-stage {
  position: relative;
  height: var(--cs-card-h);
}

/* Drag cursor + touch-action apply only to the >=992px coverflow — below
   that the same element becomes the mobile scroll-snap rail (see the
   max-width:991px block further down), which needs the browser's default
   touch-action (horizontal pan) for native swipe, not this pan-y lock. */
@media screen and (min-width: 992px) {
  .cs-stage {
    cursor: grab;
    touch-action: pan-y;
  }

  /* js/case-studies-carousel.js adds this only once a pointerdown has moved
     past the drag threshold — a plain click/tap never sees it. */
  .cs-stage.cs-dragging {
    cursor: grabbing;
  }
}

.cs-item {
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--cs-card-w);
  height: 100%;
  margin-left: calc(var(--cs-card-w) / -2);
  transition: transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1),
              opacity 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: transform, opacity;
}

/* The card fills its slot so every slide is the same height. Desktop only in
   practice — .cs-item's slot is the fixed-height absolute stage there, but
   on the <=991px rail (see below) .cs-item's own height is its natural
   content height, so 100% is a no-op there rather than forcing a shared
   tallest-card height. */
.cs-item > .casestudy-card {
  height: 100%;
}

.cs-item[data-state='active'] {
  z-index: 3;
  opacity: 1;
  transform: translateX(0) scale(1);
}

.cs-item[data-state='prev'],
.cs-item[data-state='next'] {
  z-index: 2;
  opacity: var(--cs-side-opacity);
  cursor: pointer;
}

.cs-item[data-state='prev'] {
  transform: translateX(calc(var(--cs-peek) * -1)) scale(var(--cs-side-scale));
}

.cs-item[data-state='next'] {
  transform: translateX(var(--cs-peek)) scale(var(--cs-side-scale));
}

.cs-item[data-state='prev']:hover,
.cs-item[data-state='next']:hover {
  opacity: 0.7;
}

/* Cards further than one step away: parked behind the neighbours. */
.cs-item[data-state='hidden'] {
  z-index: 1;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.75);
}

/* Inert slides must not be reachable by keyboard or screen readers. */
.cs-item[data-state='prev'] :is(a, button),
.cs-item[data-state='next'] :is(a, button),
.cs-item[data-state='hidden'] :is(a, button) {
  pointer-events: none;
}

/* ── Controls ─────────────────────────────────────────────────────────────
   The buttons keep the export's .casestudy-nav-btn look (3rem round black
   button with the inset glow, and .right's 180deg flip). That rule never
   declares `position`, so its `inset` offsets — meant for the old slider —
   are inert and it drops straight into a flex row. .cs-nav-btn only resets
   the <button> chrome the old markup didn't need (it used divs). */

.cs-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  margin-top: 2rem;
}

.cs-nav-btn {
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--_color---white--01);
  flex: none;
}

.cs-nav-btn[disabled] {
  opacity: 0.35;
  cursor: default;
}

.cs-dots {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.cs-dot {
  width: 0.5rem;
  height: 0.5rem;
  padding: 0;
  border: 0;
  border-radius: var(--_radius---round);
  background-color: var(--_color---black--05);
  cursor: pointer;
  transition: width 0.3s ease, background-color 0.3s ease;
}

.cs-dot[aria-current='true'] {
  width: 1.5rem;
  background-color: var(--_color---grey--01);
}

/* ── View all ─────────────────────────────────────────────────────────── */

.cs-viewall-wrap {
  display: flex;
  justify-content: center;
  margin-top: 2.5rem;
}

/* ── Tablet / mobile ──────────────────────────────────────────────────────
   Below the export's 991px breakpoint .casestudy-card becomes a column, so a
   fixed-height absolute stage no longer fits. Swap the coverflow for a native
   scroll-snap rail: no transforms, no JS layout, real momentum scrolling. */

@media screen and (max-width: 991px) {
  .cs-stage {
    position: static;
    height: auto;
    display: flex;
    /* Default align-items:stretch would size every .cs-item to match the
       tallest card in the rail (a flex row's implicit cross-axis stretch —
       .cs-item's own height:auto here doesn't opt out of it), and
       .cs-item > .casestudy-card's height:100% then fills that stretched
       box, leaving empty space below shorter cards' content. flex-start
       lets each item (and the card filling it) size to its own content. */
    align-items: flex-start;
    gap: 1rem;
    overflow-x: auto;
    overflow-y: hidden;
    /* mandatory, not proximity — proximity leaves the browser free to decide
       a given rest position isn't "close enough" to bother snapping, which
       in practice was landing a normal swipe between two cards instead of
       on one (the reported bug: "останавливается между карточками").
       mandatory always resolves to the nearest .cs-item. The PREVIOUS
       reason for proximity — mandatory's axis lock was observed grabbing a
       vertical page swipe that started over the rail on iPhone — is
       addressed directly instead, with touch-action:pan-x: it tells the
       browser up front that this element only claims horizontal panning,
       so a vertical gesture starting here has nothing local to capture it
       and goes straight to the page, independent of scroll-snap-type. */
    scroll-snap-type: x mandatory;
    touch-action: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* bleed to the screen edges so neighbours hint at the sides */
    margin-left: calc(var(--_spacing---small) * -1);
    margin-right: calc(var(--_spacing---small) * -1);
    padding: 0 var(--_spacing---small);
  }

  .cs-stage::-webkit-scrollbar {
    display: none;
  }

  .cs-item {
    position: static;
    flex: 0 0 min(34rem, 86vw);
    /* A flex item defaults to min-width:auto, so its min-content size wins over
       the basis — cards with longer titles rendered wider than the rest
       (341/323/446px instead of a uniform 323px). min-width:0 lets the basis
       hold and keeps every card the same width in the rail. */
    min-width: 0;
    width: auto;
    height: auto;
    margin-left: 0;
    scroll-snap-align: center;
    /* Belt-and-suspenders alongside mandatory above: without this, a fast
       fling can still blow past multiple cards before momentum decays far
       enough to register a snap point at all — scroll-snap-stop:always
       forces the browser to stop at the very first snap point it reaches,
       so one swipe never lands more than one card away. */
    scroll-snap-stop: always;
    /* neutralise the coverflow transforms the JS sets for desktop */
    transform: none !important;
    opacity: 1 !important;
    cursor: default;
  }

  .cs-item[data-state='hidden'] {
    pointer-events: auto;
  }

  .cs-item :is(a, button) {
    pointer-events: auto;
  }

  /* Descriptions are unbounded (the export sets no clamp on
     .casestudy-card-desc, and an earlier version's line-clamp utility class
     was dropped when case-cards.html moved to per-case JSON copy — see
     js/case-cards-i18n.js). On desktop that's harmless: the coverflow's
     .casestudy-card gets a fixed height (--cs-card-h) regardless of content.
     But .cs-item is height:auto on this <=991px rail (see above and the
     .cs-stage align-items fix below it), so a longer EN/RO/RU description
     directly changes the card's rendered height — cards in the same rail
     end up visibly uneven, and even a single card's height becomes
     unpredictable. Clamp restores a fixed preview length with the standard
     ellipsis; the full description is always one tap away on the case's own
     detail page. */
  .casestudy-card-desc {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    -webkit-line-clamp: 3;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cs-item {
    transition: none;
  }
  .cs-stage {
    scroll-behavior: auto;
  }
}

/* Desktop case-study KPIs — the export declares three grid tracks but leaves
   the container as flex, so each item's min-content width can override its
   flex share (and can even wrap the third KPI onto a new row). Use the grid
   tracks that were already intended and center each KPI within its third. */
@media screen and (min-width: 992px) {
  .casestudy-card-bottom {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    display: grid;
  }

  .casestudy-card-number-wrap {
    min-width: 0;
    align-items: stretch;
    white-space: normal;
    overflow-wrap: break-word;
    text-align: center;
  }
}

/* Case study cards — show all 3 KPIs and keep them an even row, on every
   width that uses the mobile scroll-snap rail (<=991px — see the .cs-stage
   breakpoint above). The export's .casestudy-card-number-wrap is flex:1 with
   white-space:nowrap: an unbreakable long label (RO/RU run noticeably longer
   than EN — "Vizibilitate în Pipeline", "Профинансированных проектов") forces
   its own column wider than the other two instead of wrapping, so the row
   reads as 3 uneven columns rather than 3 even ones. Switch to a 3-equal-
   column grid and let a long label wrap (with a 2-line clamp further down)
   instead of stretching its column. Font sizing is untouched here — the
   export already scales .casestudy-card-number/-title per breakpoint
   (991/767px), and that's independent of this column-width fix. */
@media screen and (max-width: 991px) {
  .casestudy-card-bottom {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-column-gap: 0.625rem;
  }
  .casestudy-card-number-wrap {
    /* Base rule is align-items:flex-start (shrink-to-fit), which sizes the
       value/label to their own content width instead of the grid column —
       a long single word then overflows past the column into the next one
       instead of wrapping. Stretch makes them fill (and wrap within) the
       column like the rest of this override expects; text-align:center
       then centers both lines within that now full-width column instead of
       hugging its left edge. */
    align-items: stretch;
    white-space: normal;
    overflow-wrap: break-word;
    text-align: center;
  }
  .casestudy-card-number-title {
    /* Labels range from one short word to long RO/RU phrases; left
       unbounded, the longer ones wrap to 3-4 lines and grow the whole card
       unpredictably, and the three KPI columns end up visibly uneven. Cap at
       2 lines with the standard ellipsis fallback, and reserve that same
       2-line height (min-height, not a fixed height — an unusually short
       label still sits at its natural 1-line height, just padded down to
       match) so all three columns read as one even row regardless of label
       length. line-height is pinned to a known 1.3 rather than left to
       inherit (it's 1.571 by default at some of these widths) so the
       min-height formula below reserves exactly 2 lines, not an
       approximation that under- or overshoots depending on tier. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    -webkit-line-clamp: 2;
    line-height: 1.3;
    min-height: calc(1.3em * 2);
  }
}

/* Hero buttons — the export's own mobile override for .button-wrap
   (assets/conicorn.shared*.css) switches it to flex-flow:column AND
   align-items:stretch, and .button itself carries no width/align-self of
   its own to resist that — so each button stretched to the full column
   width instead of sizing to its own content, visibly wider than the
   template. center instead of stretch is the only change: buttons size to
   content and centre in the column, same stacking, same height/padding/
   gradient/text/arrow/GSAP entrance (all untouched, all live on .button/
   .button-inner, not .button-wrap). Scoped to the hero's own .button-wrap
   (the only one on this page) rather than the shared class globally, and
   only at this existing 479px cutoff — matches the export's own row layout
   to 991px/767px, so desktop/tablet keep the export's stretch-free default. */
@media screen and (max-width: 479px) {
  .hero .button-wrap {
    align-items: center;
  }

  .casestudy-card-number {
    /* title-s (1.25rem), not body-s (.875rem) — the value read too small
       relative to the template at this width; title-s is a step down from
       the base (desktop) title-l (2rem) rather than the full jump to body
       sizing, so it still fits 3-across without wrapping typical values. */
    font-size: var(--font-size--title-s);
    line-height: 1.15;
  }
  .casestudy-card-number-title {
    /* body-s, not body-xs — a matching small step up for the label. */
    font-size: var(--font-size--body-s);
    line-height: 1.3;
  }
}

/* Project card media — the export's <=767px rule keeps the same 3:2 landscape
   ratio as desktop/tablet (.casestudy-card-image-wrap{aspect-ratio:3/2}, set
   at its own <=991px breakpoint), but its <=479px rule swaps to a square
   (aspect-ratio:1) — noticeably taller than 3:2 at the same width, so the
   image area jumps abruptly right at that boundary and reads as too tall/
   dominant relative to the card's text below it. Keep 3:2 all the way down;
   object-fit:cover on the image itself (export's .casestudy-card-image) is
   untouched, so it still crops rather than distorting. */
@media screen and (max-width: 479px) {
  .casestudy-card-image-wrap {
    aspect-ratio: 3 / 2;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   Secondary pages — responsive
   Heading sizes scale via the export's --font-size--h1/h2 media tokens, so
   only layout (columns, padding) is adjusted here.
   ══════════════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 991px) {
  .contact-grid {
    grid-template-columns: 1fr;
    gap: 3rem;
  }
  .contact-decor {
    display: none;
  }
  .page-stack {
    gap: var(--_spacing---medium);
  }
}

@media screen and (max-width: 767px) {
  .svc-grid,
  .svc-areas {
    grid-template-columns: 1fr;
  }
  .svc-card,
  .svc-area {
    padding: 2rem;
  }
  .contact-card {
    padding: 2rem 1.5rem 2.5rem;
    border-radius: 3rem;
  }
  .contact-card-head {
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    text-align: center;
  }
  .svc-cta {
    padding: var(--_spacing---small) 1.5rem;
  }
  .legal-body {
    gap: 2rem;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   CTA button hover — GSAP SplitText letter flip (js/cta-buttons.js)
   Each .js-cta-split char is masked (overflow:hidden) around the original
   letter only; the clone is absolutely positioned so it never adds to the
   mask's box — the button keeps its normal height in every browser
   (including Safari, where an unconstrained split wrapper is a common
   source of line-box inflation), with no explicit height needed.
   ══════════════════════════════════════════════════════════════════════════ */
.js-cta-split {
  height: 22px;
  overflow: hidden;
  white-space: nowrap;
  line-height: 22px;
}
.cta-char-mask {
  display: inline-block;
  position: relative;
  overflow: hidden;
  vertical-align: top;
  line-height: 22px;
}
.cta-char {
  display: inline-block;
  line-height: 22px;
}
.cta-char-clone {
  position: absolute;
  top: 0;
  left: 0;
  text-shadow: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   Footer — dark base + clickable contact line (single shared component:
   components/footer-bottom.html + footer.html / footer-plain.html).
   ══════════════════════════════════════════════════════════════════════════ */

/* The export ships .footer on a light grey--05 base that the dark footer video
   is meant to cover; the footer's own text is white (--white--01). When the
   video is slow, blocked or absent, the light base shows through and the white
   text drops to ~1.5:1 contrast. Pin a dark base (as the template renders it)
   so the footer is always dark and readable, and the dark contact scene flows
   straight into it with no hard grey seam. Applies on every route → identical. */
.footer {
  background-color: var(--_color---grey--01);
}
/* The previous iteration also filled .footer-main with a solid dark gradient
   (grey--01 → grey--01) at z-index 2, which painted OVER the footer video
   (z-index -1) and hid the animation — the "background replaced by a flat fill"
   report. .footer-main must stay transparent: the dark base on .footer above is
   the only fallback needed, and it sits BEHIND the video, so the animation plays
   on top and the flat base takes over only when the video is slow/blocked/absent.
   The export itself paints .footer-main with a light grey→transparent gradient
   (#b8b8b8 → transparent) that would wash out the top of the dark footer, so
   clear that too — leave .footer-main fully transparent. */
.footer-main {
  background-image: none;
}

/* Homepage only: #FAQ-section (.section.faq) is immediately followed by the
   shared footer only on this route, so the sibling selector below is a free,
   HTML-free scope — no data attribute needed, and no other page is touched.
   .section.faq itself is untouched here — its own grey--05 (#b8b8b8) fade,
   left alone by the export, is the correct design and matches /template's
   FAQ section exactly (white → grey--05 gradient, same token).

   The seam was never the FAQ fade — it was that .footer-main's OWN matching
   gradient (grey--05 → transparent, disabled above) got disabled outright
   instead of fixed, because at 100% of the full, very tall footer height it
   stayed opaque for most of the footer and hid the video ("flat fill" bug).
   /template solves this with a dedicated layer inside its footer's video
   wrapper: the same grey→transparent gradient, but confined to the TOP 50%
   of the footer only (position:absolute; height:50%; z-index above the
   video). Restoring that — same gradient, same colors, sized to the top half
   instead of the whole box, homepage-scoped — continues the FAQ fade into the
   footer's top edge with no flat-fill regression, and leaves .footer-main's
   own (transparent) rule untouched for every other route. */
.section.faq + .footer .footer-main {
  background-image: linear-gradient(180deg, var(--_color---grey--05), rgba(184, 184, 184, 0));
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% 50%;
}

/* Bottom bar: copyright (left) + a compact, structured contact group (right) on
   one row; the big wordmark sits below. Stacks on mobile. Compact by design —
   it must never inflate the footer height. */
.footer-bottom-row {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  height: auto;
  gap: 2rem;
}
.footer-bottom-row-inner {
  display: flex;
  flex-direction: row-reverse;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.25rem 2rem;
}
.footer-bottom-row-text {
  color: var(--_color---white--03);
}
.footer-contact {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: flex-end;
  gap: 1rem;
}
.footer-contact-col {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.footer-contact-label {
  font-size: var(--font-size--body-s);
  letter-spacing: -0.01em;
  color: var(--_color---white--04);
}
.footer-contact-link {
  color: var(--_color---white--01);
  font-size: var(--font-size--body-m);
  line-height: 1.3;
  text-decoration: none;
  transition: color 0.2s ease;
}
.footer-contact-link:hover {
  color: var(--_color---white--02);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
/* The export floats the wordmark absolutely, centered at the very bottom; drop
   it into flow below the copyright/contact row so nothing overlaps. */
.footer-bottom-logo {
  position: static;
  width: 100%;
  max-width: 22.5rem;
  margin: 0 auto;
  /* Real file is 834x184 (checked assets/*_logofooter.png/svg), not the
     535x100 the export's srcset sizing implies — reserves the correct
     height before the (loading="lazy") image loads, so nothing shifts. */
  aspect-ratio: 834 / 184;
}
@media screen and (max-width: 767px) {
  .footer-bottom-row {
    gap: 1.75rem;
  }
  .footer-bottom-row-inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.5rem;
  }
  .footer-bottom-row-text {
    order: 2;
  }
  .footer-contact {
    order: 1;
    margin-left: 0;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 1rem 2rem;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   Interactive Card & Component Hover Effects
   ══════════════════════════════════════════════════════════════════════════ */

.capabilities-card,
.value-card,
.pricing-card {
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              border-color 0.35s ease;
}

.capabilities-card:hover,
.value-card:hover,
.pricing-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
  border-color: rgba(255, 255, 255, 0.15);
}

/* WHY US mobile — two regressions traced to the same export rule,
   `.value-bottom { overflow: auto }` (its own <=991px tier — meant for a
   tablet/wider context where the 3 cards may need horizontal scroll; at
   <=767px .value-bottom is a single flex-column stack, per the export's own
   narrower tier below, so there's nothing to legitimately scroll on either
   axis here — confirmed empirically, scrollHeight===clientHeight and
   scrollWidth===clientWidth). Left in place at this width, overflow:auto
   still (a) clips each card's own outer box-shadow at the container edge
   (the shadow paints as ink beyond the card's box, which a scroll container
   crops same as any other overflowing content) and (b) on iOS Safari
   specifically, box-shadow/filter "ink" can register as scrollable content
   in an overflow:auto box even with zero measured scrollHeight overflow — a
   touch landing on that sliver can capture the gesture as an internal
   vertical scroll instead of passing it through to the page (the reported
   "iPhone 17 Pro Max" bug). overflow:visible removes both: shadows render
   fully, and there's no scroll container left to capture the gesture —
   plain page scroll takes over, as intended for a single-column stack. */
@media screen and (max-width: 767px) {
  .value-bottom {
    overflow: visible;
    /* .5rem (export, carried over from the grid's row-gap) reads as too
       tight once the cards run edge-to-edge (see the width reduction
       below) — a plainer, more generous stack rhythm. */
    gap: 1.25rem;
  }

  /* The export caps .value-card at max-width:18rem (288px) inside
     .value-bottom's centered column, leaving visible empty space on either
     side within the mobile container — the previous full-bleed
     (width/max-width:100%) fix corrected that, but reads as too large/
     edge-to-edge now that shadows render uncropped (see above). A
     percentage inset (not a rem cap — a fixed rem stops mattering below
     the width it names, missing exactly the narrow phones this needs to
     affect) keeps a consistent, modest margin at every width instead;
     align-items:center (export, this same breakpoint) centers the now-
     narrower card in the stack. */
  .value-card {
    width: 92%;
    max-width: 92%;
  }
}

.capabilities-card .capabilities-card-image {
  /* The HTML width/height attributes reserve the correct intrinsic ratio
     before lazy loading; height:auto also makes max-width scale both axes. */
  height: auto;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.capabilities-card:hover .capabilities-card-image {
  transform: scale(1.03);
}

/* IT Infrastructure & Support has no dedicated page yet. It stays in the grid
   as a plain (non-link) card, so it must not inherit the lift/shadow hover
   that signals the other cards are clickable. */
.capabilities-card-static {
  cursor: default;
}

.capabilities-card-static:hover {
  transform: none;
  box-shadow: inset 0 -2px 1px #0000001f, inset 0 0 1px 2px #fff;
  border-color: var(--_color---grey--07);
}

/* IT Infrastructure card's inner 4-icon grid (.capabilities-data-grid) —
   the export only switches it to 2x2 below a VIEWPORT width (991/767px),
   but this card's own rendered width doesn't track the viewport 1:1: it
   spans 2 of .capabilities-grid's 3 desktop columns, so at e.g. 992px
   (viewport just past the 991px tier) the card is only ~407px wide — much
   narrower than at 768px (~678px, where the export's own 2x2 already
   applies) — while the icon grid is still forced to 4 columns because
   991/767 are viewport breakpoints, not card-width ones. Flexbox then
   shrinks each 5.75rem icon to fit (measured as low as ~27px at 992px),
   which is the "capability items too narrow" failure this fixes. A
   container query keys off the card's own rendered width instead, so it
   reacts correctly regardless of which viewport-driven column count
   .capabilities-grid happens to be using. 700px is comfortably above the
   widest broken width measured (~667px) and below the narrowest already-fine
   one (~678px @ 768px viewport, ~797px @ 900px) — those keep their existing
   4-column layout unchanged; nothing wider is affected, so tablet/desktop
   above that never see 2 columns. container-type:inline-size costs nothing
   visually here — the card's width is already grid-determined, not
   content-driven. */
.capabilities-card {
  container-type: inline-size;
}
@container (max-width: 700px) {
  .capabilities-data-grid {
    grid-template-columns: 1fr 1fr;
    /* Each row's own track otherwise sizes to ONLY that row's tallest cell
       (CSS Grid's default: independent auto row tracks — measured 208px for
       row 1 vs 186px for row 2, since row 1's two items happen to need more
       lines of title text), so the top row reads visibly taller than the
       bottom one even though every .capabilities-data item has the same
       padding/content structure. 1fr row tracks resolve differently: with
       no definite grid height to distribute, the spec sizes every 1fr track
       to the LARGEST base (content) size among them (CSS Grid §11.5.1), so
       both rows end up exactly as tall as row 1 needed to be — real grid
       stretch, not a guessed fixed height. Row-gap doubled (.5rem -> 1rem)
       to stop the two rows from crowding each other now that they're each
       taller; column-gap (still .5rem, inherited) is unchanged. */
    grid-template-rows: 1fr 1fr;
    row-gap: 1rem;
  }
}

/* Homepage service cards are links to the service detail pages. Reset the
   global underlined-link style so the cards render exactly as the old divs. */
a.capabilities-card {
  color: inherit;
  text-decoration: none;
}

/* Shared section-heading reveal (src/pages/index.html initSectionHeadingsReveal)
   animates each word from y:10px + blur(10px) up to its resting position.
   .heading-style-h2's own line-height:1em + overflow:hidden (export) clips
   that motion's bottom edge mid-animation. Scoped to just the reveal-wrapped
   heading, not .heading-style-h2 globally (page-head/legal-head/etc. don't
   run this animation and don't need the bleed). padding+negative margin
   cancel out, so the box gains clipping room without moving the heading or
   changing the gap to whatever follows it. */
[data-custom-reveal="section-heading"] .heading-style-h2 {
  padding-bottom: 6px;
  margin-bottom: -6px;
}
