:root {
  --color-bg-hero: #EFEFEF;
  --color-bg-about: #0A0A0A;
  --color-bg-projects: #0A0A0A;
  --color-bg-dark: #111827;
  --color-accent: #F26522;
  --color-accent-hover: #E05A1A;
  --color-accent-blue: #2F5FEF;
  --color-accent-blue-hover: #1B3FC2;
  --color-accent-violet: #7C4DFF; /* same violet as the hero's ::before glow */
  --color-text: #F5F5F5;
  --color-text-secondary: #A1A1AA;
  --color-ink: #111827;
  --color-ink-secondary: #4B5563;
  --radius-full: 9999px;
  --radius-card: 16px;
  --radius-card-lg: 24px;
  --ease-default: cubic-bezier(0.25, 0.1, 0.25, 1);
  --duration-default: 500ms;
  --container-max: 1440px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Belt-and-suspenders fix for "solid fill leaks past the rounded corner" on the site's main
   card shells, reported as still visible after the earlier per-element overflow:hidden pass
   (and reported in both Chrome and Safari, so not treated as a browser-only quirk here).
   -webkit-mask-image with a fully-opaque white-to-black radial gradient is visually a no-op
   (it doesn't fade anything — "black" in a mask context still isn't 0-alpha the way it would be
   as a real color) but forces the corner to be clipped via the mask-compositing path instead of
   the plain border-radius+overflow-clip path, which is the standard documented fix for exactly
   this symptom. Applied to every major rounded card shell on the site as a single shared rule
   rather than repeating it on each selector individually.
   .hero-visual-frame deliberately excluded — a mask clips its content the same way
   overflow:hidden would, which trapped .hero-visual-glow inside the frame instead of letting it
   bleed out above it (that bleed is intentional, see .hero-visual-glow's own comment). */
.bento-card, .feature-card, .gs-content, .gs-visual, .wl-bento-card,
.team-grid-photo, .section-badge, .hero-badge {
  -webkit-mask-image: -webkit-radial-gradient(white, black);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  color: var(--color-text);
  background: var(--color-bg-about);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; background: none; border: none; cursor: pointer; }
ul { list-style: none; }

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 1.25rem;
}
@media (min-width: 1024px) {
  .container { padding: 0 3rem; }
}

.media-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 200px;
  border-radius: var(--radius-card);
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: repeating-linear-gradient(135deg, #15151a, #15151a 12px, #1c1c22 12px, #1c1c22 24px);
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  text-align: center;
  padding: 1rem;
}

.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 30; padding: 1.5rem 0;
  transition: background-color var(--duration-default) var(--ease-default), box-shadow var(--duration-default) var(--ease-default), backdrop-filter var(--duration-default) var(--ease-default);
}
@media (min-width: 1024px) { .site-header { padding: 1.75rem 0; } }
.site-header.is-scrolled {
  background: rgba(10, 10, 15, 0.72);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.navbar { padding-top: 0; padding-bottom: 0; }
.navbar-inner {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  background: transparent; border-radius: var(--radius-full);
  padding: 0.5rem 0; box-shadow: none;
  transition: padding var(--duration-default) var(--ease-default);
}
.site-header.is-scrolled .navbar-inner { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.navbar-side { flex: 1 1 0; display: flex; align-items: center; }
.navbar-side-left { justify-content: flex-start; }
.navbar-side-right { justify-content: flex-end; gap: 0.75rem; }
/* Same shell + underline-on-hover as .nav-link (see below) — the toggle is visually just
   another menu item now, not a distinct bordered pill. */
.lang-toggle {
  display: inline-flex; align-items: center; justify-content: center; position: relative;
  font-size: 0.875rem; color: #fff;
  background: transparent; cursor: pointer;
  border-radius: var(--radius-full);
  padding: 0.5rem 1.125rem;
  transition: color var(--duration-default) var(--ease-default);
}
.site-header.is-scrolled .lang-toggle { color: var(--color-text); }
.lang-toggle::after {
  content: ''; position: absolute; left: 1.125rem; right: 1.125rem; bottom: 0; height: 1px; background: #fff;
  transform: scaleX(0); transform-origin: left; transition: transform 250ms var(--ease-default);
}
.lang-toggle:hover::after { transform: scaleX(1); }
.logo {
  display: flex; align-items: center;
  min-height: 44px;
  transition: min-height var(--duration-default) var(--ease-default);
}
.site-header.is-scrolled .logo { min-height: 34px; }
.logo-img { height: 22px; width: auto; }
.footer-logo-img { height: 26px; }
.nav-links { display: none; align-items: center; gap: 2.25rem; flex: 0 0 auto; }
@media (min-width: 768px) { .nav-links { display: flex; } }
.nav-link {
  font-size: 0.875rem; color: #fff; position: relative;
  padding: 0.5rem 1.125rem; border-radius: var(--radius-full);
  transition: color var(--duration-default) var(--ease-default), background-color var(--duration-default) var(--ease-default);
}
.site-header.is-scrolled .nav-link { color: var(--color-text); }
.nav-link::after {
  content: ''; position: absolute; left: 1.125rem; right: 1.125rem; bottom: 0; height: 1px; background: #fff;
  transform: scaleX(0); transform-origin: left; transition: transform 250ms var(--ease-default);
}
.nav-link.is-active::after, .nav-link:hover::after { transform: scaleX(1); }

.menu-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%; background: transparent;
}
@media (min-width: 768px) { .menu-toggle { display: none; } }
.menu-toggle .icon-close { display: none; }
.menu-toggle.is-open .icon-menu { display: none; }
.menu-toggle.is-open .icon-close { display: block; }

.mobile-menu { position: fixed; inset: 0; z-index: 40; visibility: hidden; }
.mobile-menu.is-open { visibility: visible; }
.mobile-menu-backdrop {
  position: absolute; inset: 0; background: rgba(0, 0, 0, 0.6); opacity: 0;
  transition: opacity var(--duration-default) var(--ease-default);
}
.mobile-menu.is-open .mobile-menu-backdrop { opacity: 1; }
.mobile-menu-sheet {
  position: absolute; left: 0.75rem; right: 0.75rem; bottom: 0.75rem;
  background: #0f0f14; border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 1rem; padding: 1.5rem;
  display: flex; flex-direction: column; gap: 1.5rem;
  transform: translateY(120%);
  transition: transform var(--duration-default) cubic-bezier(0.32, 0.72, 0, 1);
}
.mobile-menu.is-open .mobile-menu-sheet { transform: translateY(0); }
.mobile-menu-links { display: flex; flex-direction: column; gap: 1rem; }
.mobile-menu-link { font-size: 1.75rem; font-weight: 500; min-height: 44px; display: flex; align-items: center; }

.hero {
  position: relative; min-height: 100vh; display: flex; align-items: center;
  overflow: hidden; background: var(--color-bg-about);
}
/* Reverse-engineered from the Vetra reference via devtools (not guessed): 3 concentric dashed
   rings + 9 small elements orbiting them at different radii/speeds — desktop only (hidden below
   1024px, same as the reference), sitting behind all real content. The reference's bigger
   orbiting items are a plain circle-outline icon (not a brand logo — safe to reuse as-is), mixed
   with small solid dots; kept that same dot/ring-icon mix here. */
/* Masked off below ~90% (measured: the mockup frame's own bottom edge sits at ~90% of .hero's
   height) so the rings' lower arcs don't poke out below the screenshot into the plain background
   there — everything above that line (including the overlap with the heading and the glow) stays
   visible, only the bit that would show below the mockup gets cut. */
.hero-orbits {
  display: none; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  mask-image: linear-gradient(to bottom, #000 89%, transparent 90.5%);
  -webkit-mask-image: linear-gradient(to bottom, #000 89%, transparent 90.5%);
}
@media (min-width: 1024px) { .hero-orbits { display: block; } }
/* top:59% (not 50%) — measured on the reference: its orbit center sits 26.2% down inside the
   mockup frame, not centered on the whole hero. Converted to our own layout (badge/heading/frame
   proportions differ), that same relative point lands at 59% of .hero's own height — low enough
   that the outer r:500 ring's top edge lands inside .hero-heading's own box (a deliberate slight
   overlap, matching the reference) and overlaps .hero-visual-glow near the frame's top, same as
   the reference blends its glow and orbit layers there. Tied to current content proportions like
   the rest of this file's hero measurements — re-check if hero copy length changes a lot. */
.hero-orbit-ring {
  position: absolute; top: 59%; left: 50%;
  width: calc(var(--r) * 2px); height: calc(var(--r) * 2px);
  border: 1px dashed rgba(255, 255, 255, 0.1); border-radius: 50%;
  transform: translate(-50%, -50%);
}
.hero-orbit-item {
  position: absolute; top: 59%; left: 50%;
  width: var(--size); height: var(--size);
  margin-top: calc(var(--size) * -0.5); margin-left: calc(var(--size) * -0.5);
  animation: hero-orbit calc(var(--duration) * 1s) linear infinite;
}
/* Same technique as the reference: rotate to --angle, translate out by --radius, rotate back so
   the item itself stays upright — then animate a full turn on both rotations together, so the
   item travels the ring without spinning in place. */
@keyframes hero-orbit {
  0% { transform: rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg)); }
  100% { transform: rotate(calc(var(--angle) * 1deg + 1turn)) translateY(calc(var(--radius) * 1px)) rotate(calc((var(--angle) * -1deg) - 1turn)); }
}
.hero-orbit-item-dot { border-radius: 50%; background: rgba(255, 255, 255, var(--dot-opacity, 0.6)); }
.hero-orbit-item-icon { border: 1.5px solid rgba(255, 255, 255, 0.55); border-radius: 50%; }

.hero-content {
  position: relative; z-index: 2; padding-top: 244px; padding-bottom: 150px; text-align: center;
  width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 1rem;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.2);
}
/* Desktop-only: brings the Hero→Features gap in line with the other section boundaries below
   (Features↔Projects↔About), all normalized to a shared 6rem/6rem top+bottom = 12rem total gap —
   see the .features/.projects/.about padding rules further down, changed as one matching set per
   an explicit "make every section gap equal" ask. Doesn't touch the mobile/base 150px value
   (untested at that size, left as-is) or anything inside .hero-visual-frame — the glow/fade
   layers there are positioned relative to the frame's own box, not this outer padding, so
   shrinking it doesn't shift them. */
@media (min-width: 1024px) { .hero-content { padding-bottom: 6rem; } }
/* margin-bottom: 0.5rem on top of .hero-content's 1rem flex gap = 1.5rem (24px) total to the
   heading — 1.5x the default 1rem gap, per an explicit ask to space the badge further from it. */
.hero-badge {
  position: relative; z-index: 2; margin-top: -16px; margin-bottom: 0.5rem;
  display: inline-flex; align-items: center;
  border-radius: var(--radius-full); overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.02);
}
@property --hero-badge-angle {
  syntax: '<angle>'; inherits: false; initial-value: 0deg;
}
.hero-badge-ring {
  position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  background: conic-gradient(from var(--hero-badge-angle), transparent 0%, transparent 82%, rgba(255, 255, 255, 0.95) 92%, transparent 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: hero-badge-spin 3.5s linear infinite;
  will-change: transform;
}
/* Padding intentionally asymmetric: left = top/bottom (0.4rem) so the gap from the outer
   badge border to the capsule reads equal on those three sides; right stays wider (1rem) for
   breathing room after the trailing text — unrelated to the capsule, not touched. */
.hero-badge-content {
  position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 1rem 0.4rem 0.4rem; font-size: 0.8125rem; color: rgba(255, 255, 255, 0.85);
}
/* Small violet capsule at the badge's left edge, standing in for the reference's blue "new"
   chip — the literal "DEV" (same font as the rest of the badge text, just bolder) instead of
   an icon. Sized by its own text+padding (not a fixed width/height), so it's naturally an
   elongated pill rather than a circle. */
.hero-badge-dot {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  padding: 0.15rem 0.45rem; border-radius: var(--radius-full); line-height: 1;
  background: var(--color-accent-violet); color: #fff;
  font-size: inherit; font-weight: 800; letter-spacing: 0.02em;
}
@keyframes hero-badge-spin { to { --hero-badge-angle: 360deg; } }
@media (max-width: 420px) {
  .hero-badge-content { font-size: 0.75rem; gap: 0.375rem; padding: 0.3rem 0.75rem 0.3rem 0.3rem; }
  .hero-badge-dot { padding: 0.1rem 0.375rem; }
}

/* Static twin of .hero-badge (same pill shell — border/radius/background) used above the
   product titles in Gem Space/Gem Team/White Label. No .hero-badge-ring/spin animation, per
   an explicit ask ("статичные, без всякой анимации"). align-self:flex-start because
   .gs-content-header has no align-items set (defaults to stretch) — without it this inline-flex
   pill would stretch to the header's full width instead of hugging its own text. */
.section-badge {
  align-self: flex-start;
  display: inline-flex; align-items: center;
  border-radius: var(--radius-full); overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.02);
  padding: 0.4rem 1rem;
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
}

.hero-heading {
  font-size: clamp(1.53rem, 5.5vw, 3.8rem); font-weight: 700; line-height: 1.25; letter-spacing: normal; color: #fff;
}
.hero-heading-line { display: block; }
@media (min-width: 640px) { .hero-heading { font-size: clamp(2.1rem, 4.2vw, 3.8rem); } }
.hero-text { max-width: 46rem; margin: 0.5rem auto 0; font-size: 1rem; line-height: 1.55; color: var(--color-text-secondary); }
@media (min-width: 640px) { .hero-text { font-size: 1.125rem; } }

.hero-cta {
  position: relative; z-index: 2; margin-top: 1.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.875rem 1.75rem; border-radius: 10px;
  background: #fff; color: var(--color-ink); font-size: 0.9375rem; font-weight: 600;
  transition: transform 200ms var(--ease-default), background-color 200ms ease;
}
.hero-cta:hover { background: #e6e6e6; transform: translateY(-2px); }

/* z-index:1, not 2 — lower than .hero-badge/.hero-cta (both z-index:2) on purpose. The new
   .hero-visual-glow's heavy blur bleeds well above .hero-visual-frame's own box, and at equal
   z-index the later-in-DOM element (.hero-visual) would paint over the badge/CTA above it,
   visibly tinting their white backgrounds. Keeping this lower ensures the glow bleed always
   renders behind the hero's actual content. */
.hero-visual { position: relative; z-index: 1; margin-top: 3.5rem; padding: 0 1rem; width: 100%; }
.hero-visual-frame {
  position: relative; z-index: 1; max-width: 1152px; margin: 0 auto;
  border-radius: 32px; border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(16px);
  padding: 0.5rem;
}
/* Reverse-engineered directly from the Vetra reference via devtools (not guessed): the glow
   there isn't a radial-gradient at all — it's a plain rectangular div, sized 75%/25% of the
   mockup frame and centered right on the frame's own top edge, blurred by a huge 10rem/160px
   filter so it bleeds far outside its own box. It sits inside the frame (behind the screenshot,
   z-index:-1) — the parts of the blur that spill above the frame's top edge (where there's no
   opaque image covering them) are what actually reads as "the glow". Same technique here, just
   violet instead of the reference's blue. No overflow:hidden on .hero-visual-frame — required
   for the blur to bleed out. */
.hero-visual-glow {
  position: absolute; z-index: -1; pointer-events: none;
  top: 12.5%; left: 50%; width: 75%; height: 25%;
  transform: translate(-50%, -50%);
  background: linear-gradient(to right, #a78bfa, var(--color-accent-violet));
  filter: blur(4rem);
}
@media (min-width: 1024px) { .hero-visual-glow { filter: blur(10rem); } }
.hero-visual-inner {
  display: block; width: 100%;
  aspect-ratio: 2284 / 1218; /* matches images/gemteam-hero.png's own ratio, so nothing is cropped */
  object-fit: cover;
  border-radius: 26px;
}
.hero-visual-fade {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; height: 50%;
  background: linear-gradient(to top, var(--color-bg-about), transparent);
  pointer-events: none;
}

.bento-grid {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: 1fr; grid-auto-rows: minmax(120px, auto); gap: 0.75rem;
}
@media (min-width: 768px) {
  .bento-grid { grid-template-columns: repeat(6, 1fr); }
  .bento-card-1 { grid-column: span 4; grid-row: span 2; }
  .bento-card-2 { grid-column: span 2; }
  .bento-card-3 { grid-column: span 2; }
  .bento-card-4 { grid-column: span 3; }
  .bento-card-5 { grid-column: span 3; }
}

.bento-card {
  position: relative; overflow: hidden;
  border-radius: var(--radius-card-lg); padding: 30px;
  border: 1px solid rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.06);
  color: #fff;
  transition: border-color 150ms ease;
}
.bento-card:hover { border-color: #fff; }

.bento-card-title { font-size: 1rem; font-weight: 600; line-height: 1.25; margin-bottom: 0.5rem; padding-right: 5rem; }
@media (min-width: 768px) { .bento-card-title { font-size: 1.125rem; } }
.bento-card-tag {
  position: absolute; bottom: 24px; left: 24px;
  border: 1px solid rgba(255, 255, 255, 0.3); border-radius: var(--radius-full);
  padding: 0.125rem 0.5rem; font-size: 0.625rem; text-transform: lowercase; letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.85);
  background: rgba(255, 255, 255, 0.08);
}
.bento-card-text { font-size: 0.875rem; color: rgba(255, 255, 255, 0.7); max-width: 65ch; }

.bento-card-hover { position: absolute; inset: 0; opacity: 0; pointer-events: none; }
.bento-card:hover .bento-card-hover { opacity: 1; }
.bento-card-hover-ring {
  position: absolute; inset: -4px; border-radius: calc(var(--radius-card-lg) + 4px); border: 1px solid var(--color-accent-violet);
  mask-image: radial-gradient(180px 180px at 50% 50%, white, transparent);
  -webkit-mask-image: radial-gradient(180px 180px at 50% 50%, white, transparent);
}

.features { background: var(--color-bg-about); padding: 4rem 0 3rem; }
/* Desktop top/bottom both 6rem — same value on every section boundary (Hero/.features/.projects/
   .about all share it) so every gap between sections is identical: 6rem + 6rem = 12rem, per an
   explicit "make every section gap equal" ask. */
@media (min-width: 1024px) { .features { padding: 6rem 0 6rem; } }
/* Widened from 40rem so the (now hero-sized) Features heading has room for its own manual
   2-line break without wrapping a 3rd time — see .features-subtext below, which pins itself
   back to the original 40rem so the subtext paragraph doesn't get uncomfortably wide too. */
.features-heading-block { max-width: 72rem; margin-left: auto; margin-right: auto; text-align: center; }
/* Same font-size AND line-height as .hero-heading (the Hero's H1) — used by both "Всё
   необходимое..." (Features) and "Наши проекты" (Projects), which share this class. */
.features-heading { font-size: clamp(1.53rem, 5.5vw, 3.8rem); font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; }
@media (min-width: 640px) { .features-heading { font-size: clamp(2.1rem, 4.2vw, 3.8rem); } }
/* Same font-size (both breakpoints), heading-to-subtext gap, AND container max-width as
   .hero-text (the Hero's own subtext) — used by both "Всё необходимое..." (Features) and
   "Наши проекты" (Projects). Widened from 40rem to 46rem per an explicit ask. */
.features-subtext { max-width: 46rem; margin: 1.5rem auto 0; font-size: 1rem; line-height: 1.6; color: var(--color-text-secondary); }
@media (min-width: 640px) { .features-subtext { font-size: 1.125rem; } }

/* No max-width — this used to be capped at 74rem/1184px to match a reference design's own
   measured grid width, which left it narrower than every other section's .container (1344px
   inner width at a 1728px viewport). Now spans the same width as the rest of the site. */
/* margin-top matches .hero-visual's own margin-top (3.5rem) — same gap as the Hero's
   "Наши проекты" button → screenshot, per explicit ask, fixed across breakpoints like the
   hero's value (no separate 1024px bump). */
.features-grid { margin-top: 3.5rem; gap: 1.5rem; }
@media (min-width: 768px) {
  .features-grid.bento-grid { grid-template-columns: repeat(6, 1fr); }
  .feature-card:nth-child(1),
  .feature-card:nth-child(2),
  .feature-card:nth-child(3) { grid-column: span 2; }
  .feature-card:nth-child(4) { grid-column: span 4; }
  .feature-card:nth-child(5) { grid-column: span 2; }
  /* IT-outsourcing's card is double-wide — let its paragraph use that extra width so the
     full sentence sets in 2 lines instead of wrapping at the same narrow column as its
     single-width siblings. */
  .feature-card:nth-child(4) .feature-card-text { max-width: 76ch; }
}
.feature-card {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column;
  border-radius: var(--radius-card-lg); padding: 1.5rem;
  border: 1px solid rgba(255, 255, 255, 0.1); background: #0a0a0a;
  color: #fff;
  transition: border-color 200ms ease;
}
.feature-card:hover { border-color: rgba(255, 255, 255, 0.2); }
.feature-card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; flex-shrink: 0; }
.feature-card-icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; color: #fff;
}
.feature-card-title { font-size: 1.25rem; font-weight: 600; line-height: 1.3; }
.feature-card-text {
  font-size: 0.875rem; line-height: 1.55; color: var(--color-text-secondary); max-width: 42ch;
  margin-bottom: auto;
}

/* All visual sections share one height so every bento card's "graphic area" lines up. */
.feature-card-visual { margin-top: 1.5rem; height: 86px; flex-shrink: 0; }

/* IT-outsourcing's chart spans the full width of its (double-wide) card, reproducing the
   reference's "Real-Time Analytics" mockup: a period-tab row, then two bordered chart panels
   side by side (each an axis + 3-column bar chart), the first panel highlighted. Built from
   plain flex/grid boxes, not a fixed SVG viewBox, so it can fill whatever width it's given
   (see the growth-chart's letterboxing note above). */
.feature-card-visual.feature-visual-kpi {
  height: auto; display: flex; flex-direction: column;
}
.kpi-tabs { display: flex; align-items: center; gap: 1.125rem; flex-shrink: 0; }
.kpi-tab { font-size: 0.8125rem; color: rgba(255, 255, 255, 0.5); padding: 0.5rem 0.875rem; border-radius: 8px; white-space: nowrap; }
.kpi-tab-active { background: rgba(255, 255, 255, 0.08); color: #fff; font-weight: 500; }

.kpi-panels { display: flex; gap: 1.25rem; margin-top: 1rem; }
.kpi-panel {
  flex: 1; min-width: 0; height: 150px;
  display: flex; flex-direction: column;
  border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.08); padding: 0.875rem 1rem 0.75rem;
}
.kpi-panel-active { border-color: rgba(155, 124, 255, 0.5); box-shadow: inset 0 0 0 1px rgba(155, 124, 255, 0.15); }

.kpi-row { display: grid; grid-template-columns: 1.75rem repeat(3, 1fr); column-gap: 1rem; }
.kpi-row-chart {
  flex: 1; min-height: 0; align-items: end;
  padding-bottom: 0.5rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.kpi-axis {
  grid-column: 1; height: 100%;
  display: flex; flex-direction: column; justify-content: space-between;
  font-size: 10px; color: rgba(255, 255, 255, 0.32); padding-bottom: 2px;
}
.kpi-col { position: relative; height: 100%; display: flex; align-items: flex-end; justify-content: center; }
.kpi-bar {
  width: 56%; max-width: 44px; border-radius: 8px 8px 4px 4px;
  background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.16);
}
/* left/right insets widened 6%→22% (was 88% of the column wide, now 56%) per explicit ask to
   shorten these — still centered over the same middle .kpi-col via the equal left/right values. */
.kpi-target-line {
  position: absolute; left: 22%; right: 22%; height: 3px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.85); box-shadow: 0 0 6px rgba(255, 255, 255, 0.4);
}
/* The middle column's bar in each panel now deliberately rises ABOVE its own .kpi-target-line
   (its inline height% is set higher than the line's own bottom% in the markup) — reads as
   "we beat the target," per explicit ask — and gets the same violet gradient as the White Label
   card's animated .wl-layout-bar bars (same two-stop pair, var(--color-accent-violet) → #a78bfa)
   instead of the plain translucent-white fill every other bar keeps, so this "our metric" column
   visually stands out from the two neutral ones beside it. Border/radius stay the same as
   .kpi-bar so only the fill changes. */
.kpi-bar-highlight {
  background: linear-gradient(180deg, #a78bfa, var(--color-accent-violet));
  border-color: rgba(155, 124, 255, 0.5);
}
.kpi-row-footer { margin-top: 0.5rem; font-size: 11px; color: rgba(255, 255, 255, 0.35); }
.kpi-row-footer span { text-align: center; }
.kpi-unit { text-align: left; }

/* "Сильная команда" and "White Label" get the same +14px bump so their graphic occupies as
   much height as "Собственные продукты"'s notify-stack (which effectively spans 100px once
   its peeking back-card is counted) instead of sitting in the shared 86px box with extra
   absorbed slack above it. */
.feature-card-visual.feature-visual-tiles,
.feature-card-visual.feature-visual-browser { height: 100px; }

.feature-visual-tiles { position: relative; }
.tile-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); gap: 0.5rem; height: 100%; }
.tile-cell { display: block; border-radius: 10px; background: rgba(255, 255, 255, 0.03); }
.tile-cell-skeleton {
  display: flex; flex-direction: column; justify-content: center; gap: 0.3125rem;
  padding: 0 0.625rem; background: rgba(255, 255, 255, 0.06);
}
.tile-line { display: block; height: 3px; border-radius: 2px; background: rgba(255, 255, 255, 0.18); width: 80%; }
.tile-line-head { width: 45%; background: rgba(255, 255, 255, 0.32); }
.tile-check {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: #161616; border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4);
}

.feature-visual-notify { position: relative; margin-top: 2.375rem; }
.notify-card {
  border-radius: 14px; display: flex; align-items: flex-start; gap: 0.625rem;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.notify-card-back {
  position: absolute; left: 6%; right: 6%; top: -14px; z-index: 0;
  height: 34px; padding: 0.375rem 0.875rem;
  background: #161616; border: 1px solid rgba(255, 255, 255, 0.06);
}
.notify-back-chip { width: 20px; height: 20px; border-radius: 6px; background: #0a0a0a; flex-shrink: 0; }
.notify-back-text {
  display: inline-flex; align-items: center; gap: 0.375rem;
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.4);
}
.notify-card-front {
  position: relative; z-index: 1; padding: 0.75rem;
  display: flex; align-items: stretch; gap: 0.75rem;
  background: #1c1c1c; border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Height matches the 3-line text stack's natural height (tag + title + time, see .notify-body
   below: 17+19+18px rows + 2×3px gaps = 60px) instead of a fixed 44px, so it fills the same
   vertical span as the text next to it. (Tried deriving this from the stretched row height via
   `aspect-ratio: 1` on an auto-width flex item, but a flex item's main-axis size resolves before
   cross-axis stretch is applied, so aspect-ratio had nothing to size width from and fell back to
   the SVG's own intrinsic 20px — hence the explicit numbers here, which are also what keep the
   padding/gap around it — all 0.75rem — visually equal on every side.) Widened past square into
   a rectangle so the bolt icon (sized off height, auto-width to stay undistorted) has less
   leftover dead space to its right. */
.notify-icon {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 72px; height: 60px; border-radius: 12px; background: #0a0a0a;
}
.notify-icon svg { width: auto; height: 50%; }
.notify-body { display: flex; flex-direction: column; justify-content: center; gap: 0.1875rem; min-width: 0; }
.notify-tag {
  display: inline-flex; align-items: center; gap: 0.3125rem;
  font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-accent-violet);
}
.notify-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-accent-violet); flex-shrink: 0; }
.notify-dot-muted { background: rgba(255, 255, 255, 0.35); }
.notify-title { font-size: 1rem; font-weight: 700; color: #fff; line-height: 1.2; }
.notify-time { font-size: 0.75rem; color: var(--color-text-secondary); }

.feature-visual-browser {
  display: flex; flex-direction: column;
  border-radius: 0.5rem; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.03);
  padding: 0.5rem 0.625rem; box-sizing: border-box;
}
.browser-dots { display: flex; gap: 0.3rem; margin-bottom: 0.5rem; flex-shrink: 0; }
.browser-dots span { width: 7px; height: 7px; border-radius: 50%; display: block; }
.browser-dots span:nth-child(1) { background: #FF6159; }
.browser-dots span:nth-child(2) { background: #FFC02E; }
.browser-dots span:nth-child(3) { background: #2ACF52; }
/* align-items: stretch (was center) so .browser-content now spans the SAME full height as
   .browser-rail instead of sitting as a small vertically-centered cluster — per explicit ask, so
   the profile row's top lines up with the rail's top and (via .browser-content's own
   justify-content below) the last gray line's bottom lines up with the rail's bottom. */
.browser-bar {
  flex: 1; border-radius: 4px; background: rgba(255, 255, 255, 0.06);
  display: flex; align-items: stretch; gap: 0.5rem; padding: 0 0.5rem; min-height: 0;
}
.browser-rail { width: 8px; margin: 0.375rem 0; border-radius: 3px; background: rgba(255, 255, 255, 0.12); flex-shrink: 0; }
/* justify-content: space-between (was just a fixed gap) spreads the profile row + 3 lines across
   the full stretched height — top row flush with the rail's top, bottom line flush with the
   rail's bottom, per explicit ask ("растяни отступы... чтобы они доходили до нижнего края"). */
.browser-content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; margin: 0.375rem 0; min-width: 0; }
.browser-line { display: block; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.18); width: 70%; }
.browser-line-head { width: 40%; background: rgba(255, 255, 255, 0.32); }
.browser-line-short { width: 52%; }
/* Profile-header row — moved above the text lines and per explicit ask ("шапка профиля"): a
   round avatar/logo placeholder plus a name/handle chip next to it, same height as each other so
   they read as one baseline-aligned unit. Was previously a single .browser-chip sitting at the
   BOTTOM of .browser-content with no avatar — .browser-avatar is new, .browser-chip itself got
   thicker (9px→11px) and shorter (32%→20% width) to read more like a short name label than the
   long stray bar it was before. */
/* Avatar bumped 11px→15px per explicit ask ("increase it a bit so it reaches the rail's edge") —
   now that .browser-content stretches flush with .browser-rail's own top (see above), a bigger
   circle reads as properly reaching/aligning with the rail rather than floating small and
   centered. .browser-chip no longer matches the avatar's height 1:1 — it went 15px→12px→8px
   across three follow-up asks ("too thick", then "still didn't look narrower, go further"), so
   it now reads as a genuinely slim name/handle label next to the bigger avatar, not matching its
   bulk — align-items:center on .browser-profile keeps it vertically centered against the avatar
   either way, so the two don't need equal height for that centering to hold. */
.browser-profile { display: flex; align-items: center; gap: 0.25rem; }
.browser-avatar { display: block; width: 15px; height: 15px; border-radius: 50%; background: var(--color-accent-violet); opacity: 0.85; flex-shrink: 0; }
.browser-chip { display: block; width: 20%; height: 8px; border-radius: 4px; background: var(--color-accent-violet); opacity: 0.85; }

.feature-visual-bars { display: flex; align-items: flex-end; gap: 0.375rem; height: 44px; }
.feature-visual-bars span { flex: 1; display: block; border-radius: 3px 3px 0 0; background: var(--color-accent-violet); opacity: 0.85; }

/* Unlike the other cards' visuals, this one is let to grow: "Цифровой консалтинг" sits next
   to the now much-taller IT-outsourcing card, and row-stretch would otherwise leave a large
   gap ABOVE this fixed-height chart (the text's margin-bottom:auto soaking up all the slack).
   flex-grow here consumes that slack instead, so the gap after the text stays the standard
   1.5rem from the shared .feature-card-visual rule and the chart itself fills the rest. */
.feature-card-visual.feature-visual-growth { height: auto; flex: 1 1 auto; min-height: 86px; }
.feature-visual-growth { overflow: hidden; }
.feature-visual-growth svg { width: 100%; height: 100%; display: block; }
.feature-visual-growth .growth-grid line { stroke: rgba(255, 255, 255, 0.1); stroke-width: 1; stroke-dasharray: 2 3; }
.feature-visual-growth .growth-labels text { fill: rgba(255, 255, 255, 0.35); font-size: 11px; text-anchor: middle; font-family: inherit; }

.about { background: var(--color-bg-about); padding: 4rem 0 3rem; }
/* Same shared 6rem/6rem desktop padding as .features/.projects — see comment on .features. */
@media (min-width: 1024px) { .about { padding: 6rem 0 6rem; } }
/* Heading/subtext now reuse .features-heading-block/.features-heading/.features-subtext — the
   same centered, same-size heading+paragraph pattern as the Features and Projects sections
   above, per an explicit ask ("по аналогии с предыдущими разделами"), replacing the old
   left-aligned two-column .team-intro-grid layout. .team-grid below already supplies its own
   margin-top, so no extra spacing rule is needed on .about-heading-block itself. */
/* About's own paragraph is a full multi-sentence company description — noticeably longer than
   Features'/Projects' one-liners — so at the shared 46rem it wrapped to 6 lines. Widened by
   ~20% (46rem → 55.2rem) HERE ONLY, scoped through .about-heading-block, rather than changing
   .features-subtext's own max-width — that shared rule still serves Features/Projects' shorter
   text fine and wasn't asked to change. */
.about-heading-block .features-subtext { max-width: 55.2rem; }

.reveal-up {
  opacity: 0; transform: translateY(16px);
  transition: opacity var(--duration-default) ease-out, transform var(--duration-default) ease-out;
}
.reveal-up.is-visible { opacity: 1; transform: translateY(0); }

/* Static portrait grid, replacing the old infinite-scroll marquee — per an explicit ask, modeled
   on a supplied reference (role reveals on hover, "_0N" index). Reference's own grayscale→color
   photo treatment was dropped per a follow-up ask — photos stay in color always now, only the
   height/radius hover shrink survived from that part of the reference.
   1/2/3 columns by breakpoint, matching the reference's own sm:grid-cols-2 lg:grid-cols-3. */
.team-grid { display: grid; grid-template-columns: 1fr; gap: 2rem 1.5rem; margin-top: 3rem; }
@media (min-width: 640px) { .team-grid { grid-template-columns: repeat(2, 1fr); margin-top: 4rem; } }
@media (min-width: 1024px) { .team-grid { grid-template-columns: repeat(4, 1fr); } }

.team-grid-card { position: relative; }
/* Real photos pulled for all 6 people for now — per an explicit ask, every card is a plain flat
   gray placeholder rectangle until real photos are supplied (drop a background-image back onto
   the individual .team-grid-photo divs in index.html when they're ready). */
/* border-radius: --radius-card-lg (24px) — the reference's own 0.5rem/8px (rounded-md) read as
   too tight/off-brand next to the rest of the site, which reserves that small a radius for tiny
   chips (notify icons, kpi tabs) and uses 16–24px for anything card- or panel-sized. This panel
   is as big as the hero screenshot frame / bento cards, so it now shares their larger radius. */
.team-grid-photo {
  height: 24rem; border-radius: var(--radius-card-lg); overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  background-size: cover; background-position: center top;
  transition: height 500ms ease, border-radius 500ms ease;
}
/* Slight height/radius shrink on hover — mirrors the reference's own group-hover:h-[22.5rem]
   group-hover:rounded-xl treatment, scaled up to match the new larger base radius. */
.team-grid-card:hover .team-grid-photo { height: 22.5rem; border-radius: 1.75rem; }

.team-grid-name { margin-top: 0.75rem; font-size: 1rem; font-weight: 500; color: #fff; transition: letter-spacing 500ms ease; }
.team-grid-card:hover .team-grid-name { letter-spacing: 0.03em; }

/* Role stays hidden until hover (like the reference's Linktree/role reveal) — EXCEPT on
   touch devices, which have no hover state to trigger it; (hover: none) below keeps it always
   visible there so the role isn't permanently inaccessible on mobile/tablet. */
.team-grid-role {
  margin-top: 0.25rem; font-size: 0.875rem; color: var(--color-text-secondary);
  opacity: 0; transform: translateY(0.5rem);
  transition: opacity 300ms ease, transform 300ms ease;
}
.team-grid-card:hover .team-grid-role { opacity: 1; transform: translateY(0); }
@media (hover: none) {
  .team-grid-role { opacity: 1; transform: translateY(0); }
}

.projects { position: relative; background: var(--color-bg-about); padding: 4rem 0 4.5rem; }
/* Same shared 6rem/6rem desktop padding as .features/.about — see comment on .features. */
@media (min-width: 1024px) { .projects { padding: 6rem 0 6rem; } }

/* Two accent glows straddling the very left/right edges of the whole Projects section — mostly
   sitting off-canvas (translate ±58%, not ±50%, so more than half is pushed outside the viewport),
   with only the blurred tail reading as a soft hint of color right at the screen edges, per an
   explicit ask. .projects has no overflow:hidden, so the bleed isn't clipped. z-index:0 + the
   explicit .container z-index:1 below keep real content above them, same pattern as the earlier
   per-panel glow.
   top offsets are individually tuned (not a shared value) to sit roughly at the vertical
   midpoint of a specific panel — measured via getBoundingClientRect against .projects' own
   height: Gem Space's midpoint ≈27%, White Label's ≈84% — per an explicit "raise the left one to
   Gem Space's middle, lower the right one to White Label's middle" ask. Re-measure and adjust
   these two numbers if panel content/heights change enough to shift those midpoints. */
.projects-glow {
  position: absolute; z-index: 0; pointer-events: none;
  width: 32vw; height: 32vw; max-width: 500px; max-height: 500px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--color-accent-violet), transparent 70%);
  filter: blur(6rem); opacity: 0.3;
}
.projects-glow-left { left: 0; top: 27%; transform: translate(-58%, -50%); }
.projects-glow-right { right: 0; top: 84%; transform: translate(58%, -50%); }
@media (min-width: 1024px) { .projects-glow { filter: blur(9rem); } }
.projects > .container { position: relative; z-index: 1; }

/* No margin-top on .projects-heading-block/.projects-list — .projects already supplies the
   section's own top padding, same single-layer spacing as .features-heading-block above (its
   section's padding is the only offset there too). An extra margin-top would double up with
   that padding, same issue .gs-split had. */
/* Same margin-bottom as .features-grid's margin-top (3.5rem, matching the Hero's button→screenshot
   gap) — this is now the ONLY spacing layer before gem-space-panel (its own padding-top was
   removed, see .project-block rules below), so the visible gap matches that reference gap exactly
   instead of stacking with a second layer of padding the way it did before. */
.projects-heading-block { margin-bottom: 3.5rem; }
.projects-list { display: flex; flex-direction: column; gap: 2.5rem; }
@media (min-width: 1024px) { .projects-list { gap: 3rem; } }

/* .contacts-lead (an empty leftover section — no content, just top padding) removed entirely:
   it sat between .projects and .about with nothing in it, adding 80px of dead space on top of
   .projects' own bottom padding — the actual cause of the "gap feels too big" complaint, not a
   padding-value mismatch. Removing it also makes this boundary use the same mechanism as every
   other section gap (adjacent sections' own padding meeting directly, no spacer section between). */
/* No horizontal padding here — only vertical (set below via the #panel-id rule). This used to be
   a plain `padding: 2rem/3.5rem` shorthand, which added its own left/right inset ON TOP OF the
   parent .container's own padding — the three project panels rendered narrower (1232px at a
   1728px viewport) than every other section using a plain .container (1344px: header, footer,
   about, team marquee). Vertical-only here keeps the panels' own top/bottom spacing while
   letting them span the same width as the rest of the site. */
.project-block { position: relative; background: var(--color-bg-projects); }
/* No padding-top/bottom on the panels anymore — that spacing used to stack with .projects-list's
   own gap (double layer, same issue as gem-space-panel's heading gap had). Now .projects-list's
   gap alone controls the space between blocks, set to match the "Наши проекты" subtext-to-
   gem-space gap (2.5rem/3rem) per an explicit ask to make all these gaps consistent. */
#gem-space-panel, #gem-team-panel, #white-label-panel { background: none; }
.project-block-bg {
  position: absolute; inset: 0; z-index: 0;
  background: var(--color-bg-projects);
}
/* No .product-showcase-text color override here anymore — these panels used to sit on colored
   per-project backgrounds (orange/violet gradients) that needed near-white text for contrast.
   Now that the panels are flat dark like the rest of the site, the base .product-showcase-text
   rule's own color: var(--color-text-secondary) (the same gray as .features-subtext/the "Наши
   проекты" subtext) applies correctly instead of being overridden to near-white. */
#gem-space-panel .product-showcase-title { color: #fff; }
#gem-space-panel .accordion-item { border-bottom-color: rgba(255, 255, 255, 0.25); }
#gem-space-panel .accordion-trigger { color: #fff; }
#gem-space-panel .accordion-panel p { color: rgba(255, 255, 255, 0.75); }

#gem-team-panel .product-showcase-title { color: #fff; }

#white-label-panel .product-showcase-title { color: #fff; }

#gem-space-panel > .gem-showcase,
#gem-team-panel > .gem-showcase,
#white-label-panel > .gem-showcase { position: relative; z-index: 1; }

.store-badge {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; min-height: 44px;
  padding: 0.875rem 1.75rem; border-radius: 10px;
  background: #fff; color: var(--color-ink); font-size: 0.9375rem; font-weight: 600;
  transition: background-color 200ms ease, transform 200ms var(--ease-default);
}
.store-badge:hover { background: #e6e6e6; transform: translateY(-2px); }
/* Gem Space's App Store / Google Play / RuStore links — official logo + full wordmark artwork
   (Apple's, Google's and RuStore's own SVGs), outlined to match .store-badge's own box (the white
   pill used for Gem Team's "Посетить веб-сайт"/"Узнать подробнее" buttons): height 50.5px
   (.store-badge's real *rendered* height — its own `min-height:44px` is only a floor; the actual
   box measured via getBoundingClientRect, once padding + text line-height are added, is 50.5px)
   and the same 10px corner radius.
   Each official SVG originally ships as a COMPLETE pill graphic — its own solid background fill
   plus (RuStore's variant specifically) its own baked-in white outline stroke, each with its OWN
   corner radius (roughly 6px/6.3px/4.4px at this height for Apple/Google/RuStore respectively —
   all different from each other and from our 10px, since every brand draws its badge at its own
   proportions). Wrapping that finished pill inside a SECOND, differently-radiused border (the
   first attempt at this) put two mismatched curves on screen — for RuStore literally two visible
   white outlines at two different radii, for all three a gap/step where the image's own corner
   curve didn't line up with the wrapper's — which is exactly the "doubled, weird corners" defect.
   Fixed at the source instead of papering over it: `images/badge-*.svg` had their own background
   fill/stroke path(s) deleted (kept only the logo mark + wordmark text paths, which were already
   plain white/brand-colored with a transparent background in the original artwork), so now there
   is only ONE rounded-rect edge on screen — ours — and the logo+text content just floats inside
   it. overflow:hidden on .app-badge exists only as a safety clip, not to hide a second border. */
/* Fixed width (not auto) so all 3 badges line up the same size — RuStore's own artwork is
   narrower than App Store's/Google Play's at this shared height (its "Скачайте из RuStore"
   wordmark is shorter than "Download on the App Store"/"GET IT ON Google Play"), so left at auto
   width it visibly sat narrower than the other two per an explicit ask. 166px matches App
   Store/Google Play's own natural width at 50.5px tall (they don't need this, it's a no-op for
   them); RuStore's image (width:auto, capped by max-width:100%) just centers inside the wider
   box instead of stretching to fill it, so its own logo+text keep their correct proportions. */
/* justify-content: flex-start (was center) per explicit ask, specifically for RuStore: its own
   artwork is narrower than the shared 166px box (see the equal-width note above), and centering
   left it with visibly more empty margin on the left than App Store/Google Play's own built-in
   edge padding — flex-start lines its content up flush against the same left inset the other two
   already have baked into their own artwork, instead of splitting the leftover space on both
   sides. App Store/Google Play fill the full 166px width already, so this is a no-op for them. */
.app-badge {
  display: inline-flex; align-items: center; justify-content: flex-start;
  width: 166px; height: 50.5px; border-radius: 10px; overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.5);
  transition: border-color 200ms ease, transform 200ms var(--ease-default);
}
.app-badge:hover { border-color: #fff; transform: translateY(-2px); }
.app-badge-img { display: block; height: 100%; width: auto; max-width: 100%; }

/* Slightly smaller and less bold than .features-heading ("Наши проекты" / bento-cards headings)
   on purpose — these product-name headings (Gem Space/Gem Team/White Label) were rendering
   bigger and bolder than the section-level headings above them, which reads backwards in the
   hierarchy. */
.product-showcase-title { font-size: clamp(1.5rem, 3.4vw, 2.25rem); font-weight: 600; letter-spacing: -0.01em; }
.product-showcase-title-sub { display: block; font-size: 0.5em; font-weight: 500; margin-top: 0.375rem; }
/* Gem Space/Gem Team's square product logo, inline right before the heading text — sized in `em`
   (1em × 1em) so it's tied directly to .product-showcase-title's own font-size (a responsive
   clamp()) and always renders exactly as tall as the heading text next to it, at any viewport
   width, with no separate breakpoint math needed. Rounded-square per explicit ask (both source
   PNGs are plain full-bleed squares with no rounding of their own — the radius is purely a CSS
   clip here). vertical-align:middle plus a small negative margin-top is an optical nudge — a
   square icon set to the text's cap-height via 1em still visually reads as sitting a hair too low
   next to a capital letter's baseline, this brings it back in line. */
.product-showcase-logo {
  display: inline-block; width: 1em; height: 1em;
  margin-right: 0.3em; margin-top: -0.12em;
  border-radius: 0.22em; vertical-align: middle; object-fit: cover;
}
/* Tried as an alternative to the icon+text combo above, per an explicit "let's see how this
   looks" ask — Gem Space/Gem Team's full vector wordmark lockup (icon + "gem space"/"gem team"
   in one SVG, white text baked in for a dark background) replaces the plain-text heading
   entirely rather than sitting next to it. .product-showcase-title--logo strips the text-sizing
   properties (font-size/weight/letter-spacing are meaningless on an <h2> containing only an
   <img> now) and just centers the image within the same flex slot .product-showcase-title
   already had (flex:1 1 40% etc., untouched). Height is its own clamp() rather than reusing the
   text heading's em-based sizing, since there's no font-size on this element to tie 1em to
   anymore — tuned to roughly the same visual weight the text heading had. */
/* justify-content:center (mobile) / flex-start (768px+, matching .gem-showcase-header's own
   text-align:center→left breakpoint) — text-align has no effect on a flex child, so the image
   needs its own explicit alignment to stay centered on mobile / left-edge-aligned on desktop the
   same way the plain-text heading did via text-align. */
.product-showcase-title--logo { display: flex; align-items: center; justify-content: center; }
@media (min-width: 768px) { .product-showcase-title--logo { justify-content: flex-start; } }
.product-showcase-logo-full { display: block; height: clamp(2rem, 4.2vw, 2.75rem); width: auto; }
.product-showcase-text { margin-top: 1rem; font-size: 0.9375rem; line-height: 1.6; color: var(--color-text-secondary); max-width: 34rem; }

.gem-showcase-header { display: flex; flex-direction: column; gap: 1.5rem; text-align: center; }
@media (min-width: 768px) { .gem-showcase-header { flex-direction: row; align-items: flex-start; gap: 2rem; text-align: left; } }
.gem-showcase-header .product-showcase-title { flex: 1 1 40%; }
.gem-showcase-header .product-showcase-text { flex: 1 1 60%; margin-top: 0; max-width: none; }

/* Gem Space, Gem Team, and White Label all use this two-column split (heading + 4-item icon
   feature list + action buttons on the left, product visual on the right) — originally built
   for Gem Space from a supplied reference design, then applied to Gem Team and (structure-only,
   per an explicit ask) White Label too, for a consistent product-page pattern across all three
   sections. The reference's lime-green accent is swapped for the site's own violet
   (--color-accent-violet — same hue as the hero's ::before glow) to keep the brand color
   consistent. White Label's .gs-visual is intentionally left empty (no .gs-visual-card/image
   inside) — a placeholder right column until real graphics are supplied. */
/* No margin-top here — .projects's own section padding is the only spacing layer above the
   first block (same single-layer pattern as .features-heading-block above, whose only offset
   is its section's padding). .projects-list's flex gap handles spacing between blocks. */
/* gap fixed at 1.5rem to match .features-grid's own gap between bento cards — same spacing
   language between the two card families. */
.gs-split { display: flex; flex-direction: column; gap: 1.5rem; }
@media (min-width: 1024px) { .gs-split { flex-direction: row; align-items: stretch; } }

/* Same card shell recipe as .feature-card (bento cards) — border-radius/border/background/padding
   — so the two halves of this layout read as part of the same card system as the rest of the
   site. No hover state on purpose (unlike .feature-card's border-brighten): these aren't
   interactive tiles, just structural containers. */
/* gap separates .gs-content's 3 direct children — header (title+subtext), feature list,
   action buttons — doubled per an explicit ask to space those 3 sections further apart. */
/* overflow:hidden — without it, some browsers (Safari in particular) can render a sliver of the
   solid background peeking past the rounded corner at certain sizes/zoom levels (reported: a
   small square notch outside the curve at the top-left corner). overflow:hidden forces the
   background to actually clip to the rounded box instead of just trusting border-radius alone.
   Same fix applied to every other rounded card on the site that was missing it — see
   .wl-bento-card and .section-badge below, and .gs-visual-image's own border-radius further down. */
.gs-content {
  flex: 1 1 60%; min-width: 0; display: flex; flex-direction: column; gap: 5rem;
  border-radius: var(--radius-card-lg); border: 1px solid rgba(255, 255, 255, 0.1);
  background: #0a0a0a; padding: 3rem; overflow: hidden;
}
.gs-content-header { display: flex; flex-direction: column; gap: 1rem; text-align: left; }
/* Doubled (1rem→2rem) for Gem Space/Gem Team only, per explicit ask — this is the gap between the
   logo lockup and the paragraph text below it. Scoped by id so White Label's .gs-content-header
   (still a badge + plain-text heading, no logo swap) keeps the original 1rem. */
#gem-space .gs-content-header, #gem-team .gs-content-header { gap: 2rem; }
/* The B2C/B2B badge (removed, then brought back per a follow-up ask) gets its OWN extra
   margin-bottom on top of the shared 2rem flex gap above — "a decent distance from the logo"
   specifically, not just the same spacing the logo already has to the paragraph below it. Also
   scoped to Gem Space/Gem Team only, matching the gap override right above. */
#gem-space .section-badge, #gem-team .section-badge { margin-bottom: 1rem; }
/* White Label's badge→heading gap matched to the same 48px (2rem gap + 1rem margin, measured via
   getBoundingClientRect) that Gem Space/Gem Team's badge→logo gap uses above — White Label's own
   .gs-content-header gap stayed the un-doubled 1rem (16px), so its badge needed its own bigger
   margin-bottom (2rem, not 1rem) to land on the same total distance despite the smaller base gap. */
#white-label .section-badge { margin-bottom: 2rem; }
.gs-content-header .product-showcase-title { flex: none; }
/* No max-width — the subtext now fills the card's full content width instead of being capped
   narrower than its container. */
.gs-content-header .product-showcase-text { flex: none; margin-top: 0; max-width: none; }
.gs-content > .product-showcase-actions { margin-top: 0; }

/* margin-top: -2.5rem cancels half of .gs-content's 5rem flex gap above this element only —
   halves just the header→feature-list gap per an explicit ask, without touching the
   feature-list→actions gap below it (that one's still the full 5rem, untouched). */
.gs-feature-list { display: grid; grid-template-columns: 1fr; gap: 2rem; margin-top: -2.5rem; }
@media (min-width: 560px) { .gs-feature-list { grid-template-columns: 1fr 1fr; gap: 2rem; } }
/* Header (icon + title inline) then text below — same content structure as .feature-card-header
   on the bento cards, so these read as the same family of component. */
/* padding+negative margin of equal size (1rem) keeps each item's visible position/spacing in
   the grid exactly as before — the extra box only becomes visible as hover background, it
   doesn't shift layout. No border (per explicit ask), just a background lighten + rounded
   corners so the whole icon+title+subtitle block reads as one hoverable card. */
.gs-feature {
  display: flex; flex-direction: column; gap: 0.75rem;
  padding: 1rem; margin: -1rem; border-radius: var(--radius-card);
  transition: background-color var(--duration-default) var(--ease-default);
}
.gs-feature:hover { background: rgba(255, 255, 255, 0.06); }
.gs-feature-header { display: flex; align-items: center; gap: 0.5rem; }
/* Plain small white icon — no chip: no background, no border, no glow. */
.gs-feature-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; color: #fff; }
.gs-feature-title { font-size: 1.0625rem; font-weight: 600; color: #fff; }
.gs-feature-text { font-size: 0.875rem; line-height: 1.55; color: var(--color-text-secondary); }

.gs-visual {
  flex: 1 1 40%; min-height: 0; display: flex; justify-content: center;
  border-radius: var(--radius-card-lg); border: 1px solid rgba(255, 255, 255, 0.1);
  background: #0a0a0a; padding: 1.5rem; overflow: hidden;
}
/* ROOT CAUSE FIX (graphics vanishing in Gem Space/Gem Team below 1024px): the whole
   height:100% chain described in the next comment (.gs-visual-card → .gs-visual-image) only
   has something real to resolve against because .gs-split's align-items:stretch (row mode,
   ≥1024px) gives .gs-visual a definite height equal to .gs-content's. Below 1024px, .gs-split
   is flex-direction:column — stretch's cross-axis there is WIDTH, not height, so .gs-visual's
   height goes back to being "auto" (content-driven). Since .gs-visual-card deliberately has NO
   in-flow content (that's the whole point of the trick below), that auto height collapses to
   ~0 (just this box's own padding), and the absolutely-positioned image's height:100% then
   resolves against that same ~0, i.e. the screenshot silently shrinks to nothing — not
   display:none, so easy to miss in a DOM inspector, but effectively invisible. Giving .gs-visual
   itself an aspect-ratio here supplies a real, non-content-dependent height in column mode,
   the same way the row-mode stretch does above 1024px.
   Scoped to Gem Space/Gem Team only (by id), NOT White Label: White Label's .wl-bento cards
   have real in-flow text content (title+description), so its .gs-visual was never at risk of
   the collapse above — it was already sizing itself correctly from that content. Forcing the
   same 4:3 ratio on it too (an earlier pass did this uniformly) just clamped its height below
   what its own 4 mini-cards actually need, squeezing their text — regressed exactly because it
   didn't need this fix in the first place. */
@media (max-width: 1023px) {
  #gem-space-panel .gs-visual, #gem-team-panel .gs-visual { aspect-ratio: 4 / 3; }
}
/* Mobile section-boundary fix: on every "zigzag" desktop layout the image/content order
   alternates by DOM order (Gem Space: content then visual; Gem Team: visual then content — no
   flex `order` involved, purely source order). Below 1024px that meant the page read as
   [Gem Space content][Gem Space image][Gem Team image][Gem Team content] — two image blocks
   landing back to back with no text between them, making it unclear where one section ends and
   the next begins. `order:-1` forces .gs-visual first in EVERY section regardless of its DOM
   position, so each section always reads image-then-text on mobile — matches an explicit ask,
   and applies uniformly (harmless no-op for Gem Team, which was already visual-first). */
@media (max-width: 1023px) { .gs-visual { order: -1; } }
/* Same card shell as .gs-content above (see comment there) — the screenshot sits inside it,
   sized to match .gs-content's (the heading + 4-item list + buttons column) real rendered height.
   .gs-visual-image is taken out of flow (position:absolute) specifically so it can NOT
   contribute its own intrinsic aspect-ratio-driven height to .gs-split's align-items:stretch
   calculation — stretch sets every row item's cross-size to the tallest item's own *unstretched*
   height, and a portrait screenshot at ~480px wide has an unstretched height (~1040px) that's
   taller than the text column, so leaving it in-flow made the image dictate the row's height
   instead of following it (the actual bug behind an earlier "image way too big" report — verified
   via getBoundingClientRect in a throwaway test page, not guessed). With the image absolutely
   positioned, .gs-visual-card has no in-flow content of its own, so it contributes ~0 to the
   stretch calculation — .gs-content's natural height wins, and height:100% then carries that
   real number back down to size the image via object-fit:contain (no distortion, no overflow). */
.gs-visual-card { position: relative; width: 100%; max-width: 480px; height: 100%; }

/* White Label only (not Gem Space/Gem Team, which stay 60/40) — text column shrunk and the
   .wl-bento graphic column grown to an even 50/50 split, per an explicit ask. Scoped by id
   since .gs-content/.gs-visual's base flex-basis (60%/40%) is shared across all three sections. */
#white-label.gs-split > .gs-content { flex-basis: 50%; }
#white-label.gs-split > .gs-visual { flex-basis: 50%; }
/* border-radius: inherit — this <img> already relies on .gs-visual's overflow:hidden to get
   rounded corners, but combining a mask-image (the fade below) with a parent's border-radius
   clip is a known Safari weak spot: it can fail to round the image itself, leaving a small
   square corner poking past the parent's curve (reported at this element specifically). Setting
   the same radius directly on the image is the standard fix — it clips correctly even if the
   parent's overflow-hidden clip alone doesn't catch it. */
.gs-visual-image {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  border-radius: inherit;
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
}
/* Gem Team — its wider video-call screenshot was letterboxing with visible empty gaps above/below
   inside .gs-visual's own padding, per an explicit "make it fill the full height" ask.
   object-fit:cover fills the box completely (cropping the sides instead of leaving top/bottom
   gaps) while keeping .gs-visual's own padding untouched. */
/* object-position: left, not center — cover crops horizontally here (the box is proportionally
   narrower/taller than the screenshot), and a centered crop was cutting the macOS traffic-light
   dots off the left edge of the source image. Anchoring left keeps that edge intact and crops
   from the right side instead, which has less distinctive content to lose. */
/* Adds a right-edge fade on top of the existing bottom fade (inherited from the shared
   .gs-visual-image rule above — that one stays untouched, per an explicit "keep the bottom
   darkening as it is" ask). Two mask-image gradients (vertical + horizontal) layered together
   and combined with mask-composite:intersect (+ the -webkit- equivalent, source-in) so a spot
   only stays fully visible where BOTH gradients say "opaque" — i.e. it's alpha-multiplied, not
   just added. Without intersect, a second mask-image layer would replace the first outright
   instead of combining with it. linear-gradient(to right, ...) — not "to left" — since the
   solid/opaque stop needs to sit at the LEFT edge (0%) with the fade approaching the RIGHT edge
   (100%); "to left" would do the reverse and fade out the wrong side. */
#gem-team-panel .gs-visual-image {
  object-fit: cover; object-position: left top;
  -webkit-mask-image:
    linear-gradient(to bottom, #000 78%, transparent 100%),
    linear-gradient(to right, #000 58%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to bottom, #000 78%, transparent 100%),
    linear-gradient(to right, #000 58%, transparent 100%);
  mask-composite: intersect;
}

/* Gem Space — its taller/narrower phone screenshot was letterboxing horizontally (empty gaps
   left/right inside .gs-visual's padding) with object-fit:contain, per an explicit "fill the
   frame by width" ask. Same cover fix as Gem Team above, but the overflow here is VERTICAL
   instead of horizontal (this screenshot's aspect ratio is taller relative to its box, the
   opposite mismatch from Gem Team's), so cropping happens top/bottom, not left/right.
   object-position:top keeps the phone's status bar intact and pushes all the cropping to the
   bottom — which is exactly where the fade mask below is also strengthened (per the same ask),
   so the crop blends into that fade rather than reading as an abrupt cut. Mask start pulled way
   earlier (78%→48%) and its endpoint pulled inward too (100%→90%) for a visibly stronger,
   longer fade than the shared base rule (which Gem Team still uses unmodified). */
#gem-space-panel .gs-visual-image {
  object-fit: cover; object-position: center top;
  -webkit-mask-image: linear-gradient(to bottom, #000 48%, transparent 90%);
  mask-image: linear-gradient(to bottom, #000 48%, transparent 90%);
}

/* White Label's .gs-visual placeholder graphic — a static 3-card mini bento (no auto-playing
   motion, per explicit ask) filling the same slot Gem Space/Gem Team use for a screenshot.
   Left card spans both grid rows (tall), the two right cards stack — matches the top-left
   corner of the supplied reference bento layout. `.wl-bento`'s own width/height:100% overrides
   .gs-visual's `justify-content:center` (no free space left to center within), and .gs-split's
   `align-items:stretch` still gives it the same real-content-driven height as .gs-content. */
.wl-bento {
  width: 100%; height: 100%;
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 1rem;
}
/* border-radius: --radius-card (16px), not --radius-card-lg (24px, used by the outer .gs-visual
   frame around these three) — these inner cards are much smaller than that outer frame, so the
   same 24px radius read as noticeably rounder relative to their own size. The smaller token
   keeps them proportionate to the outer card's corner, per an explicit ask. */
.wl-bento-card {
  display: flex; flex-direction: column; gap: 0.75rem;
  border-radius: var(--radius-card); background: rgba(255, 255, 255, 0.03); padding: 1.25rem;
  overflow: hidden;
}
.wl-bento-card-tall { grid-row: 1 / 3; }
.wl-bento-graphic { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; color: #fff; }
/* Serif accent scoped to ONLY the big "Aa" glyph (not a sitewide font change, and per a
   follow-up ask, not even this card's own title/text — those stay the site's standard Inter).
   Loaded alongside Inter in index.html's Google Fonts link (now with the italic style too,
   needed for the cycle below). */
/* Cycles the glyph through 4 distinct type styles (serif upright, serif italic, sans bold,
   monospace) to visualize "adaptive typography" in motion — per an explicit ask. This is the
   one deliberate exception to the rest of the White Label graphic being static (asked for by
   name). `steps(1, end)` as the timing function forces a hard instant cut at each 25% boundary
   instead of any perceived blend — no transform/skew at all (per an explicit correction: italic
   is the only real "slant" a font has, there's no such thing as a left-leaning typeface, so the
   earlier skewX(-8deg) fake-italic effect was wrong and has been removed). Respects the sitewide
   prefers-reduced-motion rule (top of this file) automatically, like every other animation here. */
.wl-bento-graphic-type {
  font-size: clamp(5rem, 8vw, 8rem); line-height: 1;
  font-family: 'Playfair Display', serif; font-style: normal; font-weight: 700;
  animation: wl-type-cycle 8s steps(1, end) infinite;
}
@keyframes wl-type-cycle {
  0% { font-family: 'Playfair Display', serif; font-style: normal; font-weight: 700; }
  25% { font-family: 'Playfair Display', serif; font-style: italic; font-weight: 700; }
  50% { font-family: 'Inter', sans-serif; font-style: normal; font-weight: 800; }
  75% { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-style: normal; font-weight: 700; }
  100% { font-family: 'Playfair Display', serif; font-style: normal; font-weight: 700; }
}
.wl-bento-graphic-layout { flex-direction: column; align-items: stretch; justify-content: center; gap: 0.5rem; }
/* Each bar's right edge drifts smoothly in and out, left edge pinned — per an explicit ask
   ("слева не меняются, а справа ползают"). transform:scaleX (not animating width) so the
   underlying box stays 100% and the layout never reflows — transform-origin:left is what makes
   it read as "the right edge moving" instead of the bar shrinking toward its own center.
   The "random" feel is faked with per-bar different duration + a negative delay (starts each
   bar mid-cycle instead of all three launching from scaleX(1) in sync) — cheap and fully CSS,
   no JS/Math.random needed for a decorative loop like this. */
/* Violet gradient (base accent → its lighter tint, same pair .hero-visual-glow already uses)
   instead of flat translucent white, per explicit ask — gradient runs toward the drifting right
   edge, so the bar reads as "brightening toward where it's reaching" rather than a plain color
   swap. */
.wl-layout-bar {
  flex: 1; min-height: 18px; border-radius: 8px;
  background: linear-gradient(90deg, var(--color-accent-violet), #a78bfa);
  transform-origin: left center;
  animation: wl-layout-bar-drift 3.6s ease-in-out infinite;
}
.wl-layout-bar:nth-child(1) { animation-duration: 3.2s; animation-delay: -0.4s; }
.wl-layout-bar:nth-child(2) { animation-duration: 4.1s; animation-delay: -2.1s; }
.wl-layout-bar:nth-child(3) { animation-duration: 3.7s; animation-delay: -1.3s; }
@keyframes wl-layout-bar-drift {
  0% { transform: scaleX(1); }
  30% { transform: scaleX(0.78); }
  55% { transform: scaleX(0.93); }
  80% { transform: scaleX(0.85); }
  100% { transform: scaleX(1); }
}
/* Laptop + phone pair, sized so a viewer reads them as roughly real-world-proportional devices
   (laptop noticeably wider than the phone is tall) rather than two same-size icons — per an
   explicit ask.
   Each <svg>'s viewBox in the markup is cropped tight to its own drawn artwork (no shared
   "0 0 24 24" canvas with baked-in empty margin) — the laptop's base used to sit higher inside
   its old 24-unit box than the phone's rect did inside its own, so bottom-aligning the two
   *boxes* left the two *drawings* sitting on visibly different lines even with the boxes lined
   up. With tight viewBoxes, each box's bottom edge IS the artwork's bottom edge, so aligning the
   two icons to each other (align-items on this flex row) aligns the real devices, not just their
   canvases — they read as standing on one shared invisible surface.
   Width/height below are sized to each icon's own (now different) viewBox aspect ratio — forcing
   both into equal square boxes is exactly what caused the tight-crop viewBoxes to letterbox
   instead of fill. stroke-width is deliberately DIFFERENT per icon (0.44 vs 0.8): stroke-width is
   defined in viewBox user-units, and the laptop renders at a much bigger px-per-unit scale than
   the phone (76px over a 21-unit box vs 26px over a 13-unit box) — the same raw stroke-width
   number would render visibly thicker on the laptop. These two values were picked to both land
   at ~1.6px of actual on-screen stroke, so the pair reads as drawn with the same pen.
   Vertical position of the pair, as a unit, within the card: this element is itself the flex:1
   spacer between the card's top padding and .wl-bento-copy below, so plain align-items:center
   would NOT give equal top/bottom distance — the card's own padding (1.25rem/20px) is bigger
   than the gap to the copy block below (0.75rem/12px), so a pure center sits 8px too high
   relative to the two outer landmarks (card top edge vs "Адаптивность" title) the user actually
   measures against. padding-bottom:8px shrinks the area align-items centers within, from the
   bottom only, shifting the visual mid-point down by exactly that gap-vs-padding difference —
   the algebra cancels out this card's actual height and the icons' own height, so it stays
   correct regardless of language-toggle text reflow or future resizing. */
.wl-bento-graphic-mobile { align-items: center; padding-bottom: 8px; gap: 0.625rem; flex-wrap: wrap; }
.wl-bento-icon-laptop { width: 76px; height: 58px; flex-shrink: 0; stroke-width: 0.44; }
.wl-bento-icon-phone { width: 26px; height: 42px; flex-shrink: 0; stroke-width: 0.8; }
.wl-bento-copy { display: flex; flex-direction: column; gap: 0.375rem; }
.wl-bento-title { font-size: 1.0625rem; font-weight: 600; color: #fff; }
.wl-bento-text { font-size: 0.8125rem; line-height: 1.5; color: var(--color-text-secondary); }

.product-showcase-actions { display: flex; gap: 0.75rem; margin-top: 2rem; flex-wrap: wrap; }

/* Closing CTA banner, between the "DevTeam Group" section and the footer. Same page bg as its
   neighbors (--color-bg-about) so the section itself doesn't add a seam — the panel card is
   what separates it visually, same shell language as .feature-card (#0a0a0a fill, subtle white
   border, --radius-card-lg) plus one soft violet glow so it doesn't read as just another
   plain bento card. Top/bottom padding intentionally copies .features/.about's own values
   (not just eyeballed) so the gap to the previous section matches every other section-to-
   section gap on the page (each side always contributes this same padding). */
/* Small stylistic divider between the "DevTeam Group" section and the CTA banner — the full
   logo lockup (2220×274px source, plenty of resolution to stretch to the full container width
   without pixelating) spanning the same width as every other section's content. Same page bg
   as its neighbors so it reads as a beat inside the same dark stretch of the page, not a new
   section — same convention as .cta-banner/.projects/.about all sharing --color-bg-about. */
.brand-strip { background: var(--color-bg-about); padding: 2.5rem 0; overflow: hidden; }
@media (min-width: 1024px) { .brand-strip { padding: 4rem 0; } }
/* Recolored violet + resting at a low, "slightly visible" opacity, per explicit ask — plain
   <img> replaced with a single mask-image-on-a-filled-div (same logo.png alpha channel used as
   a CSS mask, so the color painted underneath is exactly --color-accent-violet regardless of the
   source PNG's own white pixels — same technique already used elsewhere on this page, e.g. the
   Gem Team image fade masks). A cursor-tracked "flashlight" hover reveal was built and then
   removed again per a follow-up ask — back to one flat, always-the-same-opacity layer, no JS
   involved. Same aspect-ratio as the source PNG (2220×274) so the box is sized identically to
   the original <img>. */
.brand-strip-logo { display: block; width: 100%; position: relative; }

/* Invisible hover target, sized to roughly the glyph's own height plus a little slack — not the
   full (now 3×-tall) box. Without this, hovering anywhere in the tall breathing-room padding
   around the letters would trigger the cascade too, which read as a way-too-generous hit area.
   Positioned absolutely + centered over .brand-strip-logo-cascade (a sibling, not an ancestor,
   so it doesn't intercept clicks meant for anything else); :has() below reads this element's own
   :hover state to drive both the cascade transform and the opacity boost, since a plain :hover
   on the outer box can't be un-scoped to just this smaller area any other way. */
.brand-strip-logo-hit {
  position: absolute; left: 0; right: 0; top: 50%; height: 45%; transform: translateY(-50%);
  z-index: 1;
}

/* Hover cascade, ported from a "TextReveal"-style component (per-character flip-on-hover: each
   char is an overflow-hidden 1em window holding one span, a text-shadow duplicate offset 1em
   below it standing in for the "next" copy, translateY(-1em) on hover swaps them with a
   per-character stagger delay). Not real text here, so the text-shadow trick doesn't apply —
   adapted to a wide logo image by slicing it into 12 equal-width columns (.brand-strip-logo-slice,
   analog of "characters"), each an overflow-hidden window holding two stacked identical copies of
   just that column's slice of the logo (.brand-strip-logo-slice-copy ×2, same violet mask
   technique as before, cropped to 1/12 of the artwork via mask-size/mask-position sprite math).
   Hovering the whole logo shifts every column's pair up by one window-height in sync, staggered
   by `--i` exactly like the source component's `i * staggerDelay` — the visible result is a wave
   of motion sweeping left-to-right across the wordmark. Pure CSS, no JS: unlike character count,
   the slice count is fixed at 12 so the per-slice --i and its mask offset are baked into the
   HTML (inline custom property) instead of computed at runtime. */
/* Height vs. the logo's own natural aspect (274 → 685, same 2220 width — 2.5×, pulled back down
   from an earlier 3× per explicit ask to narrow the visible container a bit) — this is the
   "runway" the letters travel/fade through, deliberately taller than the glyphs themselves
   (see .brand-strip-logo-slice-copy below, which locks the glyph back to its true, unstretched
   size and centers it inside this taller box). Sizing the window to the glyph's own height 1:1
   forced the fade mask to eat into the letters at rest and made the slide feel cramped/abrupt —
   so this stays a real multiple of glyph height, just a smaller one than before. */
.brand-strip-logo-cascade { display: flex; width: 100%; aspect-ratio: 2220 / 685; }
/* The sliding window itself gets a vertical fade mask — without it, `overflow: hidden` cuts
   letters off with a hard straight edge the instant they cross the top/bottom of the box mid-
   slide (looked like a visible "frame" the letters ducked behind, per explicit ask). This mask
   layers on top of the slice's own shape+wipe masks below (applied to a different element,
   so no extra mask-composite layer needed here) — content just fades to transparent approaching
   either edge instead of being clipped. Widened again (20%/80% → 26%/74% → 34%/66%) per repeated
   "blur it more" asks — past ~27% this necessarily starts softening the very top/bottom of the
   glyph itself at rest too (its own span is the middle ~40% of the box), which is the deliberate
   trade for a visibly stronger feather rather than a ceiling to avoid; a very faint edge-soften
   at rest reads fine against the already-low 0.12 resting opacity. */
.brand-strip-logo-slice {
  position: relative; flex: 1 1 0; height: 100%; overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent 0%, #000 34%, #000 66%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 34%, #000 66%, transparent 100%);
}
/* Now just a positioning context for the two absolutely-positioned copies below — no longer
   sized/doubled itself, so it no longer ties "how far apart the two copies sit" to "how tall the
   window is". Those used to be the same number (a stacked-flex pair filling a 200%-tall box
   forces spacing = 1 full window-height) — per explicit ask, decoupled: the window stayed tall
   (for the fade breathing room), but the *travel distance* between the two copies now comes from
   its own --flip-distance value, independent of window height. */
.brand-strip-logo-slice-inner { position: relative; height: 100%; --flip-distance: 72%; }
/* mask-size width 1200% = 12 slices × 100%, same horizontal sprite math as before (selects each
   slice's 1/12 column via mask-position's X value). Height is `auto` — preserves the source PNG's
   own intrinsic aspect ratio at that computed width, so the glyph renders at its true, correct
   size regardless of how tall this box is, instead of being stretched to fill it. mask-position's
   Y value (50%) centers that true-sized glyph vertically within each copy's own (window-height)
   box, so at rest there's real transparent headroom above/below it. */
.brand-strip-logo-slice-copy {
  position: absolute; left: 0; width: 100%; height: 100%;
  background-color: var(--color-accent-violet);
  mask-image: url('images/logo.png'); mask-repeat: no-repeat; mask-size: 1200% auto;
  mask-position: calc(var(--i, 0) * 100% / 11) 50%;
  -webkit-mask-image: url('images/logo.png'); -webkit-mask-repeat: no-repeat; -webkit-mask-size: 1200% auto;
  -webkit-mask-position: calc(var(--i, 0) * 100% / 11) 50%;
  transition: transform 480ms ease-in-out;
  transition-delay: calc(var(--i, 0) * 40ms);
}
/* --flip-distance (set above, on .brand-strip-logo-slice-inner): how far the pair travels —
   deliberately well under the window height (was effectively 100% of it before), so the
   departing/arriving letter don't read as far apart. The real floor isn't just "keep copy 2
   hidden at rest" (flip-distance + 50% + half the glyph's own height ≥ 100%) — the same math
   applies symmetrically to copy 1 landing fully out of view after it exits on hover, and shrinking
   the window in the same pass that tuned the fade widened the glyph's share of it, so a value
   that cleared both margins before (65%) started leaving a faint sliver of the departed glyph
   inside the fade zone post-transition. 72% clears both with a couple points to spare — confirmed
   no ghosting at rest OR settled-hover, at this window size. */
.brand-strip-logo-slice-copy:first-child { top: 0; }
.brand-strip-logo-slice-copy:last-child { top: var(--flip-distance); }
.brand-strip-logo:has(.brand-strip-logo-hit:hover) .brand-strip-logo-slice-copy {
  transform: translateY(calc(-1 * var(--flip-distance)));
}
@media (prefers-reduced-motion: reduce) {
  .brand-strip-logo-slice-copy { transition: none; }
}
/* At rest this sits at the existing .reveal-up.is-visible opacity (0.12 — "slightly visible"
   watermark, see below); bumped up on hover so the cascade itself is actually perceptible
   instead of playing out almost invisibly. */
.brand-strip-logo.reveal-up.is-visible:has(.brand-strip-logo-hit:hover) { opacity: 0.4; transition: opacity 300ms ease; }
/* Registered as a .reveal-up target in script.js (fades up from opacity:0 on scroll into view,
   same mechanism used sitewide). Resting opacity is 0.12, not the generic .reveal-up.is-visible's
   own 1 — this 3-class selector outweighs that 2-class one, so it wins without touching the
   shared rule; reads as "slightly visible" against the dark background per explicit ask.
   Pushed noticeably further than .reveal-up's shared defaults (16px/500ms, opacity+transform
   only) — a first pass at 40px/700ms still read as too subtle on this full-width, comparatively
   short strip. Own full `transition` (not just -duration) so `filter` can join opacity/transform
   as a third animated property: blur(6px)→0 sells "materializing out of the dark" on top of the
   rise, without touching width/height/scale anywhere — no dimension changes, per explicit ask,
   just position + clarity + opacity. */
.brand-strip-logo.reveal-up {
  transform: translateY(64px); filter: blur(6px);
  transition: opacity 1100ms ease-out, transform 1100ms ease-out, filter 1100ms ease-out;
}
.brand-strip-logo.reveal-up.is-visible { opacity: 0.12; transform: translateY(0); filter: blur(0); }

/* Rectangular bordered panel removed per explicit ask — the copy now sits directly on the
   section's own background (--color-bg-about, the same dark shared by every neighboring
   section), with no boxed card/outline around it. The two glow blobs moved out with it: same
   corner placement (bottom-left / top-right) as the old clipped-ring version, but now built as
   plain blurred radial-gradient circles — same recipe as .projects-glow — sitting straight on
   .cta-banner's own background with no overflow:hidden/clip needed, mirroring how
   .projects-glow-left/-right already bleed off the edges of the Projects section. */
.cta-banner { position: relative; background: var(--color-bg-about); padding: 4rem 0 3rem; }
@media (min-width: 1024px) { .cta-banner { padding: 6rem 0 6rem; } }
.cta-banner > .container { position: relative; z-index: 1; }
.cta-banner-copy { display: flex; flex-direction: column; align-items: center; text-align: center; }
.cta-banner-glow {
  position: absolute; z-index: 0; pointer-events: none;
  width: 32vw; height: 32vw; max-width: 500px; max-height: 500px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--color-accent-violet), transparent 70%);
  filter: blur(6rem); opacity: 0.35;
}
.cta-banner-glow-bl { left: 0; bottom: 0; transform: translate(-58%, 58%); }
.cta-banner-glow-tr { right: 0; top: 0; transform: translate(58%, -58%); }
@media (min-width: 1024px) { .cta-banner-glow { filter: blur(9rem); } }
.cta-banner-heading {
  font-size: clamp(1.5rem, 3.4vw, 2.25rem); font-weight: 600; letter-spacing: -0.01em; color: #fff;
}
/* max-width widened twice now (42rem→49rem→58rem) per explicit asks — was still reading too
   narrow/cramped for how wide this panel is. */
.cta-banner-text {
  margin-top: 1rem; max-width: 58rem;
  font-size: 1rem; line-height: 1.6; color: var(--color-text-secondary);
}
.cta-banner-button {
  margin-top: 3rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.875rem 1.75rem; border-radius: 10px;
  background: #fff; color: var(--color-ink); font-size: 0.9375rem; font-weight: 600;
  transition: transform 200ms var(--ease-default), background-color 200ms ease;
}
.cta-banner-button:hover { background: #e6e6e6; transform: translateY(-2px); }

.site-footer { background: var(--color-bg-about); padding: 0; }
.footer-frame { color: #9ca3af; padding: 3rem 0; }
@media (min-width: 1024px) { .footer-frame { padding: 5.25rem 0; } }
.footer-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1.5rem; }
.footer-brand { display: flex; align-items: center; gap: 0.75rem; }
.footer-nav { display: flex; align-items: center; gap: 1.75rem; flex-wrap: wrap; }
.footer-nav a { font-size: 0.875rem; color: #9ca3af; }
.footer-nav a:hover { text-decoration: underline; }

.footer-divider {
  display: flex; flex-direction: column; gap: 1.5rem;
  margin-top: 1.5rem; padding-top: 1.5rem;
}
@media (min-width: 1024px) {
  .footer-divider { display: grid; grid-template-columns: repeat(10, 1fr); gap: 0; margin-top: 1rem; padding-top: 2rem; }
  .footer-legal { grid-column: 4 / 11; grid-row: 1 / 3; align-self: end; margin-top: 0; }
  .footer-copyright { grid-column: 1 / 4; grid-row: 1 / 3; margin-top: 0; }
}
.footer-legal-list { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; }
@media (min-width: 1024px) { .footer-legal-list { justify-content: flex-end; } }
.footer-legal-list a { font-size: 0.875rem; color: #9ca3af; }
.footer-legal-list a:hover { text-decoration: underline; }

.footer-copyright { font-size: 0.875rem; line-height: 1.6; }

/* APPEND_MARKER */
