/*
  William Veleda, Portfolio
  Visual direction inspired by the hero structure of motionsites.ai
  (dark monochrome palette, mixed sans and italic serif type,
  a draggable 3D ring of project images, central prompt style
  microinteraction). Nothing here is a copy of that site, only the
  same visual language.
*/

:root {
  --bgBlack: #030303;
  --bgPanel: #0c0c0c;
  --lineColor: rgba(255, 255, 255, 0.1);
  --textWhite: #f5f5f5;
  --textGray: #f5f5f5;
  --textGrayDim: #f5f5f5;
  --chrome1: #eef0f2;
  --chrome2: #8b8f94;
  --chrome3: #3a3d40;
  --radiusPill: 999px;
  --radiusCard: 18px;
  --maxWidth: 1180px;
  --fontSans: "Inter", system-ui, sans-serif;
  --fontSerif: "Playfair Display", Georgia, serif;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bgBlack);
  color: var(--textWhite);
  font-family: var(--fontSans);
  line-height: 1.5;
  overflow-x: hidden;
}

a {
  color: inherit;
}

img {
  max-width: 100%;
  display: block;
}

.sectionInner {
  max-width: var(--maxWidth);
  margin: 0 auto;
  padding: 0 32px;
}

.noiseOverlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  background: radial-gradient(circle at 50% 0%, rgba(255,255,255,0.05), transparent 60%);
}

/* VANTA BACKGROUND */
/* Fixed, full viewport canvas holding the Vanta Fog effect from
   script.js, sitting behind every section for the whole scroll of the
   page. body keeps its own dark color as a fallback for the instant
   before Vanta loads, or for visitors with WebGL disabled. main and
   the footer get their own stacking context above it so the fog never
   covers readable content. */

#vantaBg {
  position: fixed;
  inset: 0;
  z-index: 0;
}

main,
.siteFooter {
  position: relative;
  z-index: 1;
}

/* LANGUAGE PICKER (first visit only) */
/* A centered modal over a blurred backdrop, shown once by
   setupLanguagePicker in script.js when no language choice has been
   saved yet. Same dark panel and pill button language as the rest of
   the page, just layered above .siteNav (z-index 40) so it reads as a
   deliberate step before the page, not another piece of it. */
.langPickerModal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(3, 3, 3, 0.72);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 0.25s ease;
}

.langPickerModal.isVisible {
  opacity: 1;
}

.langPickerCard {
  width: 100%;
  max-width: 320px;
  padding: 32px 28px;
  background: var(--bgPanel);
  border: 1px solid var(--lineColor);
  border-radius: var(--radiusCard);
  text-align: center;
  transform: translateY(8px);
  transition: transform 0.25s ease;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

.langPickerModal.isVisible .langPickerCard {
  transform: translateY(0);
}

.langPickerTitle {
  margin: 0 0 20px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--textWhite);
}

.langPickerOptions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.langPickerButton {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px 20px;
  border-radius: var(--radiusPill);
  border: 1px solid var(--lineColor);
  background: rgba(255, 255, 255, 0.03);
  color: var(--textWhite);
  font-family: var(--fontSans);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}

.langPickerButton .langFlag {
  font-size: 18px;
  line-height: 1;
}

.langPickerButton:hover,
.langPickerButton:focus-visible {
  background: var(--textWhite);
  color: var(--bgBlack);
  transform: translateY(-1px);
}

/* NAV */

.siteNav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;
  background: rgba(3, 3, 3, 0.7);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--lineColor);
}

.navInner {
  max-width: var(--maxWidth);
  margin: 0 auto;
  padding: 14px 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.navBrand {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}

.brandAvatar {
  /* Doubled from the original 34px, per William's request, brandText
     font sizes below stay untouched. */
  width: 68px;
  height: 68px;
  flex-shrink: 0;
  border-radius: 50%;
  position: relative;
  /* Dark badge instead of the light chrome circle used on service and
     hero prompt icons: the brand mark (images/brandmark.png) is
     already a light on dark mark, so it needs a dark backdrop to
     read the same way the source artwork did. */
  background: radial-gradient(circle at 50% 35%, #1c1c1c, #050505);
  border: 1px solid rgba(255, 255, 255, 0.14);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Same layered white glow used on hover elsewhere on the page
     (portfolio cards, the footer social icon), kept on all the time
     here instead of hover only, plus a spinning highlight ring on
     ::before below for the moving part of the effect. */
  box-shadow:
    0 0 0 1px rgba(245, 245, 245, 0.28),
    0 0 18px 2px rgba(245, 245, 245, 0.18),
    0 0 36px 8px rgba(245, 245, 245, 0.1);
}

/* Bright arc that fades into nothing the rest of the way around,
   masked down to a thin ring just outside the badge, spinning at a
   steady, constant pace. A plain even ring would not read as moving at
   all, the fading arc is what makes the spin visible. */
.brandAvatar::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    rgba(245, 245, 245, 0) 0%,
    rgba(245, 245, 245, 0.9) 10%,
    rgba(245, 245, 245, 0) 35%,
    rgba(245, 245, 245, 0) 100%
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  animation: brandNeonSpin 3.5s linear infinite;
  pointer-events: none;
}

@keyframes brandNeonSpin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .brandAvatar::before {
    animation: none;
  }
}

.brandAvatarMark {
  /* Doubled to match .brandAvatar above. */
  width: 50px;
  height: auto;
  display: block;
}

.brandText {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  min-width: 0;
}

.brandText strong {
  font-size: 14px;
  font-weight: 600;
}

.brandText small {
  font-size: 12px;
  color: var(--textGray);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* A little extra breathing room under "William Veleda", now that the
     avatar next to it is noticeably taller, on top of line-height above. */
  margin-top: 4px;
}

/* Narrow bar that exists only to hold the back to top button, kept
   separate from navLinks and navMenuToggle instead of sharing their row
   directly, see the mobile rule further down for why. */
.navUtilityBar {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Language toggle, EN and PT shown together with a small flag plus
   abbreviation each, styled as one segmented pill so it reads as a
   single control rather than two separate buttons, matching the same
   monochrome pillButton language used everywhere else on the page.
   Both options stay visible at once (rather than swapping to show only
   the other choice) so a visitor always sees which language is active
   and what the other option is, without needing to guess. */
.langToggle {
  display: flex;
  align-items: center;
  padding: 3px;
  border: 1px solid var(--lineColor);
  border-radius: var(--radiusPill);
  background: rgba(255, 255, 255, 0.03);
}

.langToggleButton {
  display: flex;
  align-items: center;
  gap: 5px;
  border: none;
  background: none;
  color: var(--textGrayDim);
  font-family: var(--fontSans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 5px 10px;
  border-radius: var(--radiusPill);
  cursor: pointer;
  transition: color 0.25s ease, background 0.25s ease;
}

.langToggleButton .langFlag {
  font-size: 13px;
  line-height: 1;
}

.langToggleButton:hover {
  color: var(--textWhite);
}

/* Same solid light fill used on .navBackToTop, so the active language
   reads as clearly pressed against the dark pill around it. */
.langToggleButton.isActive {
  background: var(--textWhite);
  color: var(--bgBlack);
}

.navLinks {
  display: flex;
  gap: 26px;
  font-size: 14px;
}

.navLinks a {
  position: relative;
  text-decoration: none;
  color: var(--textGray);
  transition: color 0.2s ease;
}

.navLinks a:hover {
  color: var(--textWhite);
}

/* Neon underline that sweeps from the first letter to the last on
   hover or keyboard focus, instead of just appearing all at once. */
.navLinks a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -5px;
  width: 100%;
  height: 1px;
  background: var(--textWhite);
  box-shadow: 0 0 6px 1px rgba(245, 245, 245, 0.65);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s ease;
}

.navLinks a:hover::after,
.navLinks a:focus-visible::after {
  transform: scaleX(1);
}

.navMenuToggle {
  display: none;
  flex-direction: column;
  gap: 4px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
}

.navMenuToggle span {
  width: 20px;
  height: 2px;
  background: var(--textWhite);
}

/* HERO */

.hero {
  position: relative;
  /* min height 100vh with everything below trimmed down (padding,
     gaps, font sizes, ring height) so the whole section actually fits
     one screen instead of needing a scroll to see it in full. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 104px 24px 20px;
  gap: 26px;
}

.heroCopy {
  max-width: 720px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.heroHeadline {
  margin: 0;
  font-size: clamp(30px, 5vw, 50px);
  font-weight: 600;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.lineSans {
  font-family: var(--fontSans);
}

.lineSerif {
  font-family: var(--fontSerif);
  font-style: italic;
  font-weight: 500;
  color: var(--textWhite);
}

.heroSubtitle {
  margin: 0;
  color: var(--textGray);
  font-size: 15px;
  max-width: 560px;
}

.heroActions {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}

.pillButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 26px;
  border-radius: var(--radiusPill);
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.pillButton:hover {
  transform: translateY(-1px);
}

.pillButtonPrimary {
  background: var(--textWhite);
  color: #0a0a0a;
  --button-background: var(--textWhite);
}

.pillButtonGhost {
  background: transparent;
  color: var(--textWhite);
  border-color: var(--lineColor);
  --button-background: transparent;
}

.pillButtonDark {
  background: #1c1c1c;
  color: var(--textWhite);
  border-color: var(--lineColor);
  --button-background: #1c1c1c;
}

/* GLOW BUTTON */
/* Adapted from a pointer tracking glow effect William found (originally
   a purple to blue gradient), ported to the site's monochrome chrome
   palette instead of introducing any hue. Applied on top of the three
   pillButton variants above, which is why this only sets structure and
   reads --button-background from whichever variant class is present,
   rather than owning its own background color. The outer element (the
   real <a> or <button>) becomes just a positioning shell: its own
   background and padding are zeroed out here, and the inner <span>
   takes over as the visible surface, since the shine ring and the
   pointer glow both need to clip to that surface with a mask rather
   than to the whole hit area. See setupGlowButtons in script.js for the
   pointermove logic that drives --pointer-x, --pointer-y and
   --button-glow. */
.glowButton {
  position: relative;
  z-index: 1;
  padding: 0;
  background: none;
  box-shadow: 0 8px 20px rgba(245, 245, 245, 0.1);
  --button-shine-left: rgba(255, 255, 255, 0.3);
  --button-shine-right: rgba(220, 222, 225, 0.65);
  --button-glow-start: #ffffff;
  --button-glow-end: #6c6f73;
}

.glowButton .gradient {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  /* A flat white to white mask, not a real fade: this only exists so
     Safari treats the layer as masked (needed for the shine sweep
     below to clip to the pill shape), without actually fading
     anything out. */
  -webkit-mask-image: -webkit-radial-gradient(white, white);
}

/* This is the piece that actually explains the notch: on a
   .pillButtonGhost (transparent) glow button, .glowButton > span below
   never paints a background of its own, so this rotating circle is the
   only thing filling the pill at all. The circle's own width has
   always been tied to the button's width (left/right: 0), which keeps
   it a true circle, but everything visible of it comes from a fixed
   50px tall band near its own top edge (top: 0, nudged up 44px by the
   transform below). Near the top of a circle the chord across it
   narrows fast, and how fast depends on the circle's radius: a big
   radius stays wide for longer near the top, a small one narrows
   almost immediately. Because the radius here was always exactly half
   the button's own width, a short button (English labels) already sat
   close to that narrow region, and a much wider button (the longer
   Portuguese labels) made the radius bigger in absolute terms but the
   visible band's distance from the top stayed the fixed 44px, so
   proportionally it sat even further into that narrowing curve,
   uncovering an ever bigger sliver at each end, exactly the notch
   reported. Sizing the circle to three times the button's width
   instead (via left/width here, offsetting by minus 100% to keep it
   centered) keeps that same 44px band comfortably inside the circle's
   wide equatorial region at any button width, so the sliver never
   opens up. */
.glowButton .gradient::before {
  content: "";
  position: absolute;
  top: 0;
  left: -100%;
  width: 300%;
  transform: scale(1.05) translateY(-44px) rotate(0deg) translateZ(0);
  padding-bottom: 300%;
  border-radius: 50%;
  background: linear-gradient(90deg, var(--button-shine-left), var(--button-shine-right));
  animation: glowButtonRotate linear 2s infinite;
}

@keyframes glowButtonRotate {
  to {
    transform: scale(1.05) translateY(-44px) rotate(360deg) translateZ(0);
  }
}

.glowButton > span {
  z-index: 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 13px 26px;
  box-sizing: border-box;
  border-radius: inherit;
  background-color: var(--button-background);
  overflow: hidden;
  /* Same flat mask fix as .glowButton .gradient above, this is the
     layer that actually paints the button's fill color, so a real
     fade here is what caused the visible notch on wide buttons. */
  -webkit-mask-image: -webkit-radial-gradient(white, white);
}

.glowButton > span::before {
  content: "";
  position: absolute;
  left: -16px;
  top: -16px;
  transform: translate(var(--pointer-x, 0px), var(--pointer-y, 0px)) translateZ(0);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background-color: var(--button-glow, transparent);
  opacity: var(--button-glow-opacity, 0);
  transition: opacity var(--button-glow-duration, 0.5s);
  filter: blur(20px);
}

.glowButton:hover {
  --button-glow-opacity: 1;
  --button-glow-duration: 0.25s;
}

/* Hero central prompt, echoing the quiz style microinteraction from the reference */

.heroPrompt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  min-height: 76px;
  justify-content: center;
}

/* CONTACT SPHERE */
/* A ring of 21 glowing circles, each tilted flat and stacked along the
   Z axis, that reads as a rotating wireframe sphere once the whole
   group spins. Adapted from a snippet William found (originally a
   full viewport orange sphere, sized in vmin), rebuilt around a --u
   unit instead so the same shape fits at whatever scale its wrapper
   needs: every value that was originally "Nvmin" is now
   calc(var(--u) * N). The color is the site's own white instead of
   the original orange, which also means the per ring hue rotate the
   original used to spread a rainbow across the rings has been dropped
   entirely, since rotating the hue of a color that has no hue left to
   rotate does nothing: this is why the shine keyframes below only
   fade the ring out and back rather than also shifting color the way
   the source demo did. This effect used to live in the hero, right
   above the prompt text, and now wraps the brand mark in the Contact
   section instead: see .contactSphere below for the sizing and the
   perspective context it needs to render as a sphere rather than a
   flat rosette. */
.sphere {
  --u: 1.28px;
  --s: 2s;
  --d: 40;
  --c: var(--textWhite);
  width: calc(var(--u) * 50);
  height: calc(var(--u) * 50);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: heroSphereSpin 9s linear 0s infinite;
}

.sphere:hover {
  animation-play-state: paused;
}

@keyframes heroSphereSpin {
  0% {
    transform: rotateX(-210deg) rotateY(0deg) rotateZ(185deg);
  }
  100% {
    transform: rotateX(-210deg) rotateY(360deg) rotateZ(185deg);
  }
}

.sphere,
.sphere div,
.sphere div::before {
  transform-style: preserve-3d;
}

.sphere div {
  --sz: 23%;
  --dv: calc(var(--s) / var(--d));
  --dl: calc(var(--dv) * -1);
  width: var(--sz);
  height: var(--sz);
  position: absolute;
  border: 1px solid var(--c);
  border-radius: 100%;
  transform: rotateX(90deg) translateZ(calc(var(--u) * 24.5));
  box-shadow: 0 0 2px 1px var(--c), 0 0 2px 1px var(--c) inset;
  animation: heroSphereShine var(--s) linear var(--dl) infinite;
}

@keyframes heroSphereShine {
  50%,
  100% {
    border-color: transparent;
    box-shadow: 0 0 1px 0 transparent, 0 0 1px 0 transparent inset;
  }
}

.sphere div::before {
  content: "";
  width: 100%;
  height: 100%;
  position: absolute;
  border: 1px solid var(--c);
  border-radius: 100%;
  box-shadow: 0 0 2px 1px var(--c), 0 0 2px 1px var(--c) inset;
  animation: heroSphereShine var(--s) linear calc(var(--dl) * -1) infinite;
}

.sphere div:nth-child(2) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 22.5));
  --sz: 43%;
  --dl: calc(var(--dv) * -2);
}
.sphere div:nth-child(3) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 20));
  --sz: 60%;
  --dl: calc(var(--dv) * -3);
}
.sphere div:nth-child(4) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 17.5));
  --sz: 71%;
  --dl: calc(var(--dv) * -4);
}
.sphere div:nth-child(5) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 15));
  --sz: 80%;
  --dl: calc(var(--dv) * -5);
}
.sphere div:nth-child(6) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 12.5));
  --sz: 86%;
  --dl: calc(var(--dv) * -6);
}
.sphere div:nth-child(7) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 10));
  --sz: 91%;
  --dl: calc(var(--dv) * -7);
}
.sphere div:nth-child(8) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 7.5));
  --sz: 95%;
  --dl: calc(var(--dv) * -8);
}
.sphere div:nth-child(9) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 5));
  --sz: 97%;
  --dl: calc(var(--dv) * -9);
}
.sphere div:nth-child(10) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * 2.5));
  --sz: 99%;
  --dl: calc(var(--dv) * -10);
}
.sphere div:nth-child(11) {
  transform: rotateX(90deg);
  --sz: 100%;
  --dl: calc(var(--dv) * -11);
}
.sphere div:nth-child(12) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -2.5));
  --sz: 99%;
  --dl: calc(var(--dv) * -12);
}
.sphere div:nth-child(13) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -5));
  --sz: 97%;
  --dl: calc(var(--dv) * -13);
}
.sphere div:nth-child(14) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -7.5));
  --sz: 95%;
  --dl: calc(var(--dv) * -14);
}
.sphere div:nth-child(15) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -10));
  --sz: 91%;
  --dl: calc(var(--dv) * -15);
}
.sphere div:nth-child(16) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -12.5));
  --sz: 86%;
  --dl: calc(var(--dv) * -16);
}
.sphere div:nth-child(17) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -15));
  --sz: 80%;
  --dl: calc(var(--dv) * -17);
}
.sphere div:nth-child(18) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -17.5));
  --sz: 71%;
  --dl: calc(var(--dv) * -18);
}
.sphere div:nth-child(19) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -20));
  --sz: 60%;
  --dl: calc(var(--dv) * -19);
}
.sphere div:nth-child(20) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -22.5));
  --sz: 43%;
  --dl: calc(var(--dv) * -20);
}
.sphere div:nth-child(21) {
  transform: rotateX(90deg) translateZ(calc(var(--u) * -24.5));
  --sz: 23%;
  --dl: calc(var(--dv) * -21);
}

.heroPromptText {
  font-size: clamp(18px, 3.4vw, 27px);
  font-weight: 500;
  color: var(--textGray);
  min-height: 34px;
}

.heroPromptText strong {
  color: var(--textWhite);
  font-weight: 600;
}

.heroPromptCursor {
  animation: blinkCursor 1s step-start infinite;
  color: var(--textWhite);
}

@keyframes blinkCursor {
  50% { opacity: 0; }
}

/* Portfolio ring carousel */
/* A draggable 3D ring of project page images, built with GSAP and its
   Draggable plugin (both loaded from a CDN in index.html). Adapted from
   a standalone demo that was written for a full page, so it originally
   set html, body and every div to position absolute and hid all page
   overflow. That would have broken scrolling and every other section
   on this one page site, so here everything is scoped to its own
   wrapper instead, sized to fit inside the hero rather than the whole
   viewport, and the drag zone only covers this ring, not the full page. */

.ringWrap {
  position: relative;
  width: 100%;
  height: 230px;
  overflow: hidden;
  background: transparent;
}

.ringContainer {
  position: absolute;
  perspective: 1600px;
  /* Matches the roughly 1470 by 835 aspect ratio of the real project
     screenshots (about 1.76 to 1), instead of a square, so each card
     shows the full screenshot with no cropping. */
  width: 340px;
  height: 193px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: transparent;
}

#portfolioRing {
  position: absolute;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  user-select: none;
  background: transparent;
}

.ringImg {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 12px;
  background-color: transparent;
  background-repeat: no-repeat;
  /* Card now shares the screenshots' own aspect ratio (see
     .ringContainer), so the background can simply fill the card
     completely with no cropping and no stretching, regardless of the
     source file's raw resolution (much larger, for retina sharpness). */
  background-size: 100% 100%;
  background-position: center;
  border: none;
  box-shadow: none;
  transform-style: preserve-3d;
  user-select: none;
  /* Monochrome to match the Vanta Fog background behind the whole page,
     instead of showing each screenshot in full color. */
  filter: grayscale(1) brightness(0.85) contrast(1.1);
  transition: filter 0.3s ease;
}

.ringImg:hover {
  filter: grayscale(0.5) brightness(1) contrast(1.05);
}

/* Mirrored reflection under each ring card. Shares the exact same
   rotateY, z and transformOrigin as its paired .ringImg (set together
   in setupPortfolioRing, script.js), so it rides along with the ring
   rotation and any drag in perfect lockstep, only offset downward and
   flipped vertically through scaleY, which also flips the background
   image itself into a proper reflection. The mask fades it out toward
   the bottom, strongest right under the card, fully gone a little
   further down, so it reads as a soft, subtle reflection rather than a
   second visible copy of the card. */
.ringReflection {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 12px;
  background-color: transparent;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  background-position: center;
  border: none;
  box-shadow: none;
  transform-style: preserve-3d;
  user-select: none;
  pointer-events: none;
  filter: grayscale(1) brightness(0.65) contrast(1.1);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
}

#portfolioRingDragger {
  position: absolute;
  inset: 0;
  background: transparent;
  /* Doubles as a click target (jumps to the matching Portfolio card,
     see setupPortfolioRing in script.js) and a drag surface to spin the
     ring, so "grab" still reads correctly for both. */
  cursor: grab;
}

#portfolioRingDragger:active {
  cursor: grabbing;
}

/* SECTIONS */

.section {
  position: relative;
  padding: 110px 0;
}

/* SECTION DIVIDERS */
/* Replaces the old hard 1px border between sections with a soft,
   animated wave line (see setupSectionWaves in script.js), so the seam
   between sections fades rather than showing a visible edge. Sits
   right at the top of the section, straddling the boundary. */
.sectionWave {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 90px;
  transform: translateY(-50%);
  pointer-events: none;
  overflow: visible;
  z-index: 1;
}

.sectionWave canvas {
  display: block;
  width: 100%;
  height: 100%;
  filter: blur(5px);
  opacity: 0.8;
}

.sectionEyebrow {
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--textGrayDim);
  margin-bottom: 18px;
}

.sectionTitle {
  margin: 0 0 26px;
  font-size: clamp(28px, 4vw, 42px);
  font-weight: 600;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.aboutInner {
  max-width: 760px;
}

.aboutText {
  color: var(--textGray);
  font-size: 16px;
  max-width: 680px;
}

/* SERVICES */

/* Services fits one viewport like the hero: min height 100vh, centered
   content, and its own tighter padding, gaps and card sizing instead of
   the default .section spacing. */
.servicesSection {
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: 70px 0;
}

.servicesSection .sectionInner {
  width: 100%;
}

.servicesSection .sectionTitle {
  margin-bottom: 18px;
}

.servicesGrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

.serviceCard {
  background: var(--bgPanel);
  border: 1px solid var(--lineColor);
  border-radius: var(--radiusCard);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.serviceIcon {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: 10px;
  background: linear-gradient(145deg, var(--chrome1), var(--chrome3));
  display: flex;
  align-items: center;
  justify-content: center;
  color: #0a0a0a;
}

.serviceIcon svg {
  width: 21px;
  height: 21px;
}

.cardTitleRow {
  display: flex;
  align-items: center;
  gap: 8px;
}

.serviceTitle {
  font-size: 17px;
  font-weight: 600;
  margin: 0;
}

.serviceText {
  font-size: 14px;
  color: var(--textGray);
  margin: 0;
  flex-grow: 1;
}

.serviceNote {
  font-size: 12px;
  color: var(--textGrayDim);
  font-style: italic;
}

.serviceLink {
  font-size: 13px;
  text-decoration: none;
  color: var(--textWhite);
  border-bottom: 1px solid var(--lineColor);
  align-self: flex-start;
}

/* INFO TOOLTIPS */
/* A small question mark next to a title. Shown on hover and keyboard
   focus for desktop, and on tap through the isOpen class added by
   script.js, since phones have no hover state. */

.infoTipWrap {
  position: relative;
  display: inline-flex;
}

.infoTipButton {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--lineColor);
  background: transparent;
  color: var(--textGrayDim);
  font-size: 11px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.infoTipButton:hover,
.infoTipButton:focus-visible {
  color: var(--textWhite);
  border-color: rgba(255,255,255,0.4);
}

.infoTipBubble {
  position: absolute;
  z-index: 20;
  top: 130%;
  left: 50%;
  transform: translateX(-50%);
  width: min(220px, 78vw);
  background: #161616;
  border: 1px solid var(--lineColor);
  border-radius: 12px;
  padding: 12px 14px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--textGray);
  text-align: left;
  box-shadow: 0 16px 40px rgba(0,0,0,0.5);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.infoTipWrap:hover .infoTipBubble,
.infoTipWrap:focus-within .infoTipBubble,
.infoTipWrap.isOpen .infoTipBubble {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

/* PORTFOLIO */

.portfolioGrid {
  display: grid;
  /* Fixed 2 columns on desktop and up, which with the current 4
     projects lays out as a 2 by 2 grid. Collapses to 1 column on
     smaller screens, see the responsive rules further down. */
  grid-template-columns: repeat(2, 1fr);
  gap: 24px;
}

.portfolioCard {
  background: var(--bgPanel);
  border: 1px solid var(--lineColor);
  border-radius: var(--radiusCard);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
  /* Fixed total height on purpose: portfolioThumb and portfolioBody
     below split this same budget between them (thumb bigger, body
     smaller, see each rule), and the expand button later needs a
     stable box to grow into without shifting the rest of the grid. */
  height: 400px;
  /* Soft monochrome neon glow on hover or keyboard focus, echoing the
     grayscale to color reveal on the thumbnail below. */
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}

.portfolioCard:hover,
.portfolioCard:focus-within {
  border-color: rgba(245, 245, 245, 0.55);
  box-shadow:
    0 0 0 1px rgba(245, 245, 245, 0.3),
    0 0 22px 2px rgba(245, 245, 245, 0.22),
    0 0 46px 10px rgba(245, 245, 245, 0.12);
  transform: translateY(-3px);
}

.portfolioThumb {
  /* Taller than before (was 170px), so more of each screenshot shows
     before portfolioBody below cuts into it. */
  height: 230px;
  flex-shrink: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  /* Fallback color, shown for an instant before the real screenshot in
     .portfolioThumbBody loads, or if it fails to load, and now also the
     letterbox color around the image itself, see background-size below. */
  background: #141414;
}

.portfolioThumbBar {
  height: 22px;
  background: rgba(0,0,0,0.25);
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0 10px;
}

.portfolioThumbBar span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.35);
}

.portfolioThumbBody {
  flex-grow: 1;
  /* Switched from cover to contain: the full screenshot now always
     shows with no cropping, letterboxed on whichever side its aspect
     ratio needs against the fallback color above, instead of the old
     behavior of cropping from the top down. Same grayscale treatment as
     the hero ring by default, turning to full color when its card is
     hovered or focused. */
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;
  filter: grayscale(1) brightness(0.85) contrast(1.1);
  transition: filter 0.4s ease;
}

.portfolioCard:hover .portfolioThumbBody,
.portfolioCard:focus-within .portfolioThumbBody {
  filter: grayscale(0) brightness(1) contrast(1);
}

.portfolioBody {
  /* Fills whatever height is left in the fixed 400px card once
     portfolioThumb takes its 230px, so it starts noticeably shorter
     than before, showing only part of portfolioText by default (see
     .portfolioBodyCollapsible). Switches to position absolute once its
     card gets the isExpanded class, so growing to fit the full text
     happens by overlaying up over the thumb rather than pushing the
     card, and the rest of the Portfolio grid, taller. */
  flex: 1;
  min-height: 0;
  padding: 16px 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--bgPanel);
  position: relative;
  z-index: 2;
}

.portfolioCard.isExpanded .portfolioBody {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Height itself is animated by setupPortfolioExpand in script.js
     (GSAP), growing up to 340px out of the 400px card, well past this
     starting point, so it needs no fixed value here, only the setup
     for the growth to overlay on. */
  box-shadow: 0 -16px 30px rgba(3, 3, 3, 0.55);
  border-radius: 0 0 var(--radiusCard) var(--radiusCard);
}

.portfolioBodyCollapsible {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Soft fade at the bottom edge of the collapsed text, hinting that it
   continues, instead of an abrupt hard cut. Fades away once expanded. */
.portfolioBodyCollapsible::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 28px;
  background: linear-gradient(to bottom, rgba(20, 20, 20, 0) 0%, var(--bgPanel) 85%);
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.portfolioCard.isExpanded .portfolioBodyCollapsible {
  overflow-y: auto;
}

.portfolioCard.isExpanded .portfolioBodyCollapsible::after {
  opacity: 0;
}

.portfolioBodyFooter {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.portfolioExpandButton {
  font-size: 12px;
  font-weight: 600;
  background: none;
  border: none;
  color: var(--textGray);
  cursor: pointer;
  padding: 0;
  transition: color 0.2s ease;
}

.portfolioExpandButton:hover,
.portfolioExpandButton:focus-visible {
  color: var(--textWhite);
}

.portfolioTags {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.portfolioTag {
  font-size: 11px;
  padding: 4px 10px;
  border-radius: var(--radiusPill);
  border: 1px solid var(--lineColor);
  color: var(--textGray);
}

.portfolioTitle {
  font-size: 18px;
  font-weight: 600;
  margin: 0;
}

.portfolioText {
  font-size: 14px;
  color: var(--textGray);
  margin: 0;
  flex-grow: 1;
}

.portfolioLink {
  font-size: 13px;
  text-decoration: none;
  color: var(--textWhite);
  border-bottom: 1px solid var(--lineColor);
  align-self: flex-start;
}

/* QUOTE */

.quoteInner {
  max-width: 640px;
}

.quoteIntro {
  color: var(--textGray);
  font-size: 15px;
  margin-bottom: 34px;
}

.quoteForm {
  display: flex;
  flex-direction: column;
  gap: 18px;
  background: var(--bgPanel);
  border: 1px solid var(--lineColor);
  border-radius: var(--radiusCard);
  padding: 28px;
}

.fieldGroup {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.fieldGroup label {
  font-size: 13px;
  color: var(--textGray);
}

.fieldGroup input,
.fieldGroup select,
.fieldGroup textarea {
  background: #141414;
  border: 1px solid var(--lineColor);
  border-radius: 10px;
  padding: 12px 14px;
  color: var(--textWhite);
  font-family: var(--fontSans);
  font-size: 14px;
  resize: vertical;
}

.fieldGroup input:focus,
.fieldGroup select:focus,
.fieldGroup textarea:focus {
  outline: none;
  border-color: rgba(255,255,255,0.4);
}

.fieldRow {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}

.formSectionLabel {
  margin: 10px 0 0;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--textGrayDim);
  border-top: 1px solid var(--lineColor);
  padding-top: 20px;
}

.quoteForm > .formSectionLabel:first-child {
  margin-top: 0;
  border-top: none;
  padding-top: 0;
}

.formSectionHint {
  margin: -8px 0 0;
  font-size: 13px;
  color: var(--textGrayDim);
}

.quoteEstimate {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--textWhite);
  min-height: 16px;
}

.checkboxGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.checkboxOption {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--textGray);
  background: #141414;
  border: 1px solid var(--lineColor);
  border-radius: 10px;
  padding: 10px 12px;
  cursor: pointer;
}

.checkboxOption input {
  accent-color: var(--textWhite);
}

.referenceList {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 10px;
}

.referenceItem {
  display: flex;
}

.referenceInput {
  width: 100%;
}

.referenceAddButton {
  align-self: flex-start;
  padding: 9px 20px;
  font-size: 13px;
}

/* Honeypot field for the quote form. Positioned off screen with
   absolute placement, zero size and zero opacity rather than display
   none, since a bot that specifically checks for display none is a
   real pattern, and this stays invisible either way for a sighted
   visitor, a screen reader (aria hidden on the wrapper) and a keyboard
   user (tabindex -1 on the input). */
.hpField {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  left: -9999px;
}

.quoteSubmit {
  border: none;
  margin-top: 6px;
}

.quoteStatus {
  font-size: 13px;
  color: var(--textGray);
  min-height: 18px;
  margin: 0;
}

.quoteStatus.isSuccess {
  color: #b7f7c1;
}

.quoteStatus.isError {
  color: #f7b7b7;
}

.quoteAlt {
  text-align: center;
  color: var(--textGray);
  font-size: 14px;
  margin-top: 22px;
}

.quoteAlt a {
  color: var(--textWhite);
  border-bottom: 1px solid var(--lineColor);
  text-decoration: none;
}

/* CONTACT */

.contactInner {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

/* Wraps the brand mark in the rotating wireframe sphere moved over
   from the hero. Sized so the sphere reads clearly bigger than the
   logo it holds, then both bumped 50% larger than that base pairing
   so the pair reads as a real centerpiece in this section. */
.contactSphere {
  position: relative;
  width: 188px;
  height: 188px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Keeps the same 20 to 1 ratio against the sphere's own diameter
     that the effect used in the hero, so it still reads as a sphere
     instead of a flat rosette at this larger size. */
  perspective: 3760px;
}

.contactSphere .sphere {
  --u: 3.76px;
}

.contactMark {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 117px;
  height: auto;
  z-index: 1;
  /* Soft glow behind the mark, matching the neon treatment used
     elsewhere on the page rather than a flat drop shadow. */
  filter: drop-shadow(0 0 14px rgba(245, 245, 245, 0.35));
}

.contactText {
  color: var(--textGray);
  max-width: 480px;
}

/* FOOTER */

.siteFooter {
  position: relative;
  padding: 30px 0 50px;
}

.footerInner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
  color: var(--textGrayDim);
}

.footerBrand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.footerMark {
  width: 22px;
  height: auto;
  display: block;
}

.footerRight {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Same line icon language as .serviceIcon, but small and outlined
   instead of filled, so it sits quietly in the footer until hovered. */
.footerSocialLink {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--lineColor);
  color: var(--textGrayDim);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease,
    transform 0.3s ease;
}

.footerSocialLink svg {
  width: 16px;
  height: 16px;
}

.footerSocialLink:hover,
.footerSocialLink:focus-visible {
  color: var(--textWhite);
  border-color: rgba(245, 245, 245, 0.55);
  transform: translateY(-2px);
  box-shadow:
    0 0 0 1px rgba(245, 245, 245, 0.3),
    0 0 14px 2px rgba(245, 245, 245, 0.28),
    0 0 30px 6px rgba(245, 245, 245, 0.14);
}

/* Back to top button, in its own narrow .navUtilityBar in the navbar
   (see .navUtilityBar near navBrand). Reuses .footerSocialLink for the
   circle shape and the neon hover glow, but overrides its color here:
   this circle gets a solid light fill instead of staying transparent,
   so the arrow needs a dark stroke to actually read against it, rather
   than a light icon on a light background. These rules come after
   .footerSocialLink on purpose, so they win over its own color rules
   at equal specificity, in both the resting and the hover state. */
.navBackToTop {
  background: var(--textWhite);
  border-color: rgba(245, 245, 245, 0.7);
  color: var(--bgBlack);
}

.navBackToTop:hover,
.navBackToTop:focus-visible {
  color: var(--bgBlack);
}

/* RESPONSIVE */

@media (max-width: 900px) {
  .servicesGrid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 720px) {
  .navLinks {
    display: none;
  }
  .navMenuToggle {
    display: flex;
  }
  /* .siteNav stays position fixed, so it works as the containing block
     here: the bar drops out of the shared row and pins itself just
     under the hamburger instead, still part of the nav itself rather
     than floating separately below the whole header. */
  .navUtilityBar {
    position: absolute;
    top: 100%;
    right: 32px;
    margin-top: 10px;
  }
  .servicesGrid,
  .portfolioGrid,
  .fieldRow,
  .checkboxGrid {
    grid-template-columns: 1fr;
  }
  .hero {
    padding-top: 118px;
  }
  .ringWrap {
    height: 190px;
  }
  .ringContainer {
    /* Same 1.76 to 1 ratio as the desktop size, just smaller. */
    width: 240px;
    height: 136px;
  }
}
