/* ==========================================================================
   Component: Menu/Footer/Links (Figma node 691:5446)

   States (Figma property "Active"): Home, Games, Videos, More.
   Exactly one link is active at a time — active link uses --color-lime-01,
   inactive links use --color-light-01. No Hover/Focus variant exists on
   this component in Figma, so none is invented here.
   ========================================================================== */

.footer-menu__list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-menu__link {
  display: block;
  height: 26px;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
  text-decoration: none;
  cursor: pointer;
  /* "Magnetic" hover — not Figma-sourced, a custom micro-interaction
     requested on top of this real Figma component (which itself defines
     no Hover variant, per the comment above). */
  will-change: transform, color;
  transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1), color 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}

.footer-menu__link.is-active {
  color: var(--color-lime-01);
}

.footer-menu__link:hover,
.footer-menu__link:focus-visible {
  color: var(--color-lime-01);
  transform: translateY(-4px);
}

/* ==========================================================================
   Component: Menu/Footer/Connect (Figma node 99:534)

   Single state — this is a plain COMPONENT, not a COMPONENT_SET like
   Menu/Footer/Links. No Active/Hover/Press variants exist in Figma.
   Three external links (LinkedIn, Email, UpWork), all --color-light-01,
   no color change on any of them.
   ========================================================================== */

.footer-connect__list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-connect__link {
  display: block;
  height: 26px;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
  text-decoration: none;
  cursor: pointer;
  /* "Magnetic" hover — not Figma-sourced, a custom micro-interaction
     requested on top of this real Figma component (single-state, no
     Hover variant, per the comment above). */
  will-change: transform, color;
  transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1), color 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}

.footer-connect__link:hover,
.footer-connect__link:focus-visible {
  color: var(--color-lime-01);
  transform: translateY(-4px);
}

/* ==========================================================================
   Component: Navigation (Figma node 691:5535)

   States (Figma property "Active"): Nav_Home, Nav_Games, Nav_Video, Nav_More.
   Pill nav bar with a sliding glow indicator behind the current item. The
   current item has no link (matches Figma: it's a <div>, not an <a>) and
   renders a larger, differently-colored icon (Default 32px/Light_01-outline
   vs Press 40px/Lime_01-fill) — separate SVG assets per state, exported
   from Figma as-is, not recolored via CSS.

   A hidden "About" nav item exists in Figma (hidden="true", no reactions) —
   not rendered here since it isn't visible in the design.
   ========================================================================== */

.nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 264px;
  height: 64px;
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: 80px;
  overflow: hidden;
  /* CORRECTION: BACKGROUND_BLUR radius in Figma is 40, but CSS blur()/
     backdrop-filter take Figma's radius / 2 (confirmed against Figma's own
     Dev Mode CSS output on the Card component — see Card_001 below). The
     20px Tailwind originally gave was actually right; my earlier "fix" to
     40px here was wrong — reverted. Unlike this, DROP_SHADOW blur-radius
     (the box-shadow lines below) needs no such conversion. */
  backdrop-filter: blur(20px);
  /* Border is a gradient too (verified via Plugin API strokes, not a solid
     Lime_01 as get_design_context's Tailwind conversion had simplified it
     to). Plain CSS border-color can't take a gradient, so: transparent
     border + 3 background layers, the last one clipped to border-box so
     it shows through as the border color.
     Stop 2 (Light_01) fades to 0 alpha on both fill and border gradients —
     verified directly on the paint stops, not held at the same % as stop 1. */
  background-image:
    linear-gradient(180deg, rgba(198, 242, 78, 0.05) 0%, rgba(241, 252, 214, 0) 100%),
    linear-gradient(var(--color-black-01), var(--color-black-01)),
    linear-gradient(180deg, rgba(198, 242, 78, 0.1) 0%, rgba(241, 252, 214, 0) 100%);
  background-origin: padding-box, padding-box, border-box;
  background-clip: padding-box, padding-box, border-box;
  box-shadow:
    0px 1px 1px 0px rgba(0, 0, 0, 0.04),
    0px 2px 3px 0px rgba(0, 0, 0, 0.12),
    0px 6px 9px 0px rgba(0, 0, 0, 0.6);
}

.nav__indicator {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 56px;
  height: 56px;
  border-radius: 32px;
  background: var(--color-light-01);
  opacity: 0.1;
  transform: translate(-50%, -50%) translateX(var(--nav-indicator-offset, 0px));
  pointer-events: none;
  /* Global Navigation system — not Figma-sourced, a custom interaction.
     `transition` is scoped to exactly the property that could change this
     element's position — the --nav-indicator-offset custom property,
     consumed by translateX() inside `transform` above. (Deliberately not
     transitioning `left`/`width` too: this element's `left` is a static
     50% and its `width` a static 56px, neither ever changes, so a
     transition on either would be dead CSS.) Left in place as a harmless
     same-document fallback, but the real cross-page slide now comes from
     view-transition-name below, not from this transition firing — see
     that comment for why. */
  will-change: transform;
  transition: transform 0.42s cubic-bezier(0.25, 1, 0.5, 1);
}

/* Cross-document "shared element" transition — not Figma-sourced, a
   custom interaction. Site architecture is 4 separate physical pages
   (index/games/video/more, see main.css's @view-transition rule), so
   there's no live DOM to animate a CSS transition on across a navigation
   — the old page's JS/DOM is gone by the time the new page paints.
   Naming this element gives the browser's own cross-document view
   transition engine the job instead: matched by this same name on every
   page (each page's own .nav[data-active] rule sets a different real
   --nav-indicator-offset, so the two snapshots are genuinely at different
   positions), the browser captures the indicator's geometry on the
   outgoing page and on the incoming page and animates
   ::view-transition-group(nav-indicator) between them automatically — no
   JS, no manual FLIP bridging required. Tuned to the same duration/easing
   the old JS-driven version used, so the motion itself is unchanged.
   Named elements are excluded from their ancestor's ::view-transition-
   old/new(root) snapshot (rendered as their own layer instead), so the
   indicator stays crisp and keeps sliding while the rest of the page
   blurs/fades beneath it — confirmed via Playwright, not assumed. */
@supports (view-transition-name: root) {
  .nav__indicator {
    view-transition-name: nav-indicator;
  }

  ::view-transition-group(nav-indicator) {
    animation-duration: 0.42s;
    animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
  }

  /* Same plus-lighter default as root (see main.css) — confirmed present
     here too via Playwright (document.getAnimations() still listed
     -ua-mix-blend-mode-plus-lighter on this element's own old/new pair).
     Likely imperceptible on a translucent (opacity:0.1) circle this small,
     but reset for the same reason it mattered on root: correctness, not
     guessing that "probably fine" holds. */
  ::view-transition-old(nav-indicator),
  ::view-transition-new(nav-indicator) {
    mix-blend-mode: normal;
  }
}

/* Indicator offsets from center — literal values from Figma (Nav/Indicator/Default x per state) */
.nav[data-active="home"] .nav__indicator  { --nav-indicator-offset: -100px; }
.nav[data-active="games"] .nav__indicator { --nav-indicator-offset: -52px; }
.nav[data-active="video"] .nav__indicator { --nav-indicator-offset: -6px; }
.nav[data-active="more"] .nav__indicator  { --nav-indicator-offset: 44px; }

.nav__item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 4px;
  border-radius: 40px;
  cursor: pointer;
  text-decoration: none;
}

.nav__icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 4px;
  border-radius: 12px;
}

.nav__icon-btn img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Current item's icon-button is larger (Press state, 40px vs 32px) */
.nav__item.is-current .nav__icon-btn {
  width: 40px;
  height: 40px;
}

/* Video icon does not stretch to fill its box in Figma — it renders at its
   own natural 23x23 size. Default: asymmetric padding (pl-4 pr-2 py-4),
   top-left aligned (verified via absoluteBoundingBox, not centered).
   Press: back to symmetric 4px padding + centered (verified: ~centered
   within the 40px box, sub-pixel off due to Figma's own inset rounding). */
.nav__icon-btn--video {
  padding: 4px 2px 4px 4px;
  align-items: flex-start;
  justify-content: flex-start;
}

.nav__icon-btn--video img {
  width: 23px;
  height: 23px;
}

.nav__item.is-current .nav__icon-btn--video {
  padding: 4px;
  align-items: center;
  justify-content: center;
}

/* Active Video icon is a proper 32x32 frame that exactly fills its content
   box (verified via fresh exportAsync) — same as Home/Games/More actives,
   unlike Default which is a smaller 23x23 glyph. Overrides the fixed 23px
   sizing above back to fill/contain. */
.nav__item.is-current .nav__icon-btn--video img {
  width: 100%;
  height: 100%;
}

.nav__divider-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 48px;
}

.nav__divider {
  width: 24px;
  height: 1px;
  border-radius: 4px;
  background: var(--color-light-01);
  opacity: 0.88;
  transform: rotate(90deg);
}

.nav__link {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border-radius: 40px;
  cursor: pointer;
  text-decoration: none;
}

/* ==========================================================================
   Component: Button/Arrow (Figma node 319:6563)

   States (Figma property "Property 1"): Default, Hover.
   Figma reaction on Default is ON_HOVER -> swap to Hover component (no
   transition specified = instant swap, no animation invented here).
   Figma reaction on Hover is ON_CLICK -> URL "" (empty) — the designer
   has not set a real destination yet, so no href/action is added here.
   ========================================================================== */

.btn-arrow {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 4px 8px;
  border: 1px solid var(--color-lime-01);
  border-radius: 80px;
  background: var(--color-black-01);
  /* Verified via raw effect data (node.effects): 3x DROP_SHADOW, radius
     9/3/1 (not halved — get_design_context's Tailwind output had this
     wrong). Identical on both Default and Hover, so this filter is not
     overridden on hover below. */
  filter:
    drop-shadow(0px 6px 9px rgba(0, 0, 0, 0.64))
    drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.16))
    drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.08));
  cursor: pointer;
  /* Bug fix, not Figma-sourced: this button's own direct parent
     (.game-card__button-wrap and its .video-card/.more-card equivalents)
     gets overflow:hidden at Tablet/Desktop — needed there for an
     unrelated reason (collapsing the hover-reveal panel to a true zero
     height; removing it would break that), so it can't just become
     overflow:visible. The largest drop-shadow above (offset-y 6px + blur
     9px) needs ~15px of clearance to render uncropped, more than the
     wrap's own tight-fitting box was leaving around this 48px button.
     margin here grows the flex item's own footprint inside that wrap
     (identical in both Default and Hover, since the filter itself
     doesn't change between them) without touching the wrap's own
     padding, which the collapse math above depends on staying exact. */
  margin: 15px;
}

.btn-arrow__icon {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translate(-50%, -50%) rotate(-90deg) scaleY(-1);
}

.btn-arrow__icon img {
  display: block;
}

.btn-arrow__icon--default img {
  width: 23.537px;
  height: 22.999px;
}

.btn-arrow__icon--hover {
  opacity: 0;
}

.btn-arrow__icon--hover img {
  width: 32px;
  height: 32px;
}

/* Hover state — border grows 1px -> 4px (solid Lime_01 both states, confirmed
   via strokes, not a gradient). Background becomes the same gradient 01
   already used elsewhere (Lime_01 0% -> Black_01 100%, both @30% — verified
   stop positions are 0/100, not 81.25% as previously assumed from Tailwind
   output). Shadow is untouched: identical DROP_SHADOW effects as Default,
   so no filter/box-shadow override here at all. */
.btn-arrow:hover,
.btn-arrow.is-hover {
  border: 4px solid var(--color-lime-01);
  background: var(--color-gradient-01);
}

.btn-arrow:hover .btn-arrow__icon--default,
.btn-arrow.is-hover .btn-arrow__icon--default {
  opacity: 0;
}

.btn-arrow:hover .btn-arrow__icon--hover,
.btn-arrow.is-hover .btn-arrow__icon--hover {
  opacity: 1;
}


/* ==========================================================================
   Section: Hero (Home page — Figma "Hero" frame, all 3 breakpoints:
   Desktop 200:2651, Tablet 201:2876, Mobile 193:1602)

   Mobile-first: base rules below are Mobile (356–695px), then
   @media (min-width: 696px) overrides for Tablet, then
   @media (min-width: 1344px) overrides for Desktop — see main.css for the
   breakpoint convention (tablet threshold has moved a few times: 701px →
   700px → current 696px).

   "Vas" panel background/border/shadow/blur reuse the exact same recipe
   already verified on Navigation and the Card components (gradient-01
   border via mask-composite, White_01-based radial sheen, same 3x
   DROP_SHADOW numbers, BACKGROUND_BLUR radius/2 for CSS blur()) —
   confirmed identical here via raw Plugin API, not assumed from reuse.

   Found and fixed two pre-existing token bugs while verifying this
   section's typography (see variables.css): Heading/Tag and
   Heading/SectionTitle letter-spacing are PERCENT of font-size, not flat
   px as originally recorded — get_variable_defs's string output doesn't
   preserve the unit, and I didn't check it directly the first time.

   Text content ("Vasja StankoviC") is transcribed literally from Figma,
   including the non-standard capital C — not "corrected" to a ć, per
   "нічого не вигадуй".

   CORRECTION PASS: re-verified Image/WelcomeFrame/TagFrame/NameFrame via
   raw absoluteBoundingBox (not get_design_context's Tailwind numbers,
   which were consistently 2–3px short on every inset across all 3
   breakpoints). Real insets: 5px mobile, 8px tablet+desktop (was 3/6px).
   Also found Tailwind's padding was directional-only on Tablet/Desktop
   (e.g. Welcome = padding-right only) while Mobile is uniform 8px — my
   first pass had layered the directional override on top of Mobile's
   uniform padding instead of resetting the other sides, leaving a stray
   8px. Fixed by writing full padding shorthand at each tier.

   RE-SYNC (Figma edited after the above): Mobile WelcomeFrame top is 94px,
   not 96px — reverted. Explore now carries a real ON_CLICK reaction on
   Desktop (Tablet/Mobile still have none) targeting node 46:262
   "Card/Teaser/Games" — the Games teaser card in Projects, not built yet.
   Explore is now an <a href="#games-card">; #games-card must be added as
   an id when Projects is built. Applied the link to all 3 breakpoints
   (Figma only wired Desktop) since a missing Tablet/Mobile reaction reads
   as an incomplete prototype wiring, not an intentional per-breakpoint
   difference — flagged to the user, not decided silently.

   RE-SYNC #2 (Vas only): Desktop's Vas subtree was restructured in Figma —
   WelcomeFrame moved inside Hero/Image (auto-layout right-align/v-center)
   and TagFrame+NameFrame got wrapped in a new "Frame 2" (auto-layout,
   gap:40px). Tablet/Mobile untouched. Kept the existing independent
   absolute-position markup (didn't rebuild the DOM to mirror the new
   wrapper nesting) since it reproduces the identical visual result with
   less structural churn — recomputed from the new geometry:
     Welcome: top 131->101px, added explicit height:566px (padding-driven
       auto-height no longer applies now that the text overflows the
       frame's own top/bottom padding — Figma just centers it regardless).
     Tag: top 454->456px.
     Name: padding-bottom 40->16px (its own auto-height shrinks, and since
       Name is bottom-anchored at 8px unchanged, this alone reproduces the
       new 40px gap between Tag and Name — no separate gap value needed).
   Vas's own fills/strokes/effects/cornerRadius — all unchanged.

   RE-SYNC #3: same "WelcomeFrame moved inside Hero/Image" restructure
   turned out to affect Tablet and Mobile too (only checked Desktop last
   time). Tag/Name unaffected on Tablet/Mobile — verified their boxes
   still match exactly, no change needed there. Welcome fixes:
     Mobile: top 94->22px, added explicit height:296px (was previously
       relying on 8px uniform padding + auto-height; content now exactly
       fills a padded box of top/bottom:80px, but since content(136px)
       fills it exactly, explicit height + centering reproduces the same
       result without needing to encode the redundant padding).
     Tablet: top 243->143px, width 84->93px, added explicit height:482px
       (same reasoning — content exactly fills the padded box, so
       explicit height + centering is equivalent and simpler).
   Also found (not new — an original oversight, re-surfaced while
   re-checking BioRow for this pass): Tablet's Hero/BioRow has
   primaryAxisAlignItems: MAX (bottom-packed) in Figma, which was never
   implemented — added justify-content: flex-end.

   STRUCTURAL REFACTOR (.hero__panel/__image/__welcome/__tag/__name renamed
   to a flat .vas/__image/__welcome/__caption — requested to fix an overlap
   bug caused by rotate(-90deg)+manual width/height swap and to remove the
   nesting). No new Figma query was made for this pass; every offset below
   is the same verified geometry from the RE-SYNC notes above, algebraically
   collapsed into the new shape:
     .vas__welcome now uses top:0/bottom:0 + flex align-items:center instead
       of an explicit top+height pair — safe because the old top/height were
       already symmetric around the panel's vertical center at all 3
       breakpoints (confirmed by the RE-SYNC #3 math). Its `right` value is
       the old `right` + `padding-right` collapsed into one, since the box is
       now auto-width (only `right` is set, no `left`/`width`) and shrinks to
       the text's own footprint: 5+8=13px (mobile), 8+32=40px (tablet),
       8+40=48px (desktop, `right` itself inherited from tablet).
     .vas__welcome-text swaps transform:rotate(-90deg) + manual width/height
       for writing-mode:vertical-rl (no pre-sized box needed — this is the
       actual bug fix). vertical-rl reads top-to-bottom by default, which is
       the opposite direction of the original rotate(-90deg) (bottom-to-top),
       so transform:rotate(180deg) is added on top to restore the original
       reading direction.
     .vas__caption merges Tag+Name into one flex column (they were already
       Figma's "Frame 2" grouping, see RE-SYNC #2). `left`/`bottom` are the
       old `left`/`bottom` + matching `padding-left`/`padding-bottom`
       collapsed the same way as Welcome: left 5+8=13px / 8+32=40px /
       8+40=48px; bottom 5+8=13px / 8+32=40px / 8+16=24px (mobile/tablet/
       desktop). `gap` is the real distance between Tag's text box and
       Name's text box, back-computed from their old absolute positions +
       padding (padding is no longer a spacer between them, gap replaces
       it): 16px mobile, 67px tablet, 40px desktop — the differing tablet
       value is a genuine Figma difference between tiers, not an error.
     max-width:74% on .vas__caption is a new safety constraint (not from
       Figma) per explicit request, since Tag/Name text stays nowrap and is
       short enough at every breakpoint to never actually reach it.

   RE-SYNC #4 (all 3 breakpoints checked independently via absoluteBoundingBox
   — per the new project rule that Desktop/Tablet/Mobile are separate designs,
   not scaled copies; nothing here was assumed from another tier):
     Figma restructured the Vas subtree again: WelcomeFrame is now a sibling
     of a single "Hero/Vas/Text" wrapper (containing TagFrame+NameFrame) —
     this now matches the flat .vas structure directly, so no further HTML
     restructuring was needed, only numbers changed:
     .vas (panel) height on Desktop moved to 716px, then bounced back to
       768px in a re-check moments later (same value as Tablet, but kept as
       an explicit override rather than removed, since it's clearly still
       being actively edited in Figma) — Tablet stays 768px throughout,
       unchanged.
     .vas__welcome `right` dropped to match the panel's own inset exactly
       (the old +padding-right collapsing no longer applies — Figma removed
       that extra padding): 5px mobile (was 13px), 8px tablet (was 40px),
       8px desktop (was 48px). top:0/bottom:0 vertical symmetry re-verified,
       still holds at all 3 tiers.
     .vas__caption on Tablet only: Tablet's Tag now renders 3 lines
       ("PUSHING/THE RIGHT/BUTTONS"), same as Mobile, not the old 2-line
       Desktop-style text — consistent with the hero-tag__break--responsive
       toggle already showing that break on Tablet. The extra line pushes
       Tag's text further down, shrinking the gap to Name: left 32px (was
       40px), bottom 41px (was 40px), gap 32px (was 67px, computed against
       the old 2-line Tag). Mobile and Desktop caption numbers were
       re-verified against fresh absoluteBoundingBox data and matched the
       existing CSS exactly — no change there.
   ========================================================================== */

hero,
.hero {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 0 24px;
  background: var(--color-black-01);
}

.hero__navbar {
  display: flex;
  justify-content: center;
  padding: 24px 0 8px;
  /* Explicit stacking-order guard requested alongside the Hero/NameFrame
     kinetic-blur animation below, so the growing/blurring name text can
     never paint over the nav regardless of how the animation is tuned
     later — position:relative is required for z-index to take effect. */
  position: relative;
  z-index: 50;
}

.vas {
  position: relative;
  width: 100%;
  height: 340px;
  border-radius: 24px;
  overflow: hidden;
  background: radial-gradient(50% 50% at 50% 75%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

/* Gradient border, same mask-composite technique as Navigation/Cards */
.vas::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.vas__image {
  position: absolute;
  inset: 5px;
  border-radius: 20px;
  overflow: hidden;
}

.vas__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.vas__welcome {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.vas__welcome-text {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  text-transform: uppercase;
  font-family: var(--font-display-hero-mobile-family);
  font-size: var(--font-display-hero-mobile-size);
  font-weight: var(--font-display-hero-mobile-weight);
  line-height: var(--font-display-hero-mobile-line-height);
  letter-spacing: var(--font-display-hero-mobile-letter-spacing);
  color: var(--color-light-01);
  text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.12), 0px 1px 1px rgba(0, 0, 0, 0.04);
  -webkit-text-stroke-width: 1px;
  -webkit-text-stroke-color: var(--color-black-01);
}

/* Scroll-driven slide-in — Hero/WELCOME (all 3 breakpoints)

   Not a Figma-sourced effect — a custom scroll interaction. The real base
   transform here is `rotate(180deg)` (writing-mode:vertical-rl already
   reads top-to-bottom, so rotate(180deg) restores the original bottom-to-
   top reading direction — see the RE-SYNC comment above .vas__welcome),
   not the rotate(-90deg) the spec assumed, so translateY's sign is solved
   for THIS real transform rather than copied from the spec: with
   transform-origin at the element's own center, `rotate(180deg)
   translateY(-50px)` (translate applied first, then flipped 180°) lands
   the glyphs 50px below rest — the requested "shifted down" initial
   state — and animating the translateY term to 0 slides them back up to
   the real rotate(180deg) rest position. Verified in-browser (not just
   worked out on paper) before treating this as correct.

   Entirely gated behind @supports(animation-timeline: view()) — a
   browser without support keeps the plain rotate(180deg)/opacity:1 rule
   above, not a permanently-faded fallback.

   Uses a NAMED timeline (view-timeline-name on .vas, referenced by
   .vas__welcome-text below) instead of the bare `view()` function — found
   this the hard way: `view()` binds to the element's own nearest scroll
   CONTAINER ancestor, and .vas's `overflow: hidden` (real, needed for its
   rounded-corner image clipping, not something to remove) already
   qualifies as one. That silently made .vas itself the "scroller" — which
   never scrolls — so the timeline was permanently pre-resolved to 100%
   from the very first paint regardless of real page scroll. Confirmed via
   an isolated repro (opacity stuck at 1 across every scroll position with
   overflow:hidden present, correctly progressive once removed). Naming
   the timeline on .vas and pointing .vas__welcome-text at that name by
   reference decouples "what scrolls" from "what's overflow-clipped." */
@supports (animation-timeline: view()) {
  .vas {
    view-timeline-name: --vas-welcome-visibility;
  }

  .vas__welcome-text {
    opacity: 0.3;
    transform: rotate(180deg) translateY(-50px);
    will-change: transform, opacity;
    animation-name: vas-welcome-slide-in;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --vas-welcome-visibility;
    animation-range: entry 0% contain 30%;
  }

  @keyframes vas-welcome-slide-in {
    from {
      opacity: 0.3;
      transform: rotate(180deg) translateY(-50px);
    }
    to {
      opacity: 1;
      transform: rotate(180deg) translateY(0);
    }
  }
}

.vas__caption {
  position: absolute;
  left: 13px;
  bottom: 13px;
  max-width: 74%;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Mobile/Tablet have a 3rd manual line break ("PUSHING / THE RIGHT /
   BUTTONS") that Desktop doesn't ("PUSHING / THE RIGHT BUTTONS") — real
   distinct line-breaking per breakpoint in Figma, not a width-driven wrap.
   One <br> is toggled instead of shipping two separate text blocks; the
   space before it in the markup ("THE RIGHT <br ...>BUTTONS") keeps the
   desktop reading "THE RIGHT BUTTONS" with a real space once the <br> is
   hidden. */
.hero-tag__break--responsive {
  display: inline;
}

@media (min-width: 1344px) {
  .hero-tag__break--responsive {
    display: none;
  }
}

.vas__tag p {
  margin: 0;
  text-transform: uppercase;
  white-space: nowrap;
  font-family: var(--font-heading-tag-mobile-family);
  font-size: var(--font-heading-tag-mobile-size);
  font-weight: var(--font-heading-tag-mobile-weight);
  line-height: var(--font-heading-tag-mobile-line-height);
  letter-spacing: var(--font-heading-tag-mobile-letter-spacing);
  color: var(--color-black-01);
  /* Fade-in + slide, page-load only — not Figma-sourced, a custom load
     sequence timed to land right as the Hero/NameFrame kinetic-blur
     animation above (VASJA STANKOVIC, see .vas__name-word) finishes
     clearing. Settles at the real Figma opacity (0.88, unchanged at every
     breakpoint), not a fabricated 1. */
  opacity: 0;
  transform: translateY(10px);
  will-change: opacity, transform;
  animation-name: vas-tag-fade-in;
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
  animation-delay: 1.2s;
  animation-fill-mode: forwards;
}

@keyframes vas-tag-fade-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 0.88;
    transform: translateY(0);
  }
}

.vas__name p {
  margin: 0;
  text-transform: uppercase;
  white-space: nowrap;
  font-family: var(--font-display-hero-mobile-family);
  font-size: var(--font-display-hero-mobile-size);
  font-weight: var(--font-display-hero-mobile-weight);
  line-height: var(--font-display-hero-mobile-line-height);
  letter-spacing: var(--font-display-hero-mobile-letter-spacing);
  color: var(--color-light-01);
  text-shadow: 0px 6px 9px rgba(0, 0, 0, 0.6), 0px 2px 3px rgba(0, 0, 0, 0.12);
}

/* ==========================================================================
   Kinetic Variable Font Blur — Hero/NameFrame ("VASJA STANKOVIC"), on load

   Not a Figma-sourced effect — a custom load-in animation, requested with
   an explicit technical spec. Runs as a plain CSS `animation` (no JS):
   an animation with no scroll-timeline autoplays as soon as the element is
   rendered, which for markup present in the initial HTML already lands at
   page load, satisfying "спрацьовує при DOMContentLoaded" without adding a
   JS trigger this element doesn't otherwise need.

   .vas__name p keeps controlling the real typography (font tokens, color,
   text-shadow) unchanged above. The animation's `to` keyframe restores
   letter-spacing to that same real --font-display-hero-mobile-letter-
   spacing token (identical value at all 3 breakpoints) rather than a
   fabricated "normal"/0 endpoint — a property only present in one of
   from/100% holds that single value for the whole animation instead of
   interpolating toward the underlying cascade, so the token has to be
   repeated explicitly in `to`, not left to inherit implicitly.
   display:inline-block is required for transform:scale to apply reliably
   to inline text. */
.vas__name-word {
  display: inline-block;
  opacity: 0;
  filter: blur(30px);
  transform: scale(0.88);
  letter-spacing: -0.05em;
  animation-name: vas-name-kinetic-blur;
  animation-duration: 1.2s;
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  animation-fill-mode: forwards;
  will-change: opacity, filter, transform, letter-spacing;
}

.vas__name-word--second {
  animation-delay: 0.15s;
}

@keyframes vas-name-kinetic-blur {
  from {
    opacity: 0;
    filter: blur(30px);
    transform: scale(0.88);
    letter-spacing: -0.05em;
  }
  to {
    opacity: 1;
    filter: blur(0px);
    transform: scale(1);
    letter-spacing: var(--font-display-hero-mobile-letter-spacing);
  }
}

@media (prefers-reduced-motion: reduce) {
  .vas__name-word {
    animation: none;
    opacity: 1;
    filter: none;
    transform: none;
    letter-spacing: var(--font-display-hero-mobile-letter-spacing);
  }
}

.hero__bio-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
}

/* Figma's "Hero/BioText" is a redundant single-child wrapper around
   "Frame 2" (the real vertical stack, itemSpacing 16) — flattened here,
   same technique already used for Partners/Footer's redundant wrappers.
   Real text content is 3 separate Figma text layers now (was previously
   ported as 5 <p>s with margin-bottom spacing): an intro line, one body
   block that itself contains 4 paragraphs joined by blank lines inside a
   single text layer (reproduced via white-space:pre-line on
   .hero__bio-text-body rather than inventing extra markup/gap values for
   a per-layer spacing Figma doesn't define), and a closing line. */
.hero__bio-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  width: 100%;
  /* All 3 text layers have textAlignHorizontal:LEFT in Figma (confirmed
     via raw Plugin API) — explicit here rather than relying on the LTR
     default, since it's a real, verified property. */
  text-align: left;
}

.hero__bio-text-intro,
.hero__bio-text-outro {
  margin: 0;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
}

/* Mobile-only real font: 14px/20px (--font-body-small-*), not the 16/26
   --font-body-base-* used everywhere else — confirmed via raw fontSize/
   lineHeight on this specific text node, distinct from Tablet/Desktop's
   own copy of the same text (see media queries below). */
.hero__bio-text-body {
  margin: 0;
  white-space: pre-line;
  font-family: var(--font-body-small-family);
  font-size: var(--font-body-small-size);
  font-weight: var(--font-body-small-weight);
  line-height: var(--font-body-small-line-height);
  letter-spacing: var(--font-body-small-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

/* ==========================================================================
   Scroll-driven Text Inversion — Hero/BioText (all 3 breakpoints)

   Not a Figma-sourced effect — a custom scroll interaction, requested with
   an explicit technical spec: native `animation-timeline: view()` only, no
   JS. Applies identically at Mobile/Tablet/Desktop since it targets the
   same .hero__bio-text-intro/-body/-outro selectors already shared across
   all 3 breakpoints (only their font tokens change per breakpoint above).

   Technique: no background-clip/stroke tricks (an earlier version used
   those and looked bad in testing) — just the text's own `color` animated
   via color-mix from 10% alpha to fully opaque --color-light-01, alongside
   a blur that clears at the same rate. `background: transparent` here is
   the literal "прозоре тло" from the spec — it's also CSS's default for a
   <p>, made explicit since the initial-state requirement calls it out.

   .hero__bio-text-body keeps its own real Figma opacity:0.6 (set above,
   untouched) — element opacity and this color-mix alpha compose rather
   than fight, so body settles at 60% of full Light_01 like it always did,
   just arrived at via the same reveal as the other two lines.

   Entirely gated behind @supports(animation-timeline: view()) — a browser
   without support renders the plain, fully legible text set above (no
   permanently-blurred/faded fallback state invented for it). */
@supports (animation-timeline: view()) {
  .hero__bio-text-intro,
  .hero__bio-text-body,
  .hero__bio-text-outro {
    background: transparent;
    color: color-mix(in srgb, var(--color-light-01) 10%, transparent);
    filter: blur(4px);
    will-change: color, filter;
    animation-name: hero-bio-text-fill;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 10% contain 40%;
  }

  @keyframes hero-bio-text-fill {
    from {
      color: color-mix(in srgb, var(--color-light-01) 10%, transparent);
      filter: blur(4px);
    }
    to {
      color: var(--color-light-01);
      filter: blur(0);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .hero__bio-text-intro,
    .hero__bio-text-body,
    .hero__bio-text-outro {
      animation: none;
      color: var(--color-light-01);
      filter: none;
    }
  }
}

/* Same size/shape at every breakpoint — verified identical Desktop/Tablet/Mobile */
/* Label + arrow are packed to the left with 0 gap (Figma:
   primaryAxisAlignItems MIN, itemSpacing 0, paddingLeft/Right 0) — not
   spread apart via space-between. Confirmed identical on all 3 breakpoints
   via raw Plugin API (itemSpacing/padding same everywhere), so the box's
   fixed 156px width leaves real empty space after the arrow by design,
   rather than pushing it to the right edge. */
.hero__explore {
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  gap: 0;
  width: 156px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 80px;
  background: var(--color-black-01);
  cursor: pointer;
  text-decoration: none;
}

.hero__explore-label {
  font-family: var(--font-ui-button-family);
  font-size: var(--font-ui-button-size);
  font-weight: var(--font-ui-button-weight);
  line-height: var(--font-ui-button-line-height);
  letter-spacing: var(--font-ui-button-letter-spacing);
  color: var(--color-lime-01);
}

.hero__explore-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  transform: scaleY(-1);
}

.hero__explore-arrow img {
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  hero,
  .hero {
    gap: 24px;
    padding: 0 64px;
  }

  .vas {
    height: 768px;
    border-radius: 40px;
  }

  .vas__image {
    inset: 8px;
    border-radius: 32px;
  }

  .vas__welcome {
    right: 8px;
  }

  .vas__welcome-text {
    font-size: var(--font-display-hero-tablet-size);
    line-height: var(--font-display-hero-tablet-line-height);
  }

  .vas__caption {
    left: 32px;
    bottom: 41px;
    gap: 32px;
  }

  .vas__tag p {
    font-size: var(--font-heading-tag-tablet-size);
    line-height: var(--font-heading-tag-tablet-line-height);
    letter-spacing: var(--font-heading-tag-tablet-letter-spacing);
  }

  .vas__name p {
    font-size: var(--font-display-hero-tablet-size);
    line-height: var(--font-display-hero-tablet-line-height);
  }

  .hero__bio-row {
    gap: 64px;
    justify-content: flex-end;
  }

  /* Real fixed width (was FILL on Mobile) — confirmed via
     layoutSizingHorizontal:FIXED on Figma's Hero/BioText, identical
     584px on both Tablet and Desktop. */
  .hero__bio-text {
    width: 584px;
  }

  /* Body block's real font matches everyone else's --font-body-base-*
     here (16/26), unlike Mobile's smaller --font-body-small-* copy of
     the same text. */
  .hero__bio-text-body {
    font-family: var(--font-body-base-family);
    font-size: var(--font-body-base-size);
    font-weight: var(--font-body-base-weight);
    line-height: var(--font-body-base-line-height);
    letter-spacing: var(--font-body-base-letter-spacing);
  }

  /* "The craft lies..." jumps to a real 24px/118.75%-line-height font at
     Tablet+Desktop (Mobile keeps the small 16/26 body size) — numerically
     identical to --font-body-email-mobile-* (confirmed via raw fontSize/
     lineHeight), reused despite the token's unrelated name per this
     project's established convention of matching by real numbers, not by
     token name. */
  .hero__bio-text-outro {
    font-family: var(--font-body-email-mobile-family);
    font-size: var(--font-body-email-mobile-size);
    font-weight: var(--font-body-email-mobile-weight);
    line-height: var(--font-body-email-mobile-line-height);
    letter-spacing: var(--font-body-email-mobile-letter-spacing);
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  hero,
  .hero {
    gap: 40px;
    padding: 0 100px;
  }

  .vas {
    height: 768px;
  }

  .vas__welcome {
    right: 8px;
  }

  .vas__welcome-text {
    font-size: var(--font-display-hero-desktop-size);
    line-height: var(--font-display-hero-desktop-line-height);
  }

  .vas__caption {
    left: 48px;
    bottom: 24px;
    gap: 40px;
  }

  .vas__tag p {
    font-size: var(--font-heading-tag-desktop-size);
    line-height: var(--font-heading-tag-desktop-line-height);
    letter-spacing: var(--font-heading-tag-desktop-letter-spacing);
  }

  .vas__name p {
    font-size: var(--font-display-hero-desktop-size);
    line-height: var(--font-display-hero-desktop-line-height);
  }

  /* Updated after a live Figma edit (re-verified via raw Plugin API):
     paddingRight dropped from 160px to 0 — the row now packs everything
     to the left (primaryAxisAlignItems:MIN) instead of leaving a large
     empty gap on the right. paddingLeft stays 48px, still real/confirmed.
     .hero__bio-text keeps its own fixed 584px width from the Tablet rule
     above rather than flex-growing.

     justify-content:flex-start is explicit here, not left to inherit —
     Tablet's rule below sets justify-content:flex-end (correct there,
     where flex-direction is still column, so it means "push to the
     bottom"). That Tablet media query keeps applying at Desktop widths
     too (min-width:696px stays true at 1344px+), so once flex-direction
     flips to row here, flex-end would silently mean "push to the right"
     instead unless overridden — that was the real bug the whole row
     shifted right, not a text-align issue. */
  .hero__bio-row {
    flex-direction: row;
    justify-content: flex-start;
    align-items: flex-end;
    gap: 64px;
    padding-left: 48px;
    padding-right: 0;
  }
}

/* ==========================================================================
   Section: Projects (Home page — Figma "Projects" frame, all 3 breakpoints:
   Desktop 911:69262, Tablet 201:2968, Mobile 193:1697)

   Checked all 3 breakpoints independently via raw Plugin API (per the
   project's Figma-independence rule — see main.css) rather than assuming
   Tablet/Desktop share Mobile's layout:
     - Mobile: "Column" is a VERTICAL auto-layout (cards stacked, gap 16px,
       full-width cards, fixed height 256px).
     - Tablet + Desktop: "Column" is HORIZONTAL with overflowDirection:
       HORIZONTAL and clipsContent:false — a real Figma horizontal-scroll
       prototype, not a wrap/grid. Cards are a fixed 640x480 at both tiers
       (identical numbers — confirmed via absoluteBoundingBox, not assumed
       from one tier) — so this renders as a non-wrapping flex row with
       overflow-x:auto and flex-shrink:0 cards, not a CSS grid.

   Card ("Card/Teaser/Games|Video|More") recipe, verified via raw
   fills/strokes/effects (get_design_context's own Tailwind output
   flattened the gradient stroke to a solid Lime_01, the same known
   flattening bug seen before — raw Plugin API confirmed it's really
   var(--color-gradient-01), same as .card/.vas):
     - Background: radial-gradient(100% 100% at 50% 0%, Light_01 0.2 / 0.02)
       — same ellipse position/size as the existing .card's white sheen,
       just recolored to Light_01 (confirmed via SVG gradientTransform
       decode, not assumed from .card).
     - Border: var(--color-gradient-01) via the same ::before mask-composite
       technique as .card/.vas/Navigation.
     - Shadow/blur: identical 3x DROP_SHADOW + BACKGROUND_BLUR radius 24
       (→ blur(12px), same /2 rule) as every other glass panel in this
       project.
     - cornerRadius: 40 desktop/tablet, 24 mobile (real, distinct — not the
       same as the unrelated .card component's fixed 40).
     - Card/Image cornerRadius: 32 desktop/tablet, 20 mobile.
     - Title uses the shared --font-heading-cardtitle-* token (desktop AND
       tablet — confirmed identical fontSize/lineHeight/letterSpacing at
       both tiers) falling back to --font-heading-cardtitle-mobile-* on
       Mobile; Category line uses --font-body-base-* unchanged at every
       breakpoint (confirmed identical). Both are existing tokens, nothing
       new added to variables.css.
     - Heading "Projects" maps exactly to --font-heading-sectiontitle-*
       (all 3 tiers matched byte-for-byte on size/lineHeight/letterSpacing).
     - Real ON_CLICK reactions on Desktop/Tablet navigate to
       Page/Games/Desktop, Page/Video/Desktop, Page/More/Desktop
       respectively (confirmed by resolving the destination node names) —
       mapped to games.html/video.html/more.html. Mobile's reactions exist
       but have a null destinationId (broken in Figma); applied the same
       games.html/video.html/more.html links there too, since a missing
       link on one tier while the other two clearly intend real navigation
       reads as incomplete prototype wiring, not an intentional
       mobile-only difference (same judgment call already made once for
       Hero's Explore button).
   ========================================================================== */

projects,
.projects {
  display: block;
  background: var(--color-black-01);
  padding: 64px 24px;
}

.projects__left {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.projects__heading {
  margin: 0;
  font-family: var(--font-heading-sectiontitle-mobile-family);
  font-size: var(--font-heading-sectiontitle-mobile-size);
  font-weight: var(--font-heading-sectiontitle-mobile-weight);
  line-height: var(--font-heading-sectiontitle-mobile-line-height);
  letter-spacing: var(--font-heading-sectiontitle-mobile-letter-spacing);
  color: var(--color-light-01);
}

.projects__row {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Tablet/Desktop-only horizontal-scroll-hijack effect for the Column row —
   requested directly (Apple-case-study style), not a Figma-fidelity
   translation; replaces the native overflow-x:auto scroll that used to be
   on .projects__row at these breakpoints. At Mobile these two wrappers
   drop out of the box model entirely (display:contents) so .projects__row
   stays a direct flex child of .projects__left, unaffected — same
   contents-collapse technique used for .games-main__pair etc. */
.projects__hscroll,
.projects__hscroll-sticky {
  display: contents;
}

.project-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 256px;
  padding: 4px;
  border-radius: 24px;
  overflow: hidden;
  text-decoration: none;
  background: radial-gradient(100% 100% at 50% 0%, rgba(241, 252, 214, 0.2) 0%, rgba(241, 252, 214, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

/* Gradient border, same mask-composite technique as .card/.vas/Navigation */
.project-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.project-card__content {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  gap: 8px;
}

.project-card__image {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  border-radius: 20px;
  overflow: hidden;
}

.project-card__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-card__meta {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 8px 16px;
}

.project-card__icon {
  display: block;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  opacity: 0.88;
}

.project-card__title-group {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-width: 0;
  gap: 12px;
}

.project-card__title {
  margin: 0;
  font-family: var(--font-heading-cardtitle-mobile-family);
  font-size: var(--font-heading-cardtitle-mobile-size);
  font-weight: var(--font-heading-cardtitle-mobile-weight);
  line-height: var(--font-heading-cardtitle-mobile-line-height);
  letter-spacing: var(--font-heading-cardtitle-mobile-letter-spacing);
  color: var(--color-lime-01);
}

.project-card__category {
  margin: 0;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  /* Bottom padding split off and reduced (was 80px uniform) — most of that
     space now lives inside .projects__hscroll-sticky instead, as room for
     the cards' own box-shadow (see comment there); the section's own
     bottom gap to Services shrinks rather than stacking on top of it. */
  .projects {
    padding: 80px 64px 32px;
  }

  .projects__left {
    gap: 24px;
  }

  .projects__heading {
    font-family: var(--font-heading-sectiontitle-tablet-family);
    font-size: var(--font-heading-sectiontitle-tablet-size);
    font-weight: var(--font-heading-sectiontitle-tablet-weight);
    line-height: var(--font-heading-sectiontitle-tablet-line-height);
    letter-spacing: var(--font-heading-sectiontitle-tablet-letter-spacing);
  }

  .projects__row {
    flex-direction: row;
    gap: 24px;
    /* Row's own width must equal the sum of its cards, not shrink to fit
       .projects__hscroll-sticky's clipped viewport — otherwise there's
       nothing for translateX() in js/main.js to reveal by scrolling. */
    width: max-content;
    will-change: transform;
  }

  /* .projects__hscroll's height is set in px by js/main.js (to the row's
     real scrollWidth, per spec) — it only needs position:relative here so
     the sticky child below pins correctly within it. */
  .projects__hscroll {
    display: block;
    position: relative;
  }

  /* Pins in place while the tall .projects__hscroll wrapper scrolls past;
     overflow:hidden clips the row so its off-screen cards don't force
     page-level horizontal scroll or stay visible past this section's own
     width. padding-bottom gives .project-card's own box-shadow (offset
     8px + 16px blur, ~24px real reach) room to render before hitting that
     clip edge — without it, overflow:hidden cut the shadow off flush with
     the card's bottom edge.

     top is set in px by js/main.js, not a fixed value here — pinning at
     top:0 filled the whole viewport with the row and hid everything above
     it (the Projects heading included); vertically centering it instead
     keeps roughly half the viewport free above the row, so that heading
     (and whatever came before it) stays in view while the cards scroll
     past horizontally. Percentage `top` on a sticky element resolves
     against its containing block's height, not the viewport, so a plain
     CSS top:50% doesn't center it — JS computes the real pixel offset
     from the actual viewport height instead. */
  .projects__hscroll-sticky {
    display: block;
    position: sticky;
    top: 0;
    width: 100%;
    padding-bottom: 32px;
    overflow: hidden;
  }

  .project-card {
    flex-shrink: 0;
    width: 640px;
    height: 480px;
    padding: 8px;
    border-radius: 40px;
  }

  .project-card__image {
    border-radius: 32px;
  }

  .project-card__meta {
    gap: 24px;
    padding: 12px 40px;
  }

  .project-card__title-group {
    gap: 20px;
  }

  .project-card__title {
    font-family: var(--font-heading-cardtitle-family);
    font-size: var(--font-heading-cardtitle-size);
    font-weight: var(--font-heading-cardtitle-weight);
    line-height: var(--font-heading-cardtitle-line-height);
    letter-spacing: var(--font-heading-cardtitle-letter-spacing);
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  /* Same rebalancing as Tablet — bottom cut from 120px to 48px, since
     .projects__hscroll-sticky's own 32px padding-bottom (shared, same
     value regardless of breakpoint — the shadow it's revealing doesn't
     scale) now covers the shadow-reveal role. */
  .projects {
    padding: 120px 100px 48px;
  }

  .projects__left {
    gap: 40px;
  }

  .projects__heading {
    font-family: var(--font-heading-sectiontitle-desktop-family);
    font-size: var(--font-heading-sectiontitle-desktop-size);
    font-weight: var(--font-heading-sectiontitle-desktop-weight);
    line-height: var(--font-heading-sectiontitle-desktop-line-height);
    letter-spacing: var(--font-heading-sectiontitle-desktop-letter-spacing);
  }
}

/* ==========================================================================
   Section: Services (Home page — Figma "Services" frame, all 3 breakpoints:
   Desktop 180:1200, Tablet 201:3052, Mobile 193:1736)

   Checked all 3 breakpoints independently via raw Plugin API — real
   structural difference confirmed, not assumed:
     - Desktop: Services itself is a HORIZONTAL auto-layout — Intro and the
       cards column sit side-by-side as two equal-width flex children
       (816px each out of a 1720px content area — implemented as flex:1 1 0
       on both rather than hardcoding 816px, since that number is only a
       consequence of the 1920px canvas reference, not a real fixed width).
     - Tablet + Mobile: Services is VERTICAL — Intro stacked above the
       cards column, full width.

   Card_Services_1 (3-item list) and Card_Services_2 (1-item list) share
   the same glass-card recipe as .card/.vas/.project-card (Light_01 radial
   sheen at 100%/100% @ 50% 0%, var(--color-gradient-01) border via the same
   ::before mask-composite technique, identical 3x DROP_SHADOW +
   BACKGROUND_BLUR radius 24 → blur(12px)) but with cornerRadius 16 —
   confirmed constant at all 3 breakpoints, unlike Projects' cards.

   Card_Services_2's single list item has its title text hidden in Figma
   (visible:false on "First-Hour Narrative Audit") — reproduced as
   description-only, no invented title.

   Padding genuinely differs between the two cards on Desktop/Tablet
   (Card_1: 32px/24px, Card_2: 24px uniform) but is identical between them
   on Mobile (24px/16px) — confirmed via paddingLeft/Right/Top/Bottom on
   both cards at all 3 tiers, not assumed from one card.

   Fonts all map to existing tokens, nothing new added to variables.css:
   heading = --font-heading-sectiontitle-* (same as Projects' heading),
   intro text = --font-body-base-*, eyebrow = --font-heading-cardtitle-*
   (shared desktop+tablet) / -mobile, item title = --font-body-base-*, item
   description = --font-body-small-*.

   Checkmark icon (Service/ListItem/Icon, 13x25.54px) had no pre-downloaded
   asset — fetched and saved as images/icons/home-services-icon-check.svg.
   ========================================================================== */

services,
.services {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 0 24px 24px;
  background: var(--color-black-01);
}

.services__intro {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.services__heading {
  margin: 0;
  font-family: var(--font-heading-sectiontitle-mobile-family);
  font-size: var(--font-heading-sectiontitle-mobile-size);
  font-weight: var(--font-heading-sectiontitle-mobile-weight);
  line-height: var(--font-heading-sectiontitle-mobile-line-height);
  letter-spacing: var(--font-heading-sectiontitle-mobile-letter-spacing);
  color: var(--color-light-01);
}

.services__intro-text {
  margin: 0;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.services__cards {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Scroll-panel active-card highlight (IntersectionObserver, see
   js/main.js) — not Figma-sourced, a custom interaction. Dimming the
   whole card's opacity to 0.25 at rest also dims its ::before gradient
   border and box-shadow below for free (both are painted as part of this
   same element's compositing), which is what satisfies the "barely
   visible border" requirement without a separate border-specific rule —
   .is-active then simply restores opacity:1, bringing back the exact
   real border/shadow already declared here, unmodified. */
.service-card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 16px;
  padding: 24px 16px;
  background: radial-gradient(100% 100% at 50% 0%, rgba(241, 252, 214, 0.2) 0%, rgba(241, 252, 214, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
  opacity: 0.25;
  will-change: opacity, color;
  transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}

.service-card.is-active {
  opacity: 1;
}

/* Gradient border, same mask-composite technique as .card/.vas/.project-card */
.service-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.service-card__content {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* Muted at rest (--color-light-01, the same neutral already used for
   every other de-emphasized text in this file, not a new invented tone)
   — .service-card.is-active below restores the real Figma color
   (--color-lime-01, this class's original value) as the highlight. */
.service-card__eyebrow {
  margin: 0;
  font-family: var(--font-heading-cardtitle-mobile-family);
  font-size: var(--font-heading-cardtitle-mobile-size);
  font-weight: var(--font-heading-cardtitle-mobile-weight);
  line-height: var(--font-heading-cardtitle-mobile-line-height);
  letter-spacing: var(--font-heading-cardtitle-mobile-letter-spacing);
  color: var(--color-light-01);
  will-change: color;
  transition: color 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}

.service-card.is-active .service-card__eyebrow {
  color: var(--color-lime-01);
}

.service-card__list {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.service-card__item {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 8px;
  padding-left: 4px;
}

.service-card__icon {
  display: block;
  flex-shrink: 0;
  width: 12.999px;
  height: 25.537px;
}

.service-card__item-text {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-width: 0;
  gap: 16px;
}

.service-card__item-title {
  margin: 0;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
}

.service-card__item-desc {
  margin: 0;
  font-family: var(--font-body-small-family);
  font-size: var(--font-body-small-size);
  font-weight: var(--font-body-small-weight);
  line-height: var(--font-body-small-line-height);
  letter-spacing: var(--font-body-small-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .services {
    gap: 24px;
    padding: 0 64px 24px;
  }

  .services__intro {
    gap: 24px;
  }

  .services__heading {
    font-family: var(--font-heading-sectiontitle-tablet-family);
    font-size: var(--font-heading-sectiontitle-tablet-size);
    font-weight: var(--font-heading-sectiontitle-tablet-weight);
    line-height: var(--font-heading-sectiontitle-tablet-line-height);
    letter-spacing: var(--font-heading-sectiontitle-tablet-letter-spacing);
  }

  .services__cards {
    gap: 24px;
  }

  .service-card {
    padding: 24px;
  }

  .service-card--primary {
    padding: 32px 24px;
  }

  .service-card__content {
    gap: 40px;
  }

  .service-card__eyebrow {
    font-family: var(--font-heading-cardtitle-family);
    font-size: var(--font-heading-cardtitle-size);
    font-weight: var(--font-heading-cardtitle-weight);
    line-height: var(--font-heading-cardtitle-line-height);
    letter-spacing: var(--font-heading-cardtitle-letter-spacing);
  }

  .service-card__list {
    gap: 32px;
  }

  .service-card__item {
    gap: 16px;
    padding-left: 16px;
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  .services {
    flex-direction: row;
    gap: 88px;
    padding: 0 100px 40px;
  }

  /* Sticky "scroll spotlight" left column — not Figma-sourced, a custom
     interaction, Desktop-only: this is the one breakpoint where .services
     is a row (flex-direction:row above), so align-items' default
     "stretch" makes .services__intro's box as tall as its .services__cards
     sibling — that's what lets sticky act as a real sidebar-follows-
     scroll effect. Tried it unconditionally first: at Mobile/Tablet
     .services stays a column (intro above cards, not beside them), so the
     pinned intro visibly collided with cards scrolling up underneath it
     (confirmed via screenshot — the heading text and a card's background
     double-exposed). Scoped here once that was visible, not guessed. */
  .services__intro {
    flex: 1 1 0;
    gap: 40px;
    position: sticky;
    top: 15%;
  }

  .services__heading {
    font-family: var(--font-heading-sectiontitle-desktop-family);
    font-size: var(--font-heading-sectiontitle-desktop-size);
    font-weight: var(--font-heading-sectiontitle-desktop-weight);
    line-height: var(--font-heading-sectiontitle-desktop-line-height);
    letter-spacing: var(--font-heading-sectiontitle-desktop-letter-spacing);
  }

  .services__cards {
    flex: 1 1 0;
  }
}

/* ==========================================================================
   Section: Partners (Home page — Figma "Partners" frame, all 3 breakpoints:
   Desktop 50:470, Tablet 201:3007, Mobile 193:1772)

   Checked all 3 breakpoints independently via raw Plugin API:
     - Heading alignment is a real difference, not styling drift: centered
       on Desktop + Tablet (Header's counterAxisAlignItems: CENTER,
       confirmed by the text's own x-offset centering in both), but
       left-aligned on Mobile (counterAxisAlignItems: MIN, text x:0).
     - "Brands" is a wrapping flex row (layoutWrap: WRAP) at every
       breakpoint. Desktop fits all 7 logos on one line (itemSpacing 32,
       1720px available — logos + gaps sum to ~1143px, no wrap triggered).
       Tablet reflows to 2 rows at the same logo sizes as Desktop but a
       tighter itemSpacing (10, not 32) and a narrower 568px container —
       confirmed via real x/y positions, not assumed from Desktop. Mobile
       has its own smaller logo sizes and reflows to 2 rows too, in a
       slightly-nested Figma structure (an outer padded "Brands" wrapping
       an inner "Logo" flex-wrap row) that collapses to the exact same box
       model as one flattened element (outer padding + inner gap/wrap, no
       other siblings) — flattened here into a single .partners__brands
       for all 3 tiers without changing the rendered result.
     - Each of the 7 logos keeps its own real intrinsic width/height
       (verified per logo, not a uniform grid cell) — identical between
       Desktop and Tablet, with Mobile using its own smaller real sizes.
     - Brands panel reuses the same glass recipe as .card/.vas/.service-card
       /.project-card (Light_01 radial sheen at 100%/100% @ 50% 0%,
       var(--color-gradient-01) border via the same ::before mask-composite
       technique, identical 3x DROP_SHADOW + BACKGROUND_BLUR radius 24 →
       blur(12px)), cornerRadius 16 — confirmed via raw fills/effects, not
       assumed from the other cards.
     - Heading maps to the same --font-heading-sectiontitle-* token already
       used by Projects/Services (byte-identical size/lineHeight/
       letterSpacing at all 3 tiers). Nothing new added to variables.css.
     - Logo images reused from already-downloaded assets
       (images/home-partners-logo-*-desktop.webp / -mobile.webp) — Tablet
       shares the Desktop asset, matching their identical real sizes.
   ========================================================================== */

/* Footer Reveal wrapper — bounds where the sticky footer below is allowed
   to stay "stuck." Without this, .footer's sticky containing block is
   effectively <body>, so scrolling back UP from the bottom kept it pinned
   over whatever content was underneath for a long stretch — Services,
   even Hero — not just Partners (reproduced and confirmed: elementFromPoint
   over Services returned the footer while scrolling up). Per spec, a
   sticky element's containing block is the same as a relatively
   positioned one's — the nearest block-container ancestor, positioned or
   not — so wrapping just Partners+Footer in one plain block here bounds
   the stuck window to roughly this wrapper's own height (~Partners'
   height), which is exactly enough room for the reveal and no more.
   Verified in an isolated repro before touching the real page: content
   before the wrapper was never covered afterward, and the reveal itself
   still worked identically. */
.footer-reveal-wrap {
  position: relative;
}

partners,
.partners {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 64px 24px 24px;
  background: var(--color-black-01);
  /* Footer Reveal effect: Partners already has a solid background (above)
     — position:relative is required for z-index to take effect at all,
     which is what lets this opaque section paint OVER the sticky footer
     beneath it (see footer/.footer below, z-index:1) as the user scrolls,
     instead of the footer's later DOM position painting on top by
     default. */
  position: relative;
  z-index: 2;
}

.partners__heading {
  margin: 0;
  font-family: var(--font-heading-sectiontitle-mobile-family);
  font-size: var(--font-heading-sectiontitle-mobile-size);
  font-weight: var(--font-heading-sectiontitle-mobile-weight);
  line-height: var(--font-heading-sectiontitle-mobile-line-height);
  letter-spacing: var(--font-heading-sectiontitle-mobile-letter-spacing);
  color: var(--color-light-01);
}

/* Card chrome (background/border/shadow/rounding) unchanged from the
   original static panel — only the flex-row-of-logos responsibility
   (display/flex-wrap/justify-content/align-items/gap) moved out, down to
   .partners__track below, since .partners__brands now wraps the marquee
   viewport instead of laying out logos directly. overflow:hidden was
   already here and is what clips the sliding track at the card's own
   rounded edges. */
.partners__brands {
  position: relative;
  padding: 16px;
  border-radius: 16px;
  overflow: hidden;
  background: radial-gradient(100% 100% at 50% 0%, rgba(241, 252, 214, 0.2) 0%, rgba(241, 252, 214, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

/* Gradient border, same mask-composite technique as .card/.vas/.service-card */
.partners__brands::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* ==========================================================================
   Infinite Logo Marquee — Partners/Brands

   Not a Figma-sourced effect — a custom interaction, pure CSS (no JS
   drives the motion). The 7 real logos and their per-breakpoint sizes
   below are all unchanged from the original static panel — only the
   layout mechanism (wrapping flex row -> looping horizontal track)
   changed.

   Seamless-loop mechanics: .partners__marquee is width:max-content
   holding two IDENTICAL .partners__track rows back-to-back (the second
   duplicated at the HTML level, aria-hidden since it's a decorative
   repeat of the first). Because both tracks are exactly the same width,
   translate3d(-50%, 0, 0) moves the marquee by precisely one track's
   width — the instant the first track has fully scrolled off, the
   (identical) second track is sitting exactly where the first one
   started, so the loop point is invisible. linear timing keeps the speed
   constant through that seam (an eased curve would visibly hitch there).

   .partners__marquee-viewport (not .partners__brands itself) carries
   overflow:hidden + the fade mask, so the mask only ever touches the
   logo row — .partners__brands's own real border/shadow/background chrome
   is untouched by it. */
.partners__marquee-viewport {
  overflow: hidden;
  width: 100%;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 8%, #000 92%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 8%, #000 92%, transparent 100%);
}

.partners__marquee {
  display: flex;
  align-items: center;
  width: max-content;
  will-change: transform;
  animation: partners-marquee 25s linear infinite;
}

/* Pause on desktop hover, on touch press (:active is the CSS-only
   equivalent available for "pressed" on touch devices — there is no
   long-press pseudo-class), and on keyboard focus inside the track. */
.partners__marquee:hover,
.partners__marquee:active,
.partners__marquee:focus-within {
  animation-play-state: paused;
}

@keyframes partners-marquee {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(-50%, 0, 0);
  }
}

.partners__track {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 8px;
}

/* flex-shrink:0 wasn't needed on the old wrapping layout (overflow just
   wrapped to a new line) — required now that the track is nowrap and
   intentionally wider than its viewport, or flexbox would compress the
   logos to try to fit them. */
.partners__track > picture {
  flex-shrink: 0;
}

.partners__logo {
  display: block;
  object-fit: contain;
}

.partners__logo--webelinx {
  width: 90px;
  height: 39px;
}

.partners__logo--microsoft {
  width: 96px;
  height: 40px;
}

.partners__logo--fcs {
  width: 90px;
  height: 32.18px;
}

.partners__logo--melsoft {
  width: 48px;
  height: 38px;
}

.partners__logo--centerscience {
  width: 67px;
  height: 32px;
}

.partners__logo--minecraft {
  width: 80px;
  height: 29.94px;
}

.partners__logo--syclonestudio {
  width: 24px;
  height: 32px;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .partners {
    gap: 24px;
    padding: 80px 64px 24px;
  }

  .partners__heading {
    font-family: var(--font-heading-sectiontitle-tablet-family);
    font-size: var(--font-heading-sectiontitle-tablet-size);
    font-weight: var(--font-heading-sectiontitle-tablet-weight);
    line-height: var(--font-heading-sectiontitle-tablet-line-height);
    letter-spacing: var(--font-heading-sectiontitle-tablet-letter-spacing);
    text-align: center;
  }

  .partners__brands {
    padding: 32px 8px;
  }

  .partners__track {
    gap: 10px;
  }

  .partners__logo--webelinx {
    width: 169px;
    height: 73px;
  }

  .partners__logo--microsoft {
    width: 177px;
    height: 74px;
  }

  .partners__logo--fcs {
    width: 179px;
    height: 64px;
  }

  .partners__logo--melsoft {
    width: 72px;
    height: 57px;
  }

  .partners__logo--centerscience {
    width: 141px;
    height: 68px;
  }

  .partners__logo--minecraft {
    width: 171px;
    height: 64px;
  }

  .partners__logo--syclonestudio {
    width: 42px;
    height: 56px;
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  .partners {
    gap: 40px;
    padding: 120px 100px 40px;
  }

  .partners__heading {
    font-family: var(--font-heading-sectiontitle-desktop-family);
    font-size: var(--font-heading-sectiontitle-desktop-size);
    font-weight: var(--font-heading-sectiontitle-desktop-weight);
    line-height: var(--font-heading-sectiontitle-desktop-line-height);
    letter-spacing: var(--font-heading-sectiontitle-desktop-letter-spacing);
  }

  .partners__track {
    gap: 32px;
  }
}

/* ==========================================================================
   Section: Footer (Home page — Figma "Footer" frame, all 3 breakpoints:
   Desktop 62:694, Tablet 201:3028, Mobile 193:1812)

   Checked all 3 breakpoints independently via raw Plugin API — two real
   findings worth flagging:

   1) The 3-column row ("Menu": links / connect / say-hello) is a GENUINE
      structural difference, not a shared layout:
        - Desktop: all 3 columns fit on one row (96+78+116+78+417=785px in
          a 1720px-wide row) and are centered as a group
          (primaryAxisAlignItems: CENTER).
        - Mobile: Figma explicitly stacks it as "Links+Connect" in one row
          (gap 78) with "Say Hello" wrapping below (gap 64) — the
          Say-Hello column is set to the full container width there.
        - Tablet: Figma's own Menu frame is a single HORIZONTAL row with
          NO wrap flag (layoutWrap: NO_WRAP), same three column widths as
          Desktop, in a 536px-wide row — 96+16+116+16+417=661px genuinely
          overflows that row by 125px. This isn't a case of "structure
          differs, reproduce as-is": an unwrapped 661px row in a 536px
          container would either clip content or force page-wide
          horizontal scroll in a real browser. Implemented flex-wrap on
          the row for all 3 tiers (real column widths/gaps unchanged) so
          Tablet naturally reflows Say-Hello onto its own line exactly
          like Mobile's explicit solution, Desktop still fits on one line
          (785 < 1720, wrap never triggers), and Mobile's own full-width
          Say-Hello column still forces its own wrap the same as before.
          Flagging this rather than silently treating Tablet as "working
          as designed" — the raw numbers say it doesn't fit without wrap.

   2) The email text ("vasja.stankovic@gmail.com") is fontSize 24 /
      lineHeight 118.75% / Open Sans 400 at ALL 3 breakpoints right now —
      but variables.css's --font-body-email-* (non-mobile) token is 32px,
      only --font-body-email-mobile-* matches (24px) what Figma currently
      shows. Since no size/breakpoint variation actually exists anymore,
      --font-body-email-mobile-* is used uniformly at all 3 tiers rather
      than adding a new token — flagging that the 32px desktop/tablet
      token now looks stale against the live file, in case that's worth
      fixing in Figma or the token itself.

   Column label ("MENU"/"CONNECT"/"SAY HELLO") maps to --font-body-base-*
   (uniform at all 3 tiers). Location text also --font-body-base-*.
   "LET'S COLLABORATE!" maps to --font-display-collaborate-* (already in
   variables.css) — Figma's textCase is UPPER (not just already-uppercase
   content), so text-transform:uppercase is applied explicitly. Bottom
   legal line maps to --font-legal-caption-*. Nothing new added to
   variables.css.

   Bottom's copy is centered as a single line on Desktop/Tablet but is
   left-aligned as an explicit 2-line break on Mobile (real \n in the
   Figma text, not a width-driven wrap) — reproduced with the same
   conditional-<br> technique as .hero-tag__break--responsive, toggling at
   the Tablet threshold instead of Desktop's.

   .footer-menu / .footer-connect (nav instances) are untouched, per
   "не чіпати вже наявний CSS" — only placed inside the new column layout.
   ========================================================================== */

/* Footer Reveal — not Figma-sourced, a custom scroll interaction. Since
   .footer is the page's last element, it reaches its natural bottom-of-
   document resting spot as soon as it scrolls into view; position:sticky;
   bottom:0 "catches" it there immediately, and z-index:1 (paired with
   .partners's own explicit z-index:2 above) lets Partners' opaque
   background keep painting over the now-pinned footer as Partners itself
   continues scrolling normally — uncovering the footer progressively as
   Partners scrolls up and off, rather than the footer just appearing
   instantly like a normal static section. .footer-reveal-wrap above is
   what bounds how long the "stuck" phase itself can last — see the
   comment there; z-index only decides who paints on top DURING that
   window, it doesn't bound the window's length.

   z-index:1, not the -1 first tried: negative z-index on a box whose
   effective stacking context is the page root paints it behind <body>'s
   own layer, and that isn't just a visual no-op — <body> then wins EVERY
   hit-test over the footer's whole area, making every link inside it
   unclickable to a real pointer. Caught this via elementsFromPoint (BODY
   came back before the actual <a>) after Playwright's real hover() timed
   out with "<body> intercepts pointer events"; a programmatic .click()
   had been masking it by bypassing hit-testing entirely. A positive value
   still sits comfortably below Partners' 2 for the cover effect, without
   leaving the normal stacking level.

   Sticky/reveal is Tablet+Desktop only (min-width: 696px, this project's
   own Mobile/Tablet threshold) — Mobile's base rule below stays
   position:static. QA audit finding: this used to be gated by a
   max-height:600px query instead, meant to catch short viewports — but a
   typical phone in portrait is well over 600px tall, so the effect was
   still firing on real mobile screens, not just the cramped-landscape
   case it was originally written for. Kept that max-height query too
   (below, now scoped inside the Tablet+ block it belongs in): a short
   Tablet/Desktop viewport (landscape phone, small window) still doesn't
   have room for the reveal to read as anything but a jump cut. */
footer,
.footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 64px 24px 48px;
  background: var(--color-black-01);
  position: static;
}

@media (min-width: 696px) {
  footer,
  .footer {
    position: sticky;
    bottom: 0;
    z-index: 1;
  }
}

@media (min-width: 696px) and (max-height: 600px) {
  footer,
  .footer {
    position: static;
  }
}

.footer__collaborate {
  margin: 0;
  text-transform: uppercase;
  text-align: center;
  font-family: var(--font-display-collaborate-mobile-family);
  font-size: var(--font-display-collaborate-mobile-size);
  font-weight: var(--font-display-collaborate-mobile-weight);
  line-height: var(--font-display-collaborate-mobile-line-height);
  letter-spacing: var(--font-display-collaborate-mobile-letter-spacing);
  color: var(--color-lime-01);
}

/* "Breathing" reveal — not Figma-sourced, a custom scroll interaction.
   -0.05em reuses the same tight-letter-spacing starting value already
   established for every other kinetic-blur/reveal effect in this file
   (Hero/NameFrame, page titles) rather than inventing a new one; the `to`
   state restores the real --font-display-collaborate-*-letter-spacing
   token (3.2px, identical at all 3 breakpoints, same pattern as Hero's
   name). ease-in-out (not linear) for the "breathing" quality asked for —
   this is the one scroll effect in this project explicitly described as
   organic rather than a mechanical scrub. Gated behind @supports for the
   same progressive-enhancement reason as every other view()-timeline
   effect here.

   Named timeline, not a bare view() on the element itself — found via the
   same kind of check that caught the Hero/WELCOME bug: once .footer is
   sticky-pinned, .footer__collaborate's OWN bounding box barely moves for
   the whole reveal (confirmed: its rect stayed within a couple px across
   ~1200px of scroll), so a view() timeline tracking its own visibility
   sits "fully contained" almost immediately and the range never actually
   progresses — the animation was frozen at transform:scale(0.8) the
   entire time regardless of scroll position. What's actually moving is
   .partners scrolling away above it (the real driver of the reveal, via
   its z-index:10 cover), so the timeline is named on .partners instead
   and .footer__collaborate subscribes to that name.

   One more piece the first attempt at this missed: a view-timeline-name
   is only usable, by default, by the declaring element's OWN descendants
   — .partners and .footer__collaborate are siblings (both direct
   children of <body>), so without an explicit timeline-scope on a shared
   ancestor, .footer__collaborate simply couldn't see --partners-exit at
   all and stayed frozen at the `to` state. Confirmed via an isolated
   repro (same symptom reproduced with two unrelated elements; adding
   timeline-scope on their common ancestor fixed it) before adding the
   body rule below.

   Range is `exit 0% exit 20%`, not exit 0%/100% — measured that "exit
   100%" (Partners' bottom pixel fully off-screen) is mathematically
   unreachable on this page: since .footer is shorter than the viewport,
   the scrollable document always ends with a bit of Partners still
   technically on-screen (the shortfall is exactly viewportHeight minus
   footerHeight, confirmed against the measured numbers). The real visual
   reveal — Partners' bottom edge clearing the pinned footer's top edge —
   completes around ~33% into Partners' exit phase on the desktop
   viewport tested, and that fraction shifts with viewport/footer/
   partners height, so there's no single percentage that's exactly
   "correct" everywhere. 20% is a deliberately conservative target well
   under that, so the breathing animation reliably finishes settling
   before the true reveal completes rather than risking getting stuck
   partway animated on some viewport/content combination, the way the
   literal exit-100% version measurably did on this one. */
@supports (animation-timeline: view()) {
  body {
    timeline-scope: --partners-exit;
  }

  .partners {
    view-timeline-name: --partners-exit;
  }

  .footer__collaborate {
    transform: scale(0.8);
    letter-spacing: -0.05em;
    will-change: transform, letter-spacing;
    animation-name: footer-collaborate-breathe;
    animation-timing-function: ease-in-out;
    animation-fill-mode: both;
    animation-timeline: --partners-exit;
    animation-range: cover 40% cover 65%;
  }

  @keyframes footer-collaborate-breathe {
    from {
      transform: scale(0.8);
      letter-spacing: -0.05em;
    }
    to {
      transform: scale(1);
      letter-spacing: var(--font-display-collaborate-mobile-letter-spacing);
    }
  }
}

/* Figma's "Top" wrapper (merged into .footer__menu, see comment above)
   carries a top-only 0.5px Light_01 hairline — same at all 3 breakpoints.
   Updated per a live Figma edit (re-verified via raw Plugin API): the
   Menu+Connect pair row is now centered as a group (justify-content) and
   the real mobile gap is row 40px (Menu's own itemSpacing) / column 48px
   (Footer/ColumnsWrap's itemSpacing — was wrongly 64/78, copy-pasted from
   Desktop's value; a later edit moved it 24→48, re-verified directly on
   Footer/ColumnsWrap on both pages). */
.footer__menu {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 40px 48px;
  padding-top: 16px;
  width: 100%;
  border-top: 0.5px solid var(--color-light-01);
}

.footer__column {
  display: flex;
  flex-direction: column;
  gap: 25px;
}

.footer__column--hello {
  width: 100%;
}

/* Mobile-only (per the same live Figma edit): SAY HELLO's own label,
   e-mail and location center within the column (verified: counterAxis
   AlignItems is CENTER on this column's inner wrapper at Mobile only —
   MENU/CONNECT columns stay left-aligned throughout, and this column
   itself resets to left-aligned again at Tablet/Desktop below). */
.footer__column--hello .footer__column-label {
  text-align: center;
}

.footer__column--hello .footer__hello {
  align-items: center;
}

.footer__column-label {
  margin: 0;
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.footer__hello {
  display: flex;
  flex-direction: column;
  gap: 41px;
}

/* Click-to-copy email — not Figma-sourced, a custom micro-interaction on
   top of this real mailto: link (its href is left untouched, so the
   normal mailto navigation still happens alongside the copy — see
   js/main.js). .footer__email-wrap only exists to anchor the floating
   tooltip below; it carries no visual styling of its own. */
.footer__email-wrap {
  position: relative;
  display: inline-block;
}

.footer__email {
  display: block;
  margin: 0;
  text-decoration: none;
  font-family: var(--font-body-email-mobile-family);
  font-size: var(--font-body-email-mobile-size);
  font-weight: var(--font-body-email-mobile-weight);
  line-height: var(--font-body-email-mobile-line-height);
  letter-spacing: var(--font-body-email-mobile-letter-spacing);
  color: var(--color-light-01);
  cursor: pointer;
  will-change: transform;
  transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}

.footer__email:hover,
.footer__email:focus-visible {
  transform: scale(1.05);
}

/* Small black-and-green tooltip, floating above the email. Typography
   reuses --font-legal-caption-* — the real small "caption" text token
   already in variables.css — rather than inventing a new font-size for
   this new UI element; color/background reuse the existing Black_01/
   Lime_01 tokens the whole page is built from. */
.footer__email-tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  padding: 4px 10px;
  border: 1px solid var(--color-lime-01);
  border-radius: 8px;
  background: var(--color-black-01);
  color: var(--color-lime-01);
  font-family: var(--font-legal-caption-family);
  font-size: var(--font-legal-caption-size);
  font-weight: var(--font-legal-caption-weight);
  line-height: var(--font-legal-caption-line-height);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  will-change: opacity, transform;
  transition: opacity 0.2s ease, transform 0.2s ease;
  transform: translateX(-50%) translateY(4px);
}

.footer__email-wrap:hover .footer__email-tooltip,
.footer__email-wrap:focus-within .footer__email-tooltip,
.footer__email-wrap.is-copied .footer__email-tooltip {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.footer__location {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* "Live location" pulse — not Figma-sourced, a custom micro-interaction.
   position:relative anchors the expanding ::after ring to the dot's own
   box; the ring reuses the dot's real Lime_01 fill rather than a new
   color, just faded and animated outward. */
.footer__location-dot {
  position: relative;
  display: block;
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-lime-01);
}

.footer__location-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--color-lime-01);
  opacity: 0.6;
  will-change: transform, opacity;
  animation: footer-location-pulse 2s ease-out infinite;
}

@keyframes footer-location-pulse {
  from {
    transform: scale(1);
    opacity: 0.6;
  }
  to {
    transform: scale(2.5);
    opacity: 0;
  }
}

.footer__location-text {
  font-family: var(--font-body-base-family);
  font-size: var(--font-body-base-size);
  font-weight: var(--font-body-base-weight);
  line-height: var(--font-body-base-line-height);
  letter-spacing: var(--font-body-base-letter-spacing);
  color: var(--color-light-01);
}

/* Same top-only 0.5px Light_01 hairline as .footer__menu ("Top"), same at
   all 3 breakpoints. */
.footer__bottom {
  display: flex;
  width: 100%;
  padding: 24px 0;
  border-top: 0.5px solid var(--color-light-01);
}

/* textAlignHorizontal is CENTER at Mobile (a real gap here — was unset,
   defaulting to left), RIGHT at Tablet/Desktop (see media queries below).
   width:100% is required too — .footer__bottom is a row flex container, so
   without an explicit width this <p> shrinks to its own content (303.5px
   of the 326px row) instead of the full FILL width the real Figma text
   node has, leaving text-align:center centering within a too-narrow box
   instead of the true row. */
.footer__legal {
  margin: 0;
  width: 100%;
  text-align: center;
  font-family: var(--font-legal-caption-family);
  font-size: var(--font-legal-caption-size);
  font-weight: var(--font-legal-caption-weight);
  line-height: var(--font-legal-caption-line-height);
  letter-spacing: var(--font-legal-caption-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.footer__legal-break {
  display: inline;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .footer {
    gap: 24px;
    padding: 80px 64px 48px;
  }

  .footer__collaborate {
    font-family: var(--font-display-collaborate-tablet-family);
    font-size: var(--font-display-collaborate-tablet-size);
    font-weight: var(--font-display-collaborate-tablet-weight);
    line-height: var(--font-display-collaborate-tablet-line-height);
    letter-spacing: var(--font-display-collaborate-tablet-letter-spacing);
    text-align: center;
  }

  .footer__menu {
    justify-content: center;
    gap: 16px;
    padding: 40px 16px 0;
  }

  /* Real width per a live Figma edit (re-verified via raw Plugin API):
     303px, was 312px — the column is HUG-sized to its widest child
     (Footer/HelloContent) and that shrank slightly. */
  .footer__column--hello {
    width: 303px;
  }

  /* Tablet now matches Desktop: MIN/left-aligned throughout (the column's
     own counterAxisAlignItems has no visual effect here since its direct
     child fills/stretches to match — confirmed by reading the actual
     alignment-holding node one level deeper, Footer/ColumnInner, which is
     MIN at both Tablet and Desktop). Reset back from Mobile's centering. */
  .footer__column--hello .footer__column-label {
    text-align: left;
  }

  .footer__column--hello .footer__hello {
    align-items: flex-start;
  }

  .footer__bottom {
    justify-content: center;
  }

  /* Reset the Mobile-only width:100% fix — at Tablet/Desktop the real
     Figma text node is narrower than its row (476px of 568/1240px) and
     centered as a block by .footer__bottom's justify-content:center
     above; forcing it full-width here would right-align against the far
     edge of the row instead of the edge of that centered 476px block. */
  .footer__legal {
    width: auto;
    text-align: right;
  }

  .footer__legal-break {
    display: none;
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  .footer {
    gap: 40px;
    padding: 120px 100px 48px;
  }

  .footer__collaborate {
    font-family: var(--font-display-collaborate-desktop-family);
    font-size: var(--font-display-collaborate-desktop-size);
    font-weight: var(--font-display-collaborate-desktop-weight);
    line-height: var(--font-display-collaborate-desktop-line-height);
    letter-spacing: var(--font-display-collaborate-desktop-letter-spacing);
  }

  .footer__menu {
    justify-content: center;
    gap: 78px;
    padding: 40px 0 0;
  }

  /* Desktop's own narrower real width (145px, not Tablet's 303px — they
     diverge, no longer a shared value). Left-alignment is already
     established at Tablet above and simply carries forward here. */
  .footer__column--hello {
    width: 145px;
  }
}

/* ==========================================================================
   Games page — NavBar

   "NavBar" frame: full-bleed 375/696/1440 wide, height 96, vertical
   auto-layout, padding 24px top / 8px bottom / 0 sides — holds one
   centered "Navigation" instance (264x64). Same shared component/variant
   used by Hero's navbar on Home (mainComponent 691:5532,
   variantProperties.Active="Nav_Games" here vs "Nav_Home" there) — reused
   verbatim as .nav, only data-active differs. Confirmed identical
   structure/sizing across all 3 breakpoints via raw Plugin API.

   NavBar's own fill: Mobile (232:4042) and Tablet (214:3139) frames carry
   an explicit Black_01 solid fill; Desktop (69:1638) has fills: [] (no
   fill of its own) there, but the underlying page frame is Black_01 at
   all 3 breakpoints (confirmed via raw Plugin API) — applied uniformly
   here per user request, matching the page's real background color. */
.navbar {
  display: flex;
  justify-content: center;
  padding: 24px 0 8px;
  background: var(--color-black-01);
  /* Explicit stacking-order guard, same as .hero__navbar on Home — keeps
     the nav above the Kinetic Variable Font Blur page-title animations
     below (Games/Video Production/More), which briefly grow via
     transform:scale during load. position:relative is required for
     z-index to take effect. */
  position: relative;
  z-index: 50;
}

/* ==========================================================================
   Kinetic Variable Font Blur — page-title headers, on load (Games/Video
   Production/More)

   Not a Figma-sourced effect — a custom load-in animation, requested with
   an explicit technical spec, reusing the same technique already built for
   Hero/NameFrame on Home (see the comment there for the full mechanics).
   One shared @keyframes reused across all 3 pages' title selectors since
   the animation shape is identical; only the settled letter-spacing
   differs (it's a real, different token per breakpoint here, unlike
   Home's name where all 3 breakpoint tokens happened to be equal).

   --kinetic-letter-spacing carries that real per-breakpoint token (an
   alias of the exact var() each title rule already used directly) so the
   shared keyframes' `to` state can reference one custom property and land
   correctly regardless of which page/breakpoint is animating. Games/More
   apply the animation straight to their <h1> (no split requested).
   Video's title is split into two <span>s instead (stagger requested) —
   those spans set no letter-spacing of their own and inherit
   --kinetic-letter-spacing from their parent .video-main__title, which
   still declares it exactly like Games/More do. */
@keyframes kinetic-title-blur {
  from {
    opacity: 0;
    filter: blur(30px);
    transform: scale(0.88);
    letter-spacing: -0.05em;
  }
  to {
    opacity: 1;
    filter: blur(0px);
    transform: scale(1);
    letter-spacing: var(--kinetic-letter-spacing);
  }
}

/* ==========================================================================
   Games page — Main

   "Main" frame: vertical auto-layout, gap 16/24/40 (mobile/tablet/desktop),
   padding 0/24/64/40/100 (see per-breakpoint rules below), Black_01 fill —
   confirmed identical to the page frame under NavBar at all 3 breakpoints.
   Two children: "Art" (page title — the intro paragraph beneath it is
   hidden:true in Figma on all 3 breakpoints, so it is not rendered here)
   and "Col"/"Column" (the 8 game cards).

   Title: Text/Display/PageTitle/Mobile|Tablet|Desktop tokens (already in
   variables.css; letter-spacing values there were flat 1px — fixed to the
   real PERCENT-1% values confirmed via raw Plugin API, same class of fix
   already applied earlier to --font-heading-cardtitle-*). */
/* Footer Reveal support — same role as .partners on Home (see the
   .footer-reveal-wrap comment there): this is the section immediately
   before <footer> on this page, so it needs the same solid background
   (already had one) + position:relative + z-index:2 to paint over the
   sticky footer during the reveal, and the same --partners-exit
   view-timeline-name (reused as-is — one page loads at a time, so the
   name never collides across pages) so .footer__collaborate's breathing
   animation has a real timeline to bind to here too. Without this it was
   confirmed stuck at transform:scale(0.8) permanently on this page — no
   .partners element exists here for the timeline to attach to. */
.games-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 0 24px 24px;
  background: var(--color-black-01);
  position: relative;
  z-index: 2;
}

@supports (animation-timeline: view()) {
  .games-main {
    view-timeline-name: --partners-exit;
  }
}

.games-main__art {
  width: 100%;
  max-width: 800px;
  padding-top: 16px;
}

.games-main__header {
  width: 100%;
  padding-top: 10px;
}

.games-main__title {
  margin: 0;
  width: 100%;
  font-family: var(--font-display-pagetitle-mobile-family);
  font-size: var(--font-display-pagetitle-mobile-size);
  font-weight: var(--font-display-pagetitle-mobile-weight);
  line-height: var(--font-display-pagetitle-mobile-line-height);
  color: var(--color-light-01);
  text-transform: uppercase;
  /* Kinetic Variable Font Blur, page-load only — see @keyframes
     kinetic-title-blur near .navbar. letter-spacing is now driven by that
     animation (from -0.05em to the real token below) instead of being set
     directly. */
  --kinetic-letter-spacing: var(--font-display-pagetitle-mobile-letter-spacing);
  letter-spacing: -0.05em;
  opacity: 0;
  filter: blur(30px);
  transform: scale(0.88);
  will-change: opacity, filter, transform, letter-spacing;
  animation-name: kinetic-title-blur;
  animation-duration: 1.2s;
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  animation-fill-mode: forwards;
}

.games-main__col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

/* Mobile has no card-pairing wrapper at all (Figma's mobile "Col" holds 8
   flat children) — Tablet/Desktop group some cards under "Grid/CardPair"
   frames instead. display:contents keeps one honest DOM tree serving all 3
   breakpoints: on Mobile these wrappers drop out of layout entirely so
   their children become plain flat items of .games-main__col (matching
   Mobile exactly); Tablet/Desktop turn them into real flex containers
   below. */
.games-main__pair,
.games-main__pair-col {
  display: contents;
}

/* ---- Game cards ----
   Card instance: cornerRadius 40, padding 8px, gradient border (GRADIENT_
   LINEAR Lime_01→Black_01 @ opacity 0.3 — same var(--color-gradient-01)
   token already used by .project-card/.btn-arrow, not the solid Lime_01
   get_design_context's Tailwind output showed — verified via raw strokes),
   background is a radial gradient (GRADIENT_RADIAL, white stops 1.0→0.1 @
   paint opacity 0.2, transform decodes to 50% 50% at 50% 75% — identical
   recipe to .vas, not .project-card's Light_01/100%-100%-at-50%-0% one),
   backdrop-blur 24÷2=12px, and the same triple DROP_SHADOW recipe used
   throughout the project. Confirmed identical across every sampled card on
   all 3 breakpoints via raw Plugin API. */
.game-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 8px;
  border-radius: 40px;
  overflow: hidden;
  text-decoration: none;
  background: radial-gradient(50% 50% at 50% 75%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

.game-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.game-card__content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.game-card__image {
  position: relative;
  width: 100%;
  height: 223px;
  border-radius: 32px;
  overflow: hidden;
}

.game-card__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same fix as .video-card__image--top (Hunting): Jungle Society's source
   art has its characters positioned in the upper half of the frame. At
   Tablet widths this "half" card runs full column width while height
   stays fixed at 436px — wider/shallower than the image's own aspect —
   so default center-cropping cut into the characters. Anchoring the crop
   to the top keeps them intact; harmless where no vertical crop happens. */
.game-card__image--top img {
  object-position: center top;
}

.game-card__details {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  padding: 16px;
}

.game-card__title {
  margin: 0;
  font-family: var(--font-heading-cardtitle-mobile-family);
  font-size: var(--font-heading-cardtitle-mobile-size);
  font-weight: var(--font-heading-cardtitle-mobile-weight);
  line-height: var(--font-heading-cardtitle-mobile-line-height);
  letter-spacing: var(--font-heading-cardtitle-mobile-letter-spacing);
  color: var(--color-lime-01);
}

.game-card__desc {
  margin: 0;
  font-family: var(--font-body-description-family);
  font-size: var(--font-body-description-size);
  font-weight: var(--font-body-description-weight);
  line-height: var(--font-body-description-line-height);
  letter-spacing: var(--font-body-description-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.game-card__button-wrap {
  display: flex;
  align-items: center;
  padding: 8px 0;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .games-main {
    align-items: flex-start;
    gap: 24px;
    padding: 0 64px 24px;
  }

  .games-main__art {
    padding-top: 48px;
  }

  .games-main__header {
    padding-top: 0;
  }

  .games-main__title {
    font-family: var(--font-display-pagetitle-tablet-family);
    font-size: var(--font-display-pagetitle-tablet-size);
    font-weight: var(--font-display-pagetitle-tablet-weight);
    line-height: var(--font-display-pagetitle-tablet-line-height);
    --kinetic-letter-spacing: var(--font-display-pagetitle-tablet-letter-spacing);
  }

  .games-main__col {
    gap: 8px;
  }

  .games-main__pair,
  .games-main__pair-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
  }

  .games-main__pair {
    flex-direction: row;
    flex-wrap: wrap;
  }

  /* Tablet's card content-width (568px) is never wide enough to fit two
     cards side by side, so every "Grid/CardPair" pairing reflows to a
     plain vertical stack here — confirmed via real Figma x/y coordinates
     (every card sits at x=0, full 568px width, whether inside a pair
     wrapper or not). Real 2-up pairing only happens at Desktop below. */

  /* Tablet/Desktop show the image alone at rest and reveal the same real
     Title/Description/Button content used on Mobile on hover/focus — this
     is a real Figma interaction (ON_HOVER → SMART_ANIMATE to a "_Hover"
     variant of the same component), not an invented effect. Its Text/
     Description layers looked empty via a plain instance read because
     Figma only exposes per-instance override text once that variant is
     actually active — confirmed by temporarily swapping each instance to
     its "_Hover" variant via the Plugin API, reading the real text (it
     matches Mobile's copy word for word), then reverting immediately (no
     change left in the Figma file).

     Sizing: an even 1fr/1fr CSS Grid split (matching the ~50/50 proportion
     the Figma Hover symbol shows for the full-size _Card_001 variant) was
     tried first but looked wrong in practice — Minecraft's real title +
     2-line description + button is much shorter than half of a 720px
     card, leaving a large empty grey gap below the button. Figma's symbol
     proportions here are unreliable preview-only data, not a real spec
     (already flagged above), so instead the Details panel is sized to its
     own real content via max-height, and the image (flex:1) absorbs
     whatever space is left — this holds for every card size without a
     size-specific gap.

     Button destinations: the same swap also exposed each Button/Arrow's
     real ON_CLICK reaction. Escape Corp/Rooms and Exits/Drift VR/Jungle
     Society have a real URL there matching Mobile's exactly. Minecraft/
     Flameheart/Family Island/Boo Boo's House have an ON_CLICK action
     present but its URL is empty in this specific Hover interaction on
     Tablet/Desktop (a real gap in the Figma file, not a misread) — their
     buttons here reuse the same real Mobile URL for the same game rather
     than link nowhere or invent a new destination. */
  .game-card__content {
    display: flex;
    flex-direction: column;
    height: 100%;
    /* the mobile base rule's gap:8px (needed between the always-visible
       image and details there) is overridden to 0 at rest — Tablet/
       Desktop show only the image until hover, no gap to show yet. */
    gap: 0;
    transition: gap 0.3s ease;
  }

  .game-card:hover .game-card__content,
  .game-card:focus-within .game-card__content {
    gap: 8px;
  }

  .game-card__image {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
  }

  /* max-height:0 alone does NOT collapse this to true zero — box-sizing:
     border-box only changes how a specified height is interpreted, it
     can't shrink the box smaller than its own padding, so the 16px
     top+bottom padding (32px total) rendered as a permanent gap even at
     max-height:0. Collapsing the vertical padding to 0 alongside
     max-height (and restoring both on hover) is required to actually
     reach zero. 480px on reveal is a generous technical ceiling for the
     transition, not a design value: every real title+description+button
     combination here is well under that, so the panel's visible height on
     hover is always governed by its actual content, never by this cap. */
  .game-card__details {
    flex: 0 0 auto;
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    overflow: hidden;
    opacity: 0;
    /* opacity was 0.2s while max-height/padding ran 0.3s — content hit full
       opacity a beat before the panel finished expanding, an unintentional
       stagger. Aligned to 0.3s so all three finish together. */
    transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
  }

  .game-card:hover .game-card__details,
  .game-card:focus-within .game-card__details {
    max-height: 480px;
    padding-top: 16px;
    padding-bottom: 16px;
    opacity: 1;
  }

  /* Title/description/button are themselves flex items of the column
     above (.game-card__details is display:flex) — each has a default
     min-height:auto that resolves to its own content-based minimum
     height and ignores the parent's max-height:0 UNLESS the item's own
     overflow is something other than visible. Without this, real card
     text/buttons kept rendering ~24px below the collapsed 0-height
     parent box (visible as a sliver bleeding past the card's rounded
     bottom edge at rest) despite the parent itself measuring 0 height. */
  .game-card__title,
  .game-card__desc,
  .game-card__button-wrap {
    overflow: hidden;
  }

  /* Same box-sizing:border-box padding floor already fixed on
     .game-card__details above — button-wrap's own 8px top+bottom padding
     (from the Mobile base rule) can't shrink below 16px total from
     overflow/min-height alone, so it kept bleeding past the card's bottom
     edge even after the fix above zeroed the button itself. Collapsed
     here, restored alongside the rest of the panel on hover. */
  .game-card__button-wrap {
    padding-top: 0;
    padding-bottom: 0;
    /* Missing here entirely, so the button's padding snapped instantly
       0->8px while .game-card__details eased open around it — added to
       match the panel's own timing. */
    transition: padding 0.3s ease;
  }

  .game-card:hover .game-card__button-wrap,
  .game-card:focus-within .game-card__button-wrap {
    padding-top: 8px;
    padding-bottom: 8px;
  }

  /* Same box-sizing:border-box padding floor as .game-card__details above
     — button-wrap's own 8px top+bottom padding (from the Mobile base
     rule) can't be shrunk below 16px total by overflow/min-height alone,
     so it still bled 16px past the card's bottom edge even after the fix
     above collapsed the button itself to 0. Collapsed here, restored on
     hover alongside the rest of the panel. */
  .game-card__button-wrap {
    padding-top: 0;
    padding-bottom: 0;
  }

  .game-card:hover .game-card__button-wrap,
  .game-card:focus-within .game-card__button-wrap {
    padding-top: 8px;
    padding-bottom: 8px;
  }

  /* Fixed heights confirmed identical on Tablet and Desktop (only the
     widths differ, handled by the flex-basis rules below) */
  .game-card--full {
    height: 720px;
  }

  .game-card--tall {
    height: 880px;
  }

  .game-card--half {
    height: 436px;
  }

  /* .games-main__pair is a ROW container here, so flex-basis governs
     WIDTH on its .game-card children — width:auto lets that flex-basis
     take over from the base 100% width. */
  .games-main__pair > .game-card {
    width: auto;
    flex: 1 1 100%;
  }

  /* .games-main__pair-col is a COLUMN container, so flex-basis on its
     children would instead govern HEIGHT — flex:1 1 100% there was
     overriding each card's fixed height:436px (main-axis flex-basis wins
     over the height property), making Escape Corp/Rooms and Exits taller
     or shorter than Drift/Jungle Society despite sharing the same
     .game-card--half modifier. flex:0 0 auto keeps their own height
     property in control; width already comes from .game-card's base
     100%, correct for a column child. */
  .games-main__pair-col > .game-card {
    flex: 0 0 auto;
  }
}

/* ---- Desktop: 1344px+ ---- */
@media (min-width: 1344px) {
  .games-main {
    gap: 40px;
    padding: 0 100px 40px;
  }

  .games-main__art {
    width: auto;
    max-width: none;
    padding-top: 40px;
  }

  .games-main__title {
    font-family: var(--font-display-pagetitle-desktop-family);
    font-size: var(--font-display-pagetitle-desktop-size);
    font-weight: var(--font-display-pagetitle-desktop-weight);
    line-height: var(--font-display-pagetitle-desktop-line-height);
    --kinetic-letter-spacing: var(--font-display-pagetitle-desktop-letter-spacing);
  }

  /* Desktop is wide enough (1240px content) for real 2-up pairing: Heart
     (tall) beside the Escape/Rooms column, Drift beside Jungle Society —
     confirmed via real x-offsets (624px = 616px card + 8px gap). */
  .games-main__pair-col,
  .games-main__pair > .game-card {
    flex: 1 1 calc(50% - 4px);
  }
}

/* ==========================================================================
   Mobile "stacking cards" effect (Col section) — Games, Video, More, Home

   Not a Figma-fidelity translation — a scroll-driven interactive effect
   requested directly, Apple-case-study style: as the user scrolls, each
   card pins in place and the next one rises to cover it. Mobile only
   (356–695px); Tablet/Desktop keep each page's own layout untouched
   (static Col stack, or — for Home's Projects row — the separate
   horizontal-scroll-hijack effect above, which only ever applies at
   ≥696px and so never competes with this block). Shared verbatim by
   .game-card/.video-card/.more-card/.project-card via the .stack-card
   class added alongside each — no page-specific CSS needed.

   CSS handles the pin + the receding look; js/main.js's IntersectionObserver
   decides WHEN a card counts as "previous" (see that file for how).

   Selector qualifies .stack-card with its element type (article or a),
   not just ".stack-card" bare: .game-card/.video-card/.more-card/
   .project-card each already set position:relative at the same
   (0,0,1,0) specificity — whichever was declared LATER in this file
   would otherwise win and silently cancel position:sticky here (this is
   exactly what happened for Video/More, both declared after this block).
   The extra element qualifier bumps specificity to (0,0,1,1) so this
   wins regardless of declaration order, including for any future page —
   .project-card is an <a>, not an <article>, hence both selectors. */
@media (max-width: 695px) {
  article.stack-card,
  a.stack-card {
    position: sticky;
    top: 80px;
    /* Scale from the top edge, not center — keeps the pinned top edge
       exactly at 80px while receding. Center-origin scaling shifted the
       top edge down a few px, which fed back into the IntersectionObserver
       trigger (see js/main.js) and caused the receding state to flicker
       right as the transform kicked in. */
    transform-origin: top center;
    will-change: transform, opacity;
    transition: transform 0.3s ease, opacity 0.3s ease;
  }

  article.stack-card.is-receding,
  a.stack-card.is-receding {
    transform: scale(0.95);
    opacity: 0.6;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* .stack-card was the only selector covered here — the Tablet/Desktop
     hover-reveal transitions on game/video/more cards (gap, max-height,
     opacity, padding) had no reduced-motion guard at all. Extended to
     cover those too. */
  .stack-card,
  .game-card__content,
  .game-card__details,
  .game-card__button-wrap,
  .video-card__content,
  .video-card__details,
  .video-card__button-wrap,
  .more-card__content,
  .more-card__details,
  .more-card__button-wrap {
    transition: none;
  }
}

/* ==========================================================================
   Video page — Main

   Same "Main" template as Games (Art/title + Col of cards) — identical
   padding/gap numbers confirmed via raw Plugin API (0/24/24 mobile,
   0/64/24 tablet, 0/100/40 desktop; Art pt 16/48/40; Header pt 10 on
   Mobile only; title uses the same Text/Display/PageTitle tokens). Video
   has only ONE grouping though: "Hunting" (tall, 880h) paired with a
   column of "ANIMA"+"SECRETS" (half, 436h each) — no standalone full-width
   cards before/after, unlike Games. Desktop Art has a real 56px
   padding-right (Header width 1184 of Art's 1240) not present on
   Mobile/Tablet (both 0) — a real, confirmed divergence, not invented.

   Cards reuse the exact same recipe as .game-card (identical fills/
   strokes/effects verified via raw API: GRADIENT_LINEAR border opacity
   0.3 = var(--color-gradient-01), GRADIENT_RADIAL 50%/50%-at-50%-75%
   background, 12px backdrop-blur, same triple shadow) and the same
   Mobile-always-visible / Tablet+Desktop-hover-reveal behavior, using the
   same real per-instance Hover-variant text (confirmed identical to
   Mobile's copy via the same temporary-variant-swap-and-revert check).
   Hunting's Hover-state ON_CLICK URL is empty in Figma on Tablet/Desktop
   (same real gap pattern as Games' full-size cards) — its button reuses
   the real Mobile URL for the same video. ANIMA/SECRETS have real URLs
   there matching Mobile exactly. */
/* Footer Reveal support — same role as .partners on Home; see the
   .games-main comment (Games's equivalent section) for the full
   reasoning. */
.video-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 0 24px 24px;
  background: var(--color-black-01);
  position: relative;
  z-index: 2;
}

@supports (animation-timeline: view()) {
  .video-main {
    view-timeline-name: --partners-exit;
  }
}

.video-main__art {
  width: 100%;
  max-width: 800px;
  padding-top: 16px;
}

.video-main__header {
  width: 100%;
  padding-top: 10px;
}

.video-main__title {
  margin: 0;
  width: 100%;
  font-family: var(--font-display-pagetitle-mobile-family);
  font-size: var(--font-display-pagetitle-mobile-size);
  font-weight: var(--font-display-pagetitle-mobile-weight);
  line-height: var(--font-display-pagetitle-mobile-line-height);
  letter-spacing: var(--font-display-pagetitle-mobile-letter-spacing);
  color: var(--color-light-01);
  text-transform: uppercase;
  /* Split into two animated <span>s below (stagger requested) — this real
     token stays declared here unchanged and is also aliased for those
     spans to inherit as their kinetic-blur animation's settled value. */
  --kinetic-letter-spacing: var(--font-display-pagetitle-mobile-letter-spacing);
}

/* Kinetic Variable Font Blur, page-load only — see @keyframes
   kinetic-title-blur near .navbar. No letter-spacing set here: these
   spans inherit --kinetic-letter-spacing from .video-main__title above,
   which already carries the real per-breakpoint token. display:inline-
   block is required for transform:scale to apply reliably to inline
   text. */
.video-main__title-word {
  display: inline-block;
  opacity: 0;
  filter: blur(30px);
  transform: scale(0.88);
  letter-spacing: -0.05em;
  will-change: opacity, filter, transform, letter-spacing;
  animation-name: kinetic-title-blur;
  animation-duration: 1.2s;
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  animation-fill-mode: forwards;
}

.video-main__title-word--second {
  animation-delay: 0.15s;
}

.video-main__col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

/* Mobile has no pairing wrapper at all (Figma's mobile "Col" holds 3 flat
   children) — Tablet/Desktop group Hunting with the ANIMA/SECRETS column
   under "Grid/CardPair" frames. display:contents keeps one honest DOM
   tree: on Mobile these wrappers drop out of layout so their children
   become plain flat items of .video-main__col; Tablet/Desktop turn them
   into real flex containers below. Same technique as .games-main__pair. */
.video-main__pair,
.video-main__pair-col {
  display: contents;
}

.video-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 8px;
  border-radius: 40px;
  overflow: hidden;
  text-decoration: none;
  background: radial-gradient(50% 50% at 50% 75%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

.video-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.video-card__content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.video-card__image {
  position: relative;
  width: 100%;
  height: 223px;
  border-radius: 32px;
  overflow: hidden;
}

.video-card__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Hunting's real source photo is a tall portrait (bird's head near the
   top). At Tablet widths this "tall" card runs full column width while
   its height stays fixed at 880px — a much wider/shallower box than the
   image's own aspect ratio — so default center-cropping cut well into
   the head. Anchoring the crop to the top keeps the head intact and
   crops from the bottom instead; harmless where no vertical crop happens
   at all (Mobile, Desktop's narrower 2-up column). Figma's own fill
   transform for this image is a plain identity/centered FILL (verified
   via raw Plugin API) — this isn't overriding a real Figma crop, just
   compensating for our fixed-height card box needing a wider aspect than
   Figma's own frame ever renders at. */
.video-card__image--top img {
  object-position: center top;
}

.video-card__details {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  padding: 16px;
}

.video-card__title {
  margin: 0;
  font-family: var(--font-heading-cardtitle-mobile-family);
  font-size: var(--font-heading-cardtitle-mobile-size);
  font-weight: var(--font-heading-cardtitle-mobile-weight);
  line-height: var(--font-heading-cardtitle-mobile-line-height);
  letter-spacing: var(--font-heading-cardtitle-mobile-letter-spacing);
  color: var(--color-lime-01);
}

.video-card__desc {
  margin: 0;
  font-family: var(--font-body-description-family);
  font-size: var(--font-body-description-size);
  font-weight: var(--font-body-description-weight);
  line-height: var(--font-body-description-line-height);
  letter-spacing: var(--font-body-description-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.video-card__button-wrap {
  display: flex;
  align-items: center;
  padding: 8px 0;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .video-main {
    align-items: flex-start;
    gap: 24px;
    padding: 0 64px 24px;
  }

  .video-main__art {
    padding-top: 48px;
  }

  .video-main__header {
    padding-top: 0;
  }

  .video-main__title {
    font-family: var(--font-display-pagetitle-tablet-family);
    font-size: var(--font-display-pagetitle-tablet-size);
    font-weight: var(--font-display-pagetitle-tablet-weight);
    line-height: var(--font-display-pagetitle-tablet-line-height);
    letter-spacing: var(--font-display-pagetitle-tablet-letter-spacing);
    --kinetic-letter-spacing: var(--font-display-pagetitle-tablet-letter-spacing);
  }

  .video-main__col {
    gap: 8px;
  }

  .video-main__pair,
  .video-main__pair-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
  }

  .video-main__pair {
    flex-direction: row;
    flex-wrap: wrap;
  }

  /* Tablet's card content-width (568px) is never wide enough to fit two
     cards side by side, so the "Grid/CardPair" pairing reflows to a plain
     vertical stack here — same real reflow already confirmed for Games. */

  .video-card__content {
    display: flex;
    flex-direction: column;
    height: 100%;
    gap: 0;
    transition: gap 0.3s ease;
  }

  .video-card:hover .video-card__content,
  .video-card:focus-within .video-card__content {
    gap: 8px;
  }

  .video-card__image {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
  }

  .video-card__details {
    flex: 0 0 auto;
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    overflow: hidden;
    opacity: 0;
    /* opacity was 0.2s while max-height/padding ran 0.3s — content hit full
       opacity a beat before the panel finished expanding, an unintentional
       stagger. Aligned to 0.3s so all three finish together. */
    transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
  }

  .video-card:hover .video-card__details,
  .video-card:focus-within .video-card__details {
    max-height: 480px;
    padding-top: 16px;
    padding-bottom: 16px;
    opacity: 1;
  }

  /* Title/description/button are themselves flex items of the column
     above (.video-card__details is display:flex) — each has a default
     min-height:auto that resolves to its own content-based minimum
     height and ignores the parent's max-height:0 UNLESS the item's own
     overflow is something other than visible. Without this, real card
     text/buttons kept rendering ~24px below the collapsed 0-height
     parent box (visible as a sliver bleeding past the card's rounded
     bottom edge at rest) despite the parent itself measuring 0 height —
     same underlying bug as .game-card, fixed the same way. */
  .video-card__title,
  .video-card__desc,
  .video-card__button-wrap {
    overflow: hidden;
  }

  /* Same box-sizing:border-box padding floor already fixed on
     .video-card__details above — button-wrap's own 8px top+bottom
     padding (from the Mobile base rule) can't shrink below 16px total
     from overflow/min-height alone, so it kept bleeding past the card's
     bottom edge even after the fix above zeroed the button itself.
     Collapsed here, restored alongside the rest of the panel on hover. */
  .video-card__button-wrap {
    padding-top: 0;
    padding-bottom: 0;
    /* Missing here entirely, so the button's padding snapped instantly
       0->8px while .video-card__details eased open around it — added to
       match the panel's own timing. */
    transition: padding 0.3s ease;
  }

  .video-card:hover .video-card__button-wrap,
  .video-card:focus-within .video-card__button-wrap {
    padding-top: 8px;
    padding-bottom: 8px;
  }

  /* Fixed heights confirmed identical on Tablet and Desktop */
  .video-card--tall {
    height: 880px;
  }

  .video-card--half {
    height: 436px;
  }

  .video-main__pair > .video-card {
    width: auto;
    flex: 1 1 100%;
  }

  .video-main__pair-col > .video-card {
    flex: 0 0 auto;
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  .video-main {
    gap: 40px;
    padding: 0 100px 40px;
  }

  .video-main__art {
    width: auto;
    max-width: none;
    padding-top: 40px;
    padding-right: 56px;
  }

  .video-main__title {
    font-family: var(--font-display-pagetitle-desktop-family);
    font-size: var(--font-display-pagetitle-desktop-size);
    font-weight: var(--font-display-pagetitle-desktop-weight);
    line-height: var(--font-display-pagetitle-desktop-line-height);
    letter-spacing: var(--font-display-pagetitle-desktop-letter-spacing);
    --kinetic-letter-spacing: var(--font-display-pagetitle-desktop-letter-spacing);
  }

  /* Desktop is wide enough (1240px content) for real 2-up pairing: Hunting
     (tall) beside the ANIMA/SECRETS column — confirmed via real x-offsets
     (624px = 616px card + 8px gap), same pattern as Games. */
  .video-main__pair-col,
  .video-main__pair > .video-card {
    flex: 1 1 calc(50% - 4px);
  }
}

/* ==========================================================================
   More page — Main

   Same "Main" template as Games/Video (Art/title + Col of cards) — identical
   padding/gap numbers confirmed via raw Plugin API (0/24/24 mobile,
   0/64/24 tablet, 0/100/40 desktop; Art pt 16/48/40, desktop Art also has
   the same real 56px padding-right as Video; Header pt 10 on Mobile only;
   title uses the same Text/Display/PageTitle tokens — raw characters are
   literally "MORE", already upper-case in Figma, textCase:UPPER on top).

   Structure differs from Games/Video though: just ONE pair of two "half"
   cards (Comic 436h + Gel 436h, HORIZONTAL Grid/CardPair at Desktop,
   confirmed via real x-offsets 624=616+8; stacks at Tablet same as
   elsewhere) followed by ONE standalone "full" card (Capsul, 720h) below
   — no "tall" card, no nested pair-col, simpler than Games/Video.

   Cards reuse the exact same recipe as .game-card/.video-card (identical
   fills/strokes/effects verified via raw API) and the same Mobile-always-
   visible / Tablet+Desktop-hover-reveal behavior with real per-instance
   Hover-variant text (confirmed identical to Mobile's copy via the same
   temporary-variant-swap-and-revert check). Comic/Gel have real ON_CLICK
   URLs there matching Mobile exactly; Capsul's Hover-state has no
   ON_CLICK reaction at all on Tablet/Desktop (same real gap pattern as
   Games'/Video's full-size cards) — its button reuses the real Mobile
   URL for the same project. */
/* Footer Reveal support — same role as .partners on Home; see the
   .games-main comment (Games's equivalent section) for the full
   reasoning. */
.more-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 0 24px 24px;
  background: var(--color-black-01);
  position: relative;
  z-index: 2;
}

@supports (animation-timeline: view()) {
  .more-main {
    view-timeline-name: --partners-exit;
  }
}

.more-main__art {
  width: 100%;
  max-width: 800px;
  padding-top: 16px;
}

.more-main__header {
  width: 100%;
  padding-top: 10px;
}

.more-main__title {
  margin: 0;
  width: 100%;
  font-family: var(--font-display-pagetitle-mobile-family);
  font-size: var(--font-display-pagetitle-mobile-size);
  font-weight: var(--font-display-pagetitle-mobile-weight);
  line-height: var(--font-display-pagetitle-mobile-line-height);
  color: var(--color-light-01);
  text-transform: uppercase;
  /* Kinetic Variable Font Blur, page-load only — see @keyframes
     kinetic-title-blur near .navbar. letter-spacing is now driven by that
     animation (from -0.05em to the real token below) instead of being set
     directly. */
  --kinetic-letter-spacing: var(--font-display-pagetitle-mobile-letter-spacing);
  letter-spacing: -0.05em;
  opacity: 0;
  filter: blur(30px);
  transform: scale(0.88);
  will-change: opacity, filter, transform, letter-spacing;
  animation-name: kinetic-title-blur;
  animation-duration: 1.2s;
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  animation-fill-mode: forwards;
}

.more-main__col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

/* Mobile has no pairing wrapper at all (Figma's mobile "Col" holds 3 flat
   children) — Tablet/Desktop group Comic+Gel under a "Grid/CardPair"
   frame. display:contents keeps one honest DOM tree: on Mobile this
   wrapper drops out of layout so its children become plain flat items of
   .more-main__col; Tablet/Desktop turn it into a real flex container
   below. Same technique as .games-main__pair/.video-main__pair. */
.more-main__pair {
  display: contents;
}

.more-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 8px;
  border-radius: 40px;
  overflow: hidden;
  text-decoration: none;
  background: radial-gradient(50% 50% at 50% 75%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.02) 100%);
  backdrop-filter: blur(12px);
  box-shadow:
    0px 1px 2px 0px rgba(0, 0, 0, 0.04),
    0px 3px 8px 0px rgba(0, 0, 0, 0.12),
    0px 8px 16px 0px rgba(0, 0, 0, 0.6);
}

.more-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: var(--color-gradient-01);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.more-card__content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.more-card__image {
  position: relative;
  width: 100%;
  height: 223px;
  border-radius: 32px;
  overflow: hidden;
}

.more-card__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.more-card__details {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  padding: 16px;
}

.more-card__title {
  margin: 0;
  font-family: var(--font-heading-cardtitle-mobile-family);
  font-size: var(--font-heading-cardtitle-mobile-size);
  font-weight: var(--font-heading-cardtitle-mobile-weight);
  line-height: var(--font-heading-cardtitle-mobile-line-height);
  letter-spacing: var(--font-heading-cardtitle-mobile-letter-spacing);
  color: var(--color-lime-01);
}

.more-card__desc {
  margin: 0;
  font-family: var(--font-body-description-family);
  font-size: var(--font-body-description-size);
  font-weight: var(--font-body-description-weight);
  line-height: var(--font-body-description-line-height);
  letter-spacing: var(--font-body-description-letter-spacing);
  color: var(--color-light-01);
  opacity: 0.6;
}

.more-card__button-wrap {
  display: flex;
  align-items: center;
  padding: 8px 0;
}

/* ---- Tablet: 696px – 1343px ---- */
@media (min-width: 696px) {
  .more-main {
    align-items: flex-start;
    gap: 24px;
    padding: 0 64px 24px;
  }

  .more-main__art {
    padding-top: 48px;
  }

  .more-main__header {
    padding-top: 0;
  }

  .more-main__title {
    font-family: var(--font-display-pagetitle-tablet-family);
    font-size: var(--font-display-pagetitle-tablet-size);
    font-weight: var(--font-display-pagetitle-tablet-weight);
    line-height: var(--font-display-pagetitle-tablet-line-height);
    --kinetic-letter-spacing: var(--font-display-pagetitle-tablet-letter-spacing);
  }

  .more-main__col {
    gap: 8px;
  }

  .more-main__pair {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    gap: 8px;
    width: 100%;
  }

  /* Tablet's card content-width (568px) is never wide enough to fit two
     cards side by side, so the "Grid/CardPair" pairing reflows to a plain
     vertical stack here — same real reflow already confirmed for Games/
     Video. */

  .more-card__content {
    display: flex;
    flex-direction: column;
    height: 100%;
    gap: 0;
    transition: gap 0.3s ease;
  }

  .more-card:hover .more-card__content,
  .more-card:focus-within .more-card__content {
    gap: 8px;
  }

  .more-card__image {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
  }

  .more-card__details {
    flex: 0 0 auto;
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    overflow: hidden;
    opacity: 0;
    /* opacity was 0.2s while max-height/padding ran 0.3s — content hit full
       opacity a beat before the panel finished expanding, an unintentional
       stagger. Aligned to 0.3s so all three finish together. */
    transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
  }

  .more-card:hover .more-card__details,
  .more-card:focus-within .more-card__details {
    max-height: 480px;
    padding-top: 16px;
    padding-bottom: 16px;
    opacity: 1;
  }

  /* Same box-sizing:border-box padding floor already fixed on
     .game-card__details/.video-card__details — button-wrap's own 8px
     top+bottom padding can't shrink below 16px total from overflow/
     min-height alone, so it kept bleeding past the card's bottom edge.
     Collapsed here, restored alongside the rest of the panel on hover. */
  .more-card__title,
  .more-card__desc,
  .more-card__button-wrap {
    overflow: hidden;
  }

  .more-card__button-wrap {
    padding-top: 0;
    padding-bottom: 0;
    /* Missing here entirely, so the button's padding snapped instantly
       0->8px while .more-card__details eased open around it — added to
       match the panel's own timing. */
    transition: padding 0.3s ease;
  }

  .more-card:hover .more-card__button-wrap,
  .more-card:focus-within .more-card__button-wrap {
    padding-top: 8px;
    padding-bottom: 8px;
  }

  /* Fixed heights confirmed identical on Tablet and Desktop */
  .more-card--half {
    height: 436px;
  }

  .more-card--full {
    height: 720px;
  }

  .more-main__pair > .more-card {
    width: auto;
    flex: 1 1 100%;
  }
}

/* ---- Desktop: 1344px і ширше ---- */
@media (min-width: 1344px) {
  .more-main {
    gap: 40px;
    padding: 0 100px 40px;
  }

  .more-main__art {
    width: auto;
    max-width: none;
    padding-top: 40px;
    padding-right: 56px;
  }

  .more-main__title {
    font-family: var(--font-display-pagetitle-desktop-family);
    font-size: var(--font-display-pagetitle-desktop-size);
    font-weight: var(--font-display-pagetitle-desktop-weight);
    line-height: var(--font-display-pagetitle-desktop-line-height);
    --kinetic-letter-spacing: var(--font-display-pagetitle-desktop-letter-spacing);
  }

  /* Desktop is wide enough (1240px content) for real 2-up pairing: Comic
     beside Gel — confirmed via real x-offsets (624px = 616px card + 8px
     gap), same pattern as Games/Video. */
  .more-main__pair {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .more-main__pair > .more-card {
    flex: 1 1 calc(50% - 4px);
  }
}

/* Moved to end-of-file deliberately: with equal selector specificity, the
   LATER rule in source order wins regardless of media-query nesting — an
   earlier placement (right after the shared @keyframes near .navbar) was
   silently overridden by each page's own later breakpoint rules, so the
   animation kept running even under prefers-reduced-motion. Placed after
   every rule it needs to beat. */
@media (prefers-reduced-motion: reduce) {
  .games-main__title,
  .more-main__title,
  .video-main__title-word {
    animation: none;
    opacity: 1;
    filter: none;
    transform: none;
    letter-spacing: var(--kinetic-letter-spacing);
  }
}

/* Same end-of-file placement, same reason: these need to win the
   specificity tie against .vas__tag p / .vas__welcome-text's own later
   declarations (mobile base + Tablet/Desktop overrides earlier in this
   file). */
@media (prefers-reduced-motion: reduce) {
  .vas__tag p {
    animation: none;
    opacity: 0.88;
    transform: none;
  }

  .vas__welcome-text {
    animation: none;
    opacity: 1;
    transform: rotate(180deg);
  }
}

/* Same end-of-file placement, same reason: needs to win the specificity
   tie against .service-card / .service-card__eyebrow's own base rule
   earlier in this file. Content stays fully legible without relying on
   the scroll-triggered highlight. */
@media (prefers-reduced-motion: reduce) {
  .service-card {
    transition: none;
    opacity: 1;
  }

  .service-card__eyebrow {
    transition: none;
    color: var(--color-lime-01);
  }
}

/* Same end-of-file placement, same reason: needs to win the specificity
   tie against .partners__marquee's own base rule earlier in this file.
   Stopping the animation alone would leave both the real and the
   aria-hidden duplicate track sitting statically side by side (14 logos
   instead of 7) — hiding the duplicate keeps the fallback to one real,
   readable row instead of a visible content-duplication glitch. */
@media (prefers-reduced-motion: reduce) {
  .partners__marquee {
    animation: none;
  }

  .partners__track[aria-hidden="true"] {
    display: none;
  }
}

/* Same end-of-file placement, same reason: needs to win the specificity
   tie against each of these selectors' own base rule earlier in this
   file (.footer__collaborate, the two footer link rules, .footer__email,
   .footer__location-dot::after). Color/opacity feedback stays (not real
   motion); transforms and the scroll-driven/infinite animations are
   disabled, landing directly on each element's settled state. */
@media (prefers-reduced-motion: reduce) {
  .footer__collaborate {
    animation: none;
    transform: none;
    letter-spacing: var(--font-display-collaborate-mobile-letter-spacing);
  }

  .footer-menu__link,
  .footer-connect__link {
    transition: color 0.3s cubic-bezier(0.25, 1, 0.5, 1);
  }

  .footer-menu__link:hover,
  .footer-menu__link:focus-visible,
  .footer-connect__link:hover,
  .footer-connect__link:focus-visible {
    transform: none;
  }

  .footer__email {
    transition: none;
  }

  .footer__email:hover,
  .footer__email:focus-visible {
    transform: none;
  }

  .footer__location-dot::after {
    animation: none;
    opacity: 0;
  }
}
