/* =========================================================
   Design tokens
   ========================================================= */
:root {
  --color-bg: #000000;
  --color-purple: #9d4edd;
  --color-purple-strong: #8a43c5;
  --color-purple-mid: #582785;
  --color-purple-deep: #11002c;
  --color-white: #ffffff;
  --color-border-gray: #d0d5dd;

  --text-dim: rgba(255, 255, 255, 0.62);
  --text-faint: rgba(255, 255, 255, 0.40);

  /* glass */
  --glass-fill: linear-gradient(135deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04));
  --glass-border: rgba(255, 255, 255, 0.25);
  --glass-blur: blur(18px) saturate(160%);
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.15);
  --radius-card: 24px;

  --font-display: "League Gothic", "Oswald", "Arial Narrow", sans-serif;
  --font-script: "Qwigley", cursive;
  --font-body: "Poppins", system-ui, sans-serif;

  /* One type scale for the whole page. Every heading, subheading,
     body run and label pulls from here, so a size is only ever
     defined once and the sections stay in step with each other. */
  --fs-display: clamp(52px, 8.4vw, 128px);
  --fs-eyebrow: clamp(28px, 3.4vw, 44px);
  --fs-lead: clamp(20px, 2.1vw, 28px);
  --fs-title: clamp(22px, 2.1vw, 30px);
  --fs-sub: clamp(16px, 1.5vw, 21px);
  --fs-body: clamp(15px, 1.15vw, 17px);
  --fs-small: 14px;
  --fs-label: 12px;

  --lh-body: 1.7;
  --lh-tight: 1.3;
  --ls-label: 0.16em;

  --hero-width: 1440;
  --hero-height: 874;
  --nav-h: 110px;
  /* Where a stacked service card parks — clear of the floating nav. */
  --stack-top: 104px;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --page-x: clamp(20px, 5vw, 80px);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  background: var(--color-bg);
  font-family: var(--font-body);
  color: var(--color-white);
  /* `clip`, not `hidden`: hidden would turn the body into a
     scroll container and break the sticky services stack. */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

@supports not (overflow: clip) {
  body {
    overflow-x: hidden;
  }
}

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

a {
  text-decoration: none;
  color: inherit;
}

ul,
ol {
  list-style: none;
}

::selection {
  background: rgba(157, 78, 221, 0.5);
}

:focus-visible {
  outline: 2px solid var(--color-purple);
  outline-offset: 3px;
  border-radius: 6px;
}

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

/* =========================================================
   Ambient background — soft purple light + grain, parallaxed
   ========================================================= */
.ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.orb {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(64px);
  transform: translateZ(0);
}

.orb-1 {
  top: -12vh;
  left: -6vw;
  width: 46vw;
  height: 46vw;
  background: radial-gradient(circle, rgba(157, 78, 221, 0.30), transparent 68%);
}

.orb-2 {
  top: 38vh;
  right: -14vw;
  width: 52vw;
  height: 52vw;
  background: radial-gradient(circle, rgba(88, 39, 133, 0.34), transparent 68%);
}

.orb-3 {
  bottom: -18vh;
  left: 22vw;
  width: 40vw;
  height: 40vw;
  background: radial-gradient(circle, rgba(138, 67, 197, 0.22), transparent 70%);
}

.grain {
  position: absolute;
  inset: 0;
  opacity: 0.16;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.5'/></svg>");
}

main,
.navbar,
.site-footer {
  position: relative;
  z-index: 1;
}

/* =========================================================
   Navbar — glassmorphism
   ========================================================= */
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 12px;
  padding: clamp(16px, 3vw, 30px) 24px;
  animation: nav-drop 0.7s var(--ease-out) both;
}

.nav-pill {
  display: inline-flex;
  align-items: center;
  gap: clamp(2px, 0.6vw, 10px);
  padding: 8px;
  background: var(--glass-fill);
  border: 1px solid var(--glass-border);
  border-radius: 50px;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
}

.nav-pill::-webkit-scrollbar {
  display: none;
}

.nav-link {
  flex-shrink: 0;
  padding: 10px clamp(16px, 2.6vw, 40px);
  border-radius: 60px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(14px, 1.35vw, 20px);
  letter-spacing: -0.3px;
  color: var(--color-white);
  white-space: nowrap;
  transition: background-color 0.35s ease, color 0.35s ease, transform 0.2s ease, border-color 0.35s ease;
  border: 1px solid transparent;
}

.nav-link:hover {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.18);
}

.nav-link.active {
  background: linear-gradient(135deg, rgba(157, 78, 221, 0.95), rgba(138, 67, 197, 0.8));
  border-color: rgba(255, 255, 255, 0.35);
  box-shadow: 0 4px 18px rgba(157, 78, 221, 0.55);
}

.nav-link.active:hover {
  background: linear-gradient(135deg, rgba(157, 78, 221, 0.95), rgba(138, 67, 197, 0.8));
}

.nav-link:active {
  transform: scale(0.96);
}

.nav-toggle {
  display: none;
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  align-items: center;
  justify-content: center;
  gap: 5px;
  flex-direction: column;
  background: var(--glass-fill);
  border: 1px solid var(--glass-border);
  border-radius: 50%;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  cursor: pointer;
}

.nav-toggle-bar {
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: var(--color-white);
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.nav-toggle.open .nav-toggle-bar:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav-toggle.open .nav-toggle-bar:nth-child(2) {
  opacity: 0;
}

.nav-toggle.open .nav-toggle-bar:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* Navbar is fixed/floating, so main reserves space for it up
   front — hero content must never load underneath the nav. */
main {
  display: block;
  padding-top: var(--nav-h);
}

/* =========================================================
   Hero — fluid canvas
   Every child is positioned as a % of the 1440x874 design and
   .hero keeps that exact aspect ratio at every width, so the
   composition scales in lockstep with no breakpoint jumps
   until the stacked mobile layout takes over. Width is also
   capped by available viewport height so the hero never grows
   taller than the screen on short/wide laptop viewports.
   ========================================================= */
.hero {
  position: relative;
  width: min(
    100%,
    calc(var(--hero-width) * 1px),
    calc((100vh - var(--nav-h)) * var(--hero-width) / var(--hero-height))
  );
  aspect-ratio: var(--hero-width) / var(--hero-height);
  margin: 0 auto;
  overflow: hidden;
  container-type: inline-size;
  container-name: hero;
}

.hero-bg-text {
  position: absolute;
  left: 50%;
  top: -4.12%;
  width: 93.2%;
  transform: translateX(-50%);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(64px, 20.8cqw, 300px);
  line-height: 1;
  letter-spacing: -10px;
  text-align: center;
  white-space: nowrap;
  background: linear-gradient(
    to bottom,
    var(--color-purple) 41.25%,
    var(--color-purple-mid) 62.892%,
    var(--color-purple-deep) 84.861%
  );
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  opacity: 0;
  animation: hero-bg-in 1.4s var(--ease-out) 0.1s forwards;
}

.hero-greeting {
  position: absolute;
  left: 15%;
  top: 23.34%;
  width: 21.53%;
  min-width: 180px;
  transform: translateX(-50%);
  font-family: var(--font-script);
  font-weight: 400;
  font-size: clamp(40px, 6.667cqw, 96px);
  text-align: center;
  color: var(--color-white);
  opacity: 0;
  animation: fade-up-x 0.8s ease-out 0.35s forwards;
}

.hero-name {
  position: absolute;
  left: 5.14%;
  top: 35.93%;
  width: 17.64%;
  min-width: 150px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(56px, 10.417cqw, 150px);
  line-height: 0.9;
  color: var(--color-white);
  white-space: nowrap;
  animation: pulse-zoom 3.2s ease-in-out 2.2s infinite;
}

/* Each line is its own mask; the inner span rises out of it,
   so the name wipes up line by line instead of just fading.
   The padding/negative-margin pair widens the mask box past
   the glyph box so League Gothic's caps and descenders are
   never sheared by the overflow clip. */
.hero-name-line1,
.hero-name-line2 {
  display: block;
  overflow: hidden;
  padding: 0.08em 0.04em 0.05em;
  margin: -0.08em -0.04em -0.05em;
}

.hero-name-inner {
  display: block;
  transform: translateY(115%);
  animation: line-rise 1s var(--ease-out) forwards;
}

.hero-name-line1 {
  letter-spacing: 2px;
}

.hero-name-line1 .hero-name-inner {
  animation-delay: 0.42s;
}

.hero-name-line2 .hero-name-inner {
  animation-delay: 0.56s;
}

.hero-role {
  position: absolute;
  left: 17.26%;
  top: 69.34%;
  transform: translateX(-50%);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(24px, 3.333cqw, 48px);
  color: var(--color-purple);
  white-space: nowrap;
  opacity: 0;
  animation: fade-up-x 0.8s ease-out 0.65s forwards;
}

/* ---- photo ---- */
.hero-photo-wrap {
  position: absolute;
  left: 30.42%;
  top: 5.61%;
  width: 34.86%;
  height: 75.74%;
  opacity: 0;
  animation: fade-up 1s ease-out 0.25s forwards, float 6s ease-in-out 1.2s infinite;
}

/* Soft purple bloom behind the portrait — it lifts her off
   the black and breathes slowly so the hero is never fully
   still, without anything actually moving. */
.hero-photo-glow {
  position: absolute;
  left: 50%;
  top: 26%;
  width: 96%;
  height: 74%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(157, 78, 221, 0.42), rgba(157, 78, 221, 0.10) 62%, transparent 78%);
  filter: blur(26px);
  opacity: 0;
  animation: glow-in 1.6s ease-out 0.4s forwards, breathe 7s ease-in-out 2s infinite;
}

.hero-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: bottom;
}

.hero-photo-shadow {
  position: absolute;
  left: -0.49%;
  top: 71.97%;
  width: 35.07%;
  height: 7.32%;
  background: rgba(0, 0, 0, 0.88);
  filter: blur(15.65px);
  border-radius: 50%;
}

/* ---- quote ---- */
.hero-quote-wrap {
  position: absolute;
  left: 70.07%;
  top: 47.25%;
  width: 27.29%;
  min-width: 220px;
  opacity: 0;
  animation: fade-up 0.8s ease-out 0.55s forwards;
}

.hero-quote {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(18px, 2.5cqw, 36px);
  line-height: normal;
  color: var(--color-white);
  text-wrap: pretty;
}

.squiggle-quote {
  position: absolute;
  right: -8px;
  top: -16px;
  width: clamp(24px, 2.7cqw, 39px);
  height: clamp(25px, 2.85cqw, 41px);
  opacity: 0;
  animation: pop-in 0.55s var(--ease-out) 1.05s forwards;
}

/* ---- CTA ---- */
.hero-cta {
  position: absolute;
  left: calc(50% + 0.31%);
  top: calc(50% + 41.19%);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  opacity: 0;
  animation: fade-up-xy 0.8s ease-out 0.8s forwards;
}

.squiggle-cta {
  position: absolute;
  left: -40px;
  top: 20px;
  width: clamp(26px, 2.78cqw, 40px);
  height: clamp(27px, 2.92cqw, 42px);
  transform: rotate(135deg) scaleY(-1);
  opacity: 0;
  animation: pop-in-cta 0.55s var(--ease-out) 1.25s forwards;
}

.cta-pill {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: clamp(260px, 25.5cqw, 367px);
  height: clamp(60px, 5.7cqw, 82px);
  padding: 10px;
  background: rgba(255, 255, 255, 0.1);
  border: 2px solid var(--color-white);
  border-radius: 50px;
  backdrop-filter: blur(7.5px);
  -webkit-backdrop-filter: blur(7.5px);
}

.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 100%;
  padding: 10px clamp(14px, 1.4cqw, 20px);
  border-radius: 60px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(16px, 1.784cqw, 25.692px);
  letter-spacing: -0.385px;
  color: var(--color-white);
  white-space: nowrap;
  transition: transform 0.25s ease, box-shadow 0.25s ease, background-color 0.25s ease;
}

.btn-resume {
  width: clamp(150px, 14.44cqw, 208px);
  background: var(--color-purple-strong);
  border: 0.5px solid var(--color-border-gray);
}

.btn-resume:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 24px rgba(138, 67, 197, 0.55);
}

.arrow-icon {
  width: clamp(26px, 2.92cqw, 42px);
  height: clamp(26px, 2.92cqw, 42px);
  transition: transform 0.25s ease;
}

.arrow-icon.sm {
  width: 22px;
  height: 22px;
}

.btn-resume:hover .arrow-icon {
  transform: translate(2px, -2px);
}

.btn-hire {
  flex: 1 0 0;
}

.btn-hire:hover {
  background: rgba(255, 255, 255, 0.12);
}

.btn:active {
  transform: scale(0.97);
}

/* =========================================================
   Section shell
   ========================================================= */
.section {
  max-width: 1440px;
  margin: 0 auto;
  padding: clamp(72px, 10vw, 150px) var(--page-x);
}

.section-head {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  margin-bottom: clamp(32px, 5vw, 64px);
}

/* A soft purple bloom sat behind the heading. On a black page
   a big headline reads as a grey shape until something lifts
   it off the ground — this is what makes the section titles
   register before the content below them. */
.section-head::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -8%;
  top: 50%;
  width: min(640px, 74%);
  height: 190%;
  transform: translateY(-50%);
  background: radial-gradient(closest-side, rgba(157, 78, 221, 0.30), transparent 70%);
  filter: blur(30px);
  pointer-events: none;
}

.section-head::after {
  content: "";
  width: clamp(72px, 9vw, 124px);
  height: 2px;
  margin-top: clamp(14px, 1.6vw, 22px);
  background: linear-gradient(90deg, var(--color-purple), rgba(157, 78, 221, 0));
}

.eyebrow {
  font-family: var(--font-script);
  font-size: var(--fs-eyebrow);
  line-height: 1.1;
  color: #c58bf5;
}

.section-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--fs-display);
  line-height: 0.92;
  letter-spacing: -2px;
  /* Stays bright the whole way down — the old ramp faded into
     a dark purple that sank into the background. */
  background: linear-gradient(to bottom, #ffffff 36%, #dcc0ff 84%, #b276ef 100%);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

.glass {
  background: var(--glass-fill);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-card);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}

/* =========================================================
   Services — sticky stack

   Each panel sticks to the top of the viewport and the next
   one scrolls up over it, so the four services deal onto one
   another going down and peel back apart going up. The panels
   must be fully opaque (no backdrop-filter here) or the ones
   underneath show through the stack.
   ========================================================= */
.section-head-only {
  padding-bottom: clamp(28px, 4vw, 56px);
}

/* The heading pins to the top and holds there until the last card
   has scrolled past, so it is on screen for the whole stack. It
   keeps the same type as every other section heading; the room the
   cards need comes out of the card, not the heading.

   The card is capped to what fits between where it parks and the
   bottom of the screen, counting the three 16px notches the deepest
   card sits lower and leaving a gap underneath, so no card's bottom
   edge runs past the fold. The floor is set by the tallest card's
   copy — shrink past that and max-height starts clipping text. */
@media (min-width: 901px) {
  /* Close-enough default so the first paint is right and the layout
     survives the script not running; JS then replaces it with the
     heading's measured height. */
  :root {
    --stack-top: 330px;
  }

  .section-head-only {
    position: sticky;
    top: 0;
    z-index: 3;
    padding-top: calc(var(--nav-h) - 14px);
    padding-bottom: clamp(20px, 2.4vw, 34px);
    background: linear-gradient(to bottom, #000 0%, #000 76%, rgba(0, 0, 0, 0) 100%);
  }

  .section-head-only .section-head {
    margin-bottom: 0;
  }

  .svc {
    --card-h: clamp(300px, calc(100svh - var(--stack-top) - 48px - 56px), 372px);
    z-index: 1;
    min-height: var(--card-h);
    max-height: var(--card-h);
  }
}

/* On a short window the full-size heading plus a readable card is
   more than the viewport holds, so the heading gives back its
   padding and a little of its type rather than the stack losing
   cards off the bottom edge. */
@media (min-width: 901px) and (max-height: 800px) {
  :root {
    --stack-top: 232px;
  }

  .section-head-only {
    padding-top: calc(var(--nav-h) - 40px);
    padding-bottom: 16px;
  }

  .section-head-only .eyebrow {
    font-size: clamp(24px, 2.6vw, 34px);
  }

  .section-head-only .section-title {
    font-size: clamp(40px, 5.6vw, 84px);
  }

  .section-head-only .section-head::after {
    margin-top: 12px;
  }
}

.stack {
  display: flex;
  flex-direction: column;
  /* The gap is never seen once the cards overlap — it is the
     scroll runway that decides how long each card holds the
     screen before the next one covers it. */
  gap: clamp(20px, 6vh, 64px);
  max-width: 1440px;
  margin: 0 auto;
  /* Small on purpose: this trailing space is what holds the last
     card (and the pinned heading) in place after it lands. Keep it
     short so the whole deck scrolls away together rather than the
     last card sitting there on its own. */
  padding: 0 var(--page-x) clamp(36px, 7vh, 80px);
}

.svc {
  position: sticky;
  /* Each card parks a notch lower than the one before, so the
     stack reads as a deck rather than one card replacing
     another in place. */
  top: calc(var(--stack-top) + var(--i) * 16px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 324px);
  align-items: center;
  gap: clamp(24px, 4vw, 56px);
  min-height: 300px;
  padding: clamp(26px, 2.4vw, 34px) clamp(26px, 2.4vw, 34px) clamp(26px, 2.4vw, 34px) clamp(26px, 3.4vw, 48px);
  background: #121212;
  box-shadow: 0 6px 20px rgba(211, 178, 252, 0.13);
  overflow: hidden;
}

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

/* Numeral and title sit on one line. `baseline` rather than
   `center` so the digits sit on the same line as the title's
   letters however the two sizes are tuned. */
.svc-head {
  display: flex;
  align-items: baseline;
  gap: clamp(14px, 1.6vw, 22px);
}

.svc-num {
  flex-shrink: 0;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-title);
  line-height: var(--lh-tight);
  color: var(--color-white);
}

.svc-title {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-title);
  line-height: var(--lh-tight);
  text-transform: uppercase;
  color: var(--color-purple);
}

/* The card runs the full width of the page, so the subtitle is the
   one line that needs a measure of its own — left to fill the
   column it becomes a single unreadable run. */
.svc-sub {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--fs-sub);
  line-height: 1.45;
  max-width: 46ch;
  color: #f1f1f1;
  text-wrap: pretty;
}

.svc-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-left: 24px;
  list-style: disc;
  font-weight: 400;
  font-size: var(--fs-body);
  line-height: 1.5;
  color: #e3e5e8;
}

.svc-list li {
  list-style: disc;
}

/* Smaller than the copy block and centred against it, rather than
   stretched to the card's full height — the picture supports the
   text here, it does not compete with it. */
.svc-media {
  align-self: center;
  aspect-ratio: 4 / 3;
  border-radius: 2px;
  box-shadow: 0 4px 24px rgba(255, 255, 255, 0.12);
  overflow: hidden;
}

.svc-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s var(--ease-out);
}

.svc:hover .svc-media img {
  transform: scale(1.04);
}

/* =========================================================
   About
   ========================================================= */
.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 64px);
  align-items: center;
}

/* ---- the ID badge ---- */
.id-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto;
  animation: badge-sway 7s ease-in-out infinite;
  transform-origin: 50% 0%;
}

.id-lanyard {
  width: clamp(96px, 11vw, 140px);
  height: auto;
  margin-bottom: -6px;
}

.id-card {
  position: relative;
  width: clamp(168px, 19vw, 240px);
  aspect-ratio: 4 / 5;
  padding: 14px 14px 18px;
  background: var(--glass-fill);
  border: 1px solid var(--glass-border);
  border-radius: 18px;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 26px 60px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(157, 78, 221, 0.16);
  transform: rotate(-2.5deg);
}

.id-card-hole {
  position: absolute;
  left: 50%;
  top: 8px;
  width: 34px;
  height: 7px;
  transform: translateX(-50%);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.55);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}

.id-card-photo {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
}

.id-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@keyframes badge-sway {
  0%,
  100% {
    transform: rotate(-2deg);
  }
  50% {
    transform: rotate(2deg);
  }
}

/* ---- the content column ---- */
.about-content {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.about-hi {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(30px, 3.4vw, 46px);
  line-height: 1;
  letter-spacing: -0.5px;
  color: var(--color-white);
}

.about-name {
  color: var(--color-purple);
}

.about-lead {
  font-weight: 500;
  font-size: var(--fs-lead);
  line-height: 1.35;
  letter-spacing: -0.4px;
  text-wrap: balance;
}

.about-text {
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-dim);
  text-wrap: pretty;
}

/* Contact stacks above Software in one column, matching the
   reference — there's no second Experience-width column to balance
   against any more, so a side-by-side split would just leave one
   side looking thin. */
.about-meta {
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 3vw, 32px);
  margin-top: 8px;
  padding-top: clamp(20px, 3vw, 32px);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.meta-block {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.meta-heading {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-sub);
  color: var(--color-white);
  margin-bottom: 10px;
}

/* ---- software: a plain row of tool badges ---- */
.software-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.software-icon {
  width: 40px;
  height: 40px;
  border-radius: 22%;
  overflow: hidden;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.14);
  transition: transform 0.3s var(--ease-out);
}

.software-icon:hover {
  transform: translateY(-3px);
}

.software-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* =========================================================
   Project
   ========================================================= */
/* =========================================================
   Project — rotating semicircle

   The wheel is a point anchored at the bottom centre of the
   stage. Each project is a spoke rotated around that point and
   pushed out by the radius, so only the upper arc is on screen
   and the rest of the ring waits below the fold.
   ========================================================= */
/* Defined on the section, not .carousel itself, so .project-info —
   a sibling of .carousel, not a descendant — can also read --r for
   its own desktop overlap offset. */
#project {
  /* Sized off the section's own width now that the wheel sits
     directly on the page rather than in a capped-width card, so it
     reads at the same scale as the rest of the section's content
     instead of looking small in all the open space. The name/link
     row now overlaps up into the dome itself on desktop (see
     .project-info) rather than adding its own row below the wheel,
     which freed up the room to size this larger again while the
     section still fits one screen. The 250px floor is load-bearing,
     not just cosmetic — .project-info needs the arrows' own gap
     (0.52 * --r, minus their 46px width) wide enough to clear its
     own width; below ~242px that gap closes below the button. */
  --r: clamp(250px, 26vw, 330px);
  /* The dome's own horizontal reach — wider than --r on purpose, so
     the arc reads as a flattened, elongated oval rather than a
     perfect half-circle. Only the painted face uses this; the
     spokes still travel a true circular path off --r alone, so the
     tiles themselves stay round and undistorted. */
  --rx: calc(var(--r) * 1.3);
  --tile-w: clamp(62px, 6.4vw, 92px);
  --tile-h: calc(var(--tile-w) * 1.3);
  /* Where the projects sit on the radius, as a fraction of it —
     pushed out further than 0.71 so there's real air between the
     side tiles and the featured card at the wheel's current,
     larger scale; 0.71 had them almost touching. */
  --ring: 0.85;
  /* Tighter than the shared .section padding — this section has a
     name and a link to fit below the wheel too, on top of the
     heading and the carousel itself, and the shared clamp alone
     used to push the whole thing past one screen's height. */
  padding-top: clamp(40px, 6vw, 90px);
  padding-bottom: clamp(40px, 6vw, 90px);
}

#project .section-head {
  margin-bottom: clamp(16px, 2.4vw, 32px);
}

.carousel {
  position: relative;
  height: calc(var(--r) + var(--extra, 0px) + 24px);
}

/* The dial is a whole circle whose centre sits on the bottom edge
   of the stage; the clip is half its height, so only the upper
   semicircle is ever on screen. The clip wraps the dial alone, so
   the card can stand taller than the semicircle without being cut
   off by it — which is what happens on a narrow phone. */
.dial-stage {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--rx) * 2);
  height: var(--r);
  transform: translateX(-50%);
}

.dial-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* Sized off --rx, not --r — an ellipse, not a circle. Its own centre
   (50%, 50%) still lands exactly on .dial-stage's bottom-centre, the
   same rotation origin the spokes are anchored to, because both
   share the same width (2 × --rx). The spokes don't care: they
   travel a true circular path off --r alone (see .spoke's own
   translate distance), so only the dome's painted shape widens —
   the tiles stay round and keep their own (narrower) circular arc. */
.dial {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--rx) * 2);
  height: calc(var(--r) * 2);
}

/* A fixed backdrop, not part of the rotating #dial — only the tiles
   orbit; the dome underneath them stays put. Sized and positioned to
   match #dial's own box exactly (same 2rx × 2r ellipse, same corner)
   so nothing shifts now that it's a sibling instead of a child; only
   the rotation is gone. One flat tint rather than alternating wedge
   opacities — alternating 0.34/0.15 read as two different colours
   (purple wedges next to near-black ones) instead of one ring, and
   made the tint look like it petered out before the true outer edge.
   A single opacity carries evenly out from the centre, filling the
   whole ellipse — nothing punches a hole for .dial-core any more. */
.dial-face {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--rx) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  /* 72deg segments, offset -36deg so a hairline bisects the gap on
     each side of top dead centre — five slices for five projects,
     not the six this was tuned for originally. */
  background:
    repeating-conic-gradient(from -36deg, rgba(255, 255, 255, 0.13) 0deg 0.3deg, transparent 0.3deg 72deg),
    rgba(157, 78, 221, 0.26);
  /* A long, slow fade out to the rim rather than a flat tint cut off
     by the clip — a hard-edged dome silhouette is exactly what read
     as "differentiated from the background". Solid through the
     middle (past where the spoke tiles sit), then a wide fade zone
     dissolves it into the page before it ever reaches the true
     edge, so there's no visible boundary line to see. */
  mask-image: radial-gradient(closest-side, #000 0%, #000 52%, transparent 100%);
  -webkit-mask-image: radial-gradient(closest-side, #000 0%, #000 52%, transparent 100%);
}

.spoke {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--tile-w);
  height: var(--tile-h);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  /* Rotate about the dial's centre, then push out along the arc. */
  transform-origin: 0 0;
  transform: rotate(var(--a)) translate(calc(var(--tile-w) / -2), calc(-1 * var(--ring) * var(--r) - var(--tile-h) / 2));
}

/* Dark well in the middle of the dial that the card sits on — sized
   well under the outer ring (not 0.78 of it) so it reads as its own
   smaller, nested inner semicircle rather than filling most of the
   dome. The card's bottom sits flush with this circle's own flat
   edge, which is what makes it read as emerging from the inner
   ring specifically, not just floating in the dome generally. */
/* No fill of its own any more — .dial-face now covers the centre
   too, so a separate disc here just reintroduces the same darker
   patch this element used to be. Kept (rather than deleted from the
   HTML) only so the DOM/JS don't need touching. */
.dial-core {
  display: none;
}

.dial-arrow {
  position: absolute;
  bottom: calc(var(--r) * 0.13);
  z-index: 3;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: rgba(12, 3, 18, 0.82);
  color: var(--color-white);
  cursor: pointer;
  transition: transform 0.3s var(--ease-out), background-color 0.3s ease, border-color 0.3s ease;
}

.dial-arrow svg {
  width: 22px;
  height: 22px;
}

/* Between the core and the ring of projects, so they never sit on
   top of a tile. */
.dial-arrow.prev {
  left: calc(50% - var(--r) * 0.52);
}

.dial-arrow.next {
  left: calc(50% + var(--r) * 0.52 - 46px);
}

.dial-arrow:hover {
  transform: scale(1.08);
  background: rgba(157, 78, 221, 0.4);
  border-color: rgba(157, 78, 221, 0.85);
}

.tile {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 10px;
  /* The real cover, in its own true colours (see .hub-shot's own
     comment on why the purple recolour came out) — a tint gradient
     stands in only until --cover loads. */
  background-image: var(--cover, linear-gradient(160deg, hsl(var(--tint, 272) 62% 46%), hsl(var(--tint, 272) 70% 18%)));
  background-size: cover;
  background-position: center top;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  transition: border-color 0.5s ease, box-shadow 0.5s ease, filter 0.5s ease;
}

.spoke[data-active] .tile {
  border-color: rgba(216, 180, 254, 0.85);
  box-shadow: 0 12px 34px rgba(157, 78, 221, 0.45);
  filter: brightness(1.14);
}

.spoke:not([data-active]) .tile {
  filter: brightness(0.78) saturate(0.85);
}

/* ---- the card in the middle of the dial ----
   Tall and narrow rather than a landscape rectangle, and its base
   dissolves into the dome's own flat tint (a fade pseudo-element, not
   a hard edge) so the card reads as rising up out of the wheel
   rather than a shape floating in front of it.

   Anchored flush with the inner ring's own rim (see below), free to
   run tall enough that its top pokes past the outer arc — that's the
   reference's own read: the poster rises above the wheel, not below
   it. */
.hub {
  position: absolute;
  left: 50%;
  /* High enough that .dial-face's own hairlines (the two nearest
     vertical, at ±30°) have already spread past the card's own half
     -width by the time they reach this height — otherwise they cross
     behind the card's lower half instead of framing it. Given the
     hairlines' 30° angle, clearing them needs bottom ≳ width * 0.87,
     which at the mobile breakpoint's own worst case (where its width
     formula caps out) works out to bottom ≳ 0.596 of --r — the floor
     this can't go below without touching them. 0.61 sits just above
     that floor (nudged down as far as it safely goes), and clears
     the desktop range with plenty to spare. Also comfortably higher
     than the side tiles, so the card reads as the highlighted one. */
  bottom: calc(var(--r) * 0.61);
  z-index: 2;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Scaled off --r (not a flat percentage of the stage) so the card
     always stays inside the gap the arrows sit in — that gap itself
     is a fraction of --r, and a flat percentage let the two collide
     at the smaller end of the desktop range. Narrower coefficients
     than a plain "half the safe arrow gap" — the card was crowding
     the side tiles at the wheel's current, larger scale. */
  width: clamp(70px, calc(var(--r) * 0.85 - 90px), 170px);
  text-align: center;
  transition: opacity 0.36s ease, transform 0.36s var(--ease-out);
}

/* The card sinks back and fades while the dial turns, then rises
   back into place as the step lands — the swap itself repeats the
   emerging motion, not just the resting shape. */
.carousel[data-turning] .hub {
  opacity: 0;
  transform: translateX(-50%) translateY(26px) scale(0.93);
}

.hub-shot {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4.2;
  /* No border at all — a line on any edge crossed straight through
     the dome's own hairlines right behind it, reading as two things
     intersecting instead of one emerging from the other. The fade
     (below) and the box-shadow are what carry and lift the card
     instead. */
  border-radius: 20px 20px 7px 7px;
  /* The real cover, in its own true colours — this is the first
     look a visitor gets at the actual work, not a mood board, so a
     purple recolour was working against the showcase rather than
     for it. A tint gradient still stands in until --cover loads. */
  background-image: var(--cover, linear-gradient(160deg, hsl(var(--tint, 272) 55% 40%), hsl(var(--tint, 272) 65% 14%)));
  background-size: cover;
  background-position: center top;
  box-shadow: 0 22px 54px rgba(0, 0, 0, 0.6);
  overflow: hidden;
  transition: background-image 0.7s ease, box-shadow 0.35s ease, transform 0.35s var(--ease-out);
}

/* A soft purple glow behind the card and a slight lift on hover —
   the wheel doesn't pause for it any more, so this is what tells the
   viewer the featured card itself is the interactive-feeling one. */
.hub-shot:hover {
  box-shadow: 0 26px 60px rgba(0, 0, 0, 0.6), 0 0 70px rgba(157, 78, 221, 0.55);
  transform: translateY(-4px);
}

/* The blend into the core — its colour matches .dial-core's own
   fill exactly, so the join is invisible rather than a visible
   second gradient sitting on top of the first. */
/* A taller fade than a simple bottom trim — the lower part of the
   cover dissolves into the ring's own colour well before the card's
   actual bottom edge, so nothing reads as a hard rectangle sitting
   in the well. That dissolve is what sells "emerging": the visible
   poster effectively ends partway down, above the true box edge and
   clear of the ring's own border, and floats a little higher than
   the side tiles as a result. */
.hub-shot::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 24%;
  /* Just enough to dissolve the bottom edge into the dome behind it
     — not a large solid block. The whole point of showing the real
     cover was to let its own colours read; a tall, fully-opaque fade
     (like this used to be) ate most of the card back into a flat
     purple panel. Reaches full colour only right at the edge, not
     partway up, so nearly all of the card stays true-colour. */
  background: linear-gradient(to bottom, transparent, rgba(157, 78, 221, 0.4));
  pointer-events: none;
}

/* Name + case-study link for whichever project the card is showing
   — sits below the whole wheel rather than on the card itself, so
   the poster stays just the cover and this can carry as much text
   as a real project needs without crowding it. */
.project-info {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-top: clamp(10px, 1.6vw, 18px);
  text-align: center;
}

@media (min-width: 901px) {
  /* Pulled up into the dome's own empty space below the featured
     card — between the card's bottom (0.61 * --r above the
     baseline) and the baseline itself — instead of sitting in normal
     flow below the whole wheel. */
  .project-info {
    margin-top: calc(var(--r) * -0.42);
  }
}

.project-name {
  /* Poppins semibold, not the condensed display face used for page
     headings — League Gothic is drawn narrow on purpose for big
     type; at project-name's smaller size that same narrowness just
     read as shrunken. A plain, confident weight is what portfolio
     sites use for case-study titles. */
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(26px, 2.6vw, 42px);
  letter-spacing: -0.3px;
  color: var(--color-white);
  /* Sits over the dome's own wedge pattern now, not a plain
     background — the shadow is what keeps it legible regardless of
     what's behind it. */
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7);
}

.project-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: clamp(44px, 3.6vw, 52px);
  padding: 0 clamp(20px, 2vw, 28px);
  border-radius: 50px;
  border: 1px solid var(--glass-border);
  background: var(--glass-fill);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(15px, 1.1vw, 18px);
  color: var(--color-white);
  transition: transform 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
}

.project-link .arrow-icon {
  transition: transform 0.25s ease;
}

.project-link:hover {
  transform: translateY(-2px);
  background: var(--color-purple-strong);
  border-color: rgba(255, 255, 255, 0.32);
}

.project-link:hover .arrow-icon {
  transform: translate(2px, -2px);
}

/* =========================================================
   Showcase — a coverflow of glass cards

   Sized off the viewport's own height (vh), not --r-style radius
   math like the wheel above — that's what makes this section
   self-fitting on its own: shrink the browser window and the stage
   shrinks with it, rather than needing a hand-tuned floor per
   breakpoint the way the wheel's arrow-clearance did.
   ========================================================= */
#showcase {
  padding-top: clamp(28px, 5vh, 64px);
  padding-bottom: clamp(28px, 5vh, 64px);
  /* A full rounded square, not a landscape rectangle — one size for
     both width and height, off vmin so it respects whichever of the
     viewport's own width/height is tighter rather than overflowing
     the other. Shared with .showcase-nav's position too: the active
     card is always centred, so its corners sit at a fixed offset
     from the stage's own centre regardless of which project is
     showing — anchoring the buttons off this variable instead of
     tracking the card in JS keeps them glued to it for free. */
  --sc-card-s: clamp(260px, 48vmin, 520px);
}

#showcase .section-head {
  margin-bottom: clamp(16px, 2.6vh, 28px);
}

.showcase-stage {
  position: relative;
  /* Hugs the card rather than spanning the section's full width —
     a glass panel that size reads as its own framed piece, with the
     nav buttons landing in its corner near the card they control,
     instead of a nearly-invisible tint stretched edge to edge with
     the buttons stranded far off to the side of the actual card. */
  max-width: 860px;
  margin: 0 auto;
  /* Comfortably above --sc-card-s's own ceiling (520px) plus this
     rule's own padding (up to 32px a side) — otherwise the stage's
     overflow:hidden clips the card's top/bottom at large viewports
     where both hit their ceilings together. */
  height: clamp(300px, 52vh, 620px);
  padding: clamp(18px, 2.6vw, 32px);
  /* No glass panel behind the stage, and none on .showcase-info
     below it either — just the cards and plain text on the page. */
  /* The side cards translate out from centre; without this a narrow
     viewport shows a real horizontal scrollbar for however far they
     land off-screen. */
  overflow: hidden;
}

.showcase-track {
  position: relative;
  width: 100%;
  height: 100%;
  /* Gives the side cards' rotateY somewhere to actually recede into
     — without a perspective on the containing block a rotateY just
     skews flat instead of reading as a lean in 3D space. */
  perspective: 1400px;
}

.showcase-card {
  position: absolute;
  top: 50%;
  left: 50%;
  /* A true square (--sc-card-s for both), not the landscape
     rectangle this used to be — closer look at the reference showed
     a full rounded square, matching the heavier border-radius below. */
  width: var(--sc-card-s);
  height: var(--sc-card-s);
  border-radius: clamp(28px, 8vmin, 56px);
  overflow: hidden;
  background-image: var(--cover);
  background-size: cover;
  background-position: center top;
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55);
  cursor: pointer;
  opacity: 0;
  /* Centred fallback so a slow/failed script still leaves the cards
     stacked on their shared centre point instead of hanging off
     .showcase-track's own top-left corner — showcase.js overwrites
     this with the real per-offset transform once it runs. */
  transform: translate(-50%, -50%);
  /* transform (translate + the offset shift/scale JS computes) and
     opacity are the only things JS touches per frame — keeping them
     the only transitioned properties is what makes the coverflow
     step feel like one smooth move rather than a jump-cut. */
  transition: transform 0.6s var(--ease-out), opacity 0.6s ease;
}

.showcase-card.is-active {
  cursor: default;
}

/* The name/link panel — glassmorphism, matching the reference: only
   the centred card carries it, so it reads as one focused label
   rather than five overlapping ones. */
/* Sits below the stage now, in normal flow, rather than overlaid (and
   glass-backed) on the card itself — no background here at all, it's
   just centred text sitting directly on the page like .project-info
   does for the wheel above. */
.showcase-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-top: clamp(10px, 1.8vh, 18px);
  text-align: center;
}

.showcase-name {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(17px, 1.6vw, 22px);
  letter-spacing: -0.2px;
  color: var(--color-white);
}

.showcase-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(13px, 1vw, 15px);
  color: #c58bf5;
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s ease, color 0.3s ease;
}

.showcase-link:hover {
  color: #dcc0ff;
  border-bottom-color: currentColor;
}

.showcase-link .arrow-icon {
  width: 15px;
  height: 15px;
}

/* Small, one at each of the active card's own top corners (prev
   top-left, next top-right) — like controls that belong to the card
   itself, rather than a pair of big arrows bracketing the whole
   carousel from off to the sides, or both crowded onto one corner.
   Anchored off --sc-card-s (shared with .showcase-card's own size),
   not the stage's corner — the stage is wider than the card to leave
   room for the side cards to peek, so pinning these to the stage's
   own corner stranded them well past the card's actual edge. This
   keeps them glued to it regardless of viewport, with zero per-frame
   tracking needed since the active card's size never changes, only
   which project is on it. */
.showcase-nav {
  position: absolute;
  top: calc(50% - var(--sc-card-s) / 2 + 12px);
  /* Above every card's own z-index (the active one reaches 10) —
     otherwise the active card would paint over these. */
  z-index: 20;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.24);
  /* Lighter than the near-black it was — still dark enough to read
     as a control against a bright cover photo, just not as heavy. */
  background: rgba(45, 20, 60, 0.72);
  color: var(--color-white);
  cursor: pointer;
  transition: transform 0.3s var(--ease-out), background-color 0.3s ease, border-color 0.3s ease;
}

.showcase-nav svg {
  width: 16px;
  height: 16px;
}

.showcase-nav.prev {
  left: calc(50% - var(--sc-card-s) / 2 + 12px);
}

.showcase-nav.next {
  left: calc(50% + var(--sc-card-s) / 2 - 44px);
}

.showcase-nav:hover {
  transform: scale(1.08);
  background: rgba(157, 78, 221, 0.4);
  border-color: rgba(157, 78, 221, 0.85);
}

/* =========================================================
   Experience — timeline
   ========================================================= */
.timeline {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2vw, 24px);
  padding-left: 34px;
}

.timeline::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: linear-gradient(to bottom, rgba(157, 78, 221, 0.9), rgba(157, 78, 221, 0.05));
}

.timeline-item {
  position: relative;
}

.timeline-dot {
  position: absolute;
  left: -34px;
  top: 26px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--color-bg);
  border: 2px solid var(--color-purple);
  transition: box-shadow 0.4s ease, transform 0.4s var(--ease-out);
}

.timeline-item:hover .timeline-dot {
  transform: scale(1.25);
  box-shadow: 0 0 0 6px rgba(157, 78, 221, 0.18);
}

.timeline-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: clamp(22px, 2.4vw, 32px);
  transition: transform 0.45s var(--ease-out), border-color 0.4s ease;
}

.timeline-item:hover .timeline-card {
  transform: translateX(8px);
  border-color: rgba(157, 78, 221, 0.55);
}

.timeline-when {
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--color-purple);
}

.timeline-role {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-title);
  line-height: var(--lh-tight);
  letter-spacing: -0.3px;
}

.timeline-org {
  font-size: var(--fs-body);
  font-weight: 500;
  color: rgba(255, 255, 255, 0.8);
}

.timeline-text {
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-dim);
  text-wrap: pretty;
}

/* =========================================================
   Contact
   ========================================================= */
.section-contact {
  padding-bottom: clamp(48px, 6vw, 90px);
}

.contact-panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: flex-start;
  padding: clamp(30px, 4.5vw, 72px);
}

.contact-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(42px, 6.6vw, 96px);
  line-height: 0.94;
  letter-spacing: -1.5px;
  text-wrap: balance;
}

.contact-text {
  max-width: 46ch;
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-dim);
}

.mail-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  padding-bottom: 4px;
  font-weight: 500;
  font-size: clamp(18px, 2.6vw, 34px);
  letter-spacing: -0.5px;
  color: var(--color-white);
  border-bottom: 1px solid rgba(255, 255, 255, 0.28);
  word-break: break-all;
  transition: color 0.3s ease, border-color 0.3s ease;
}

.mail-link:hover {
  color: var(--color-purple);
  border-bottom-color: var(--color-purple);
}

.mail-link:hover .arrow-icon {
  transform: translate(3px, -3px);
}

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 18px;
}

.btn-resume.solid {
  height: 54px;
  width: auto;
  min-width: 168px;
  border-radius: 50px;
  font-size: 16px;
}

.socials {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.social {
  display: inline-flex;
  align-items: center;
  height: 54px;
  padding: 0 22px;
  border-radius: 50px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.05);
  font-size: var(--fs-small);
  transition: transform 0.3s var(--ease-out), background-color 0.3s ease, border-color 0.3s ease;
}

.social:hover {
  transform: translateY(-3px);
  background: rgba(157, 78, 221, 0.2);
  border-color: rgba(157, 78, 221, 0.7);
}

.contact-note {
  font-size: var(--fs-small);
  color: var(--text-faint);
}

/* =========================================================
   Footer — redesigned with torn-edge bg image + 3 columns
   ========================================================= */
.site-footer {
  position: relative;
  background-color: #6B21A8;
  color: #fff;
  font-family: var(--font-body);
}


.site-footer__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr;
  align-items: start;
  gap: 0;
  max-width: 1200px;
  margin: 0 auto;
  padding: clamp(40px, 5vw, 72px) clamp(24px, 5vw, 72px) clamp(40px, 5vw, 64px);
}

/* Vertical rule between columns */
.sf-rule {
  width: 1px;
  align-self: stretch;
  background: rgba(255, 255, 255, 0.25);
  margin: 0 clamp(24px, 4vw, 56px);
}

/* ---- Col 1: CTA ---- */
.sf-cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(14px, 2vw, 22px);
  padding-right: clamp(16px, 2vw, 32px);
}

.sf-heading {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(24px, 3.2vw, 44px);
  line-height: 1.15;
  color: #fff;
  margin: 0;
}

.sf-sub {
  font-size: clamp(13px, 1.1vw, 16px);
  font-weight: 400;
  color: rgba(255, 255, 255, 0.75);
  line-height: 1.6;
  margin: 0;
}

.sf-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  padding: 10px 22px;
  border: 1.5px solid rgba(255, 255, 255, 0.8);
  border-radius: 50px;
  font-family: var(--font-body);
  font-size: clamp(12px, 1vw, 14px);
  font-weight: 500;
  color: #fff;
  letter-spacing: 0.02em;
  transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.3s var(--ease-out);
}

.sf-btn:hover {
  background: rgba(255, 255, 255, 0.15);
  border-color: #fff;
  transform: translateY(-2px);
}

/* ---- Col 2 & 3 shared titles ---- */
.sf-col-title {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(16px, 1.4vw, 20px);
  color: #fff;
  margin: 0 0 clamp(18px, 2.4vw, 30px);
  letter-spacing: 0.01em;
}

/* ---- Col 2: Nav ---- */
.sf-nav {
  display: flex;
  flex-direction: column;
  gap: clamp(10px, 1.4vw, 16px);
}

.sf-link {
  font-size: clamp(13px, 1.1vw, 16px);
  font-weight: 400;
  color: rgba(255, 255, 255, 0.82);
  position: relative;
  display: inline-block;
  transition: color 0.25s ease, transform 0.25s var(--ease-out);
}

.sf-link::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 1px;
  background: #fff;
  transition: width 0.28s var(--ease-out);
}

.sf-link:hover {
  color: #fff;
  transform: translateX(4px);
}

.sf-link:hover::after {
  width: 100%;
}

/* ---- Col 3: Contact ---- */
.sf-contact-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(12px, 1.6vw, 18px);
}

.sf-contact-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: clamp(13px, 1.1vw, 15px);
  color: rgba(255, 255, 255, 0.82);
}

.sf-contact-list svg {
  flex-shrink: 0;
  opacity: 0.9;
}

.sf-link--contact {
  color: rgba(255, 255, 255, 0.82);
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, 0.4);
  text-underline-offset: 3px;
  transform: none;
}

.sf-link--contact:hover {
  color: #fff;
  text-decoration-color: #fff;
  transform: none;
}

.sf-link--contact::after {
  display: none;
}

/* ---- Bottom bar ---- */
.sf-bottom {
  text-align: center;
  padding: clamp(16px, 2vw, 24px) clamp(24px, 5vw, 72px) clamp(20px, 2.5vw, 32px);
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  font-size: clamp(11px, 0.9vw, 13px);
  color: rgba(255, 255, 255, 0.5);
  max-width: 1200px;
  margin: 0 auto;
}

/* ---- Mobile ---- */
@media (max-width: 860px) {
  .site-footer__inner {
    grid-template-columns: 1fr;
    padding-top: clamp(32px, 5vw, 52px);
    gap: 36px 0;
  }

  .sf-rule {
    width: 100%;
    height: 1px;
    align-self: auto;
    margin: 0;
  }

  .sf-cta { padding-right: 0; }
}

/* =========================================================
   Motion
   ========================================================= */
@keyframes nav-drop {
  from {
    opacity: 0;
    transform: translateY(-24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Variants that bake a static centering transform into the
   animation itself — a CSS animation's transform keyframes
   fully replace an element's own static `transform`, so any
   element that both animates and needs translateX(-50%) for
   positioning must use one of these instead of plain fade-up,
   or it silently loses its centering. */
@keyframes fade-up-x {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(28px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

@keyframes fade-up-xy {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) translateY(28px);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) translateY(0);
  }
}

@keyframes hero-bg-in {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(34px) scale(1.05);
    filter: blur(24px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1);
    filter: blur(0);
  }
}

@keyframes line-rise {
  from {
    transform: translateY(115%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes glow-in {
  from {
    opacity: 0;
    transform: translateX(-50%) scale(0.86);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) scale(1);
  }
}

@keyframes breathe {
  0%,
  100% {
    opacity: 0.82;
    transform: translateX(-50%) scale(0.97);
  }
  50% {
    opacity: 1;
    transform: translateX(-50%) scale(1.04);
  }
}

@keyframes pop-in {
  from {
    opacity: 0;
    transform: scale(0.4) rotate(-25deg);
  }
  to {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
}

@keyframes pop-in-cta {
  from {
    opacity: 0;
    transform: rotate(160deg) scaleY(-1) scale(0.4);
  }
  to {
    opacity: 1;
    transform: rotate(135deg) scaleY(-1) scale(1);
  }
}

@keyframes float {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-14px);
  }
}

@keyframes pulse-zoom {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.06);
  }
}

/* Scroll reveal — opt-in via JS so the page still shows
   everything if scripting is unavailable. */
.js .reveal {
  opacity: 0;
  transform: translateY(34px);
  transition: opacity 0.75s var(--ease-out), transform 0.75s var(--ease-out);
  transition-delay: var(--d, 0ms);
}

.js .reveal.in {
  opacity: 1;
  transform: none;
}

/* =========================================================
   Nav: hamburger + dropdown below 760px
   ========================================================= */
@media (max-width: 760px) {
  main {
    padding-top: 70px;
  }

  .navbar {
    justify-content: flex-end;
  }

  .nav-toggle {
    display: inline-flex;
  }

  .nav-pill {
    position: fixed;
    top: 84px;
    right: 20px;
    left: auto;
    max-width: min(72vw, 300px);
    flex-direction: column;
    align-items: stretch;
    overflow: visible;
    opacity: 0;
    transform: translateY(-12px) scale(0.97);
    pointer-events: none;
    visibility: hidden;
  }

  .nav-pill.open {
    opacity: 1;
    transform: translateY(0) scale(1);
    pointer-events: auto;
    visibility: visible;
  }

  .nav-link {
    width: 100%;
    text-align: center;
    padding: 14px 20px;
    font-size: 16px;
  }
}

/* =========================================================
   Layout: tablet
   ========================================================= */
@media (max-width: 900px) {
  .about-grid {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
  }

  .about-meta {
    align-items: center;
    text-align: left;
  }

  .software-row {
    justify-content: center;
  }

  /* The card keeps stacking on phones, rebuilt as: image
     across the top, then the numeral beside the copy. The
     image is capped in svh as well as by ratio so a card
     stays shorter than the screen even in landscape — a
     sticky card taller than the viewport strands its own
     bottom half. */
  :root {
    --stack-top: 76px;
  }

  .svc {
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
    min-height: 0;
    padding: 22px;
    top: calc(76px + var(--i) * 10px);
  }

  .svc-media {
    order: -1;
    aspect-ratio: 16 / 10;
    max-height: 40svh;
  }

  .svc-copy {
    gap: 12px;
  }

  .svc-list {
    padding-left: 20px;
  }

  /* A phone is narrower than the dial wants to be, so the radius is
     driven by the viewport width and the card inside it shrinks to
     match. --r and --ring live on #project (see the desktop rule's
     comment). */
  #project {
    /* 36vw keeps the dial (two radii wide) inside the page's own
       padding — anything larger and the section scrolls sideways. */
    --r: clamp(120px, 36vw, 195px);
    /* Projects ride further out so the card does not cover them —
       pushed out a bit further than before so there's real air
       around the featured card, not just enough to avoid the arrows. */
    --ring: 0.95;
  }

  .carousel {
    --tile-w: clamp(46px, 12vw, 66px);
  }

  .hub {
    /* Same scaled-off-radius formula as the desktop rule, narrowed
       the same way, so the card keeps real air around it at this
       breakpoint's own tighter geometry instead of crowding the
       side tiles. */
    width: clamp(42px, calc(var(--r) * 1.08 - 74px), 130px);
  }

  .dial-arrow {
    width: 38px;
    height: 38px;
  }

  /* .prev never got a matching mobile override before — it was
     still using the desktop 0.52 factor while .next used 0.66,
     leaving .prev sitting much closer to centre than intended and
     colliding with the (now wider) card. */
  .dial-arrow.prev {
    left: calc(50% - var(--r) * 0.66);
  }

  .dial-arrow.next {
    left: calc(50% + var(--r) * 0.66 - 40px);
  }
}

/* =========================================================
   Hero: stacked flow layout on small screens
   ========================================================= */
@media (max-width: 760px) {
  .hero {
    width: 100%;
    height: auto;
    aspect-ratio: auto;
    padding: 24px 24px 64px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 16px;
  }

  .hero-bg-text {
    position: static;
    transform: none;
    animation-name: fade-up;
    width: 100%;
    font-size: clamp(48px, 16vw, 160px);
    letter-spacing: -4px;
    white-space: normal;
    margin-bottom: -0.1em;
    order: 5;
  }

  .hero-greeting,
  .hero-name,
  .hero-role,
  .hero-photo-wrap,
  .hero-quote-wrap,
  .hero-cta {
    position: static;
    left: auto;
    top: auto;
    transform: none;
    width: 100%;
    min-width: 0;
  }

  .hero-greeting,
  .hero-role,
  .hero-cta {
    animation-name: fade-up;
  }

  .hero-greeting {
    order: 1;
    font-size: clamp(48px, 14vw, 96px);
  }

  .hero-name {
    order: 2;
    font-size: clamp(64px, 20vw, 150px);
    white-space: normal;
  }

  .hero-role {
    order: 3;
    font-size: clamp(26px, 7vw, 48px);
    white-space: normal;
  }

  .hero-photo-wrap {
    width: min(90vw, 420px);
    height: auto;
    aspect-ratio: 502 / 662;
    order: 4;
  }

  .hero-photo-shadow {
    display: none;
  }

  .hero-quote-wrap {
    order: 6;
    max-width: 420px;
    margin: 0 auto;
  }

  .hero-quote {
    font-size: clamp(18px, 5vw, 28px);
    text-align: center;
  }

  .squiggle-quote,
  .squiggle-cta {
    display: none;
  }

  .hero-cta {
    order: 7;
    width: 100%;
    justify-content: center;
  }

  .cta-pill {
    width: min(100%, 367px);
    min-width: 0;
  }

  .arrow-icon {
    width: 30px;
    height: 30px;
  }

  .timeline {
    padding-left: 26px;
  }

  .timeline-dot {
    left: -26px;
  }

  .timeline-item:hover .timeline-card {
    transform: none;
  }
}

/* Coarse pointers get no hover-lift (it sticks after a tap)
   and no pointer parallax. */
@media (hover: none) {
  .project:hover,
  .software-icon:hover,
  .social:hover {
    transform: none;
  }
}

/* =========================================================
   Gallery — skewed 3D card carousel (third project view)

   Adapted from the standalone card-carousel source. Cards
   tilt 60° on the Y-axis as they step off-centre, giving a
   much more dramatic perspective than the 8° tilt in
   SHOWCASE. Project name appears as an on-card caption on
   the active card; the Behance link lives in the controls
   bar below alongside the arrow buttons.
   ========================================================= */
#gallery {
  padding-top: clamp(40px, 6vw, 90px);
  padding-bottom: clamp(40px, 6vw, 90px);
}

#gallery .section-head {
  margin-bottom: clamp(16px, 2.4vw, 32px);
}

/* Stage — holds the 3D perspective and clips side overflow */
.gal-stage {
  position: relative;
  height: clamp(300px, 52vh, 520px);
  /* Perspective is set on the parent so all sibling cards share
     the same vanishing point — the same technique the original
     card-carousel uses on .carousel__stage. */
  perspective: 900px;
  overflow: hidden;
  cursor: grab;
  touch-action: pan-y;
}

.gal-stage:active {
  cursor: grabbing;
}

/* Track centres all cards; transform-style: preserve-3d lets the
   rotateY on each card read as a real 3D tilt. */
.gal-track {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-style: preserve-3d;
}

/* Cards — portrait 3:4, transform driven by three CSS custom
   properties set per-frame by JS:
   --offset  : signed integer position relative to active card
   --angle   : offset * 60deg — more dramatic tilt than SHOWCASE's 8°
   --distance: abs(offset) — drives the scale falloff           */
.gal-card {
  position: absolute;
  width: clamp(130px, 17vw, 240px);
  aspect-ratio: 3 / 4;
  border-radius: clamp(10px, 1.2vw, 16px);
  overflow: hidden;
  background-image: var(--cover);
  background-size: cover;
  background-position: center top;
  transform:
    translateX(calc(var(--offset) * 102%))
    rotateY(var(--angle))
    scale(calc(1 - min(var(--distance), 3) * 0.12));
  filter: brightness(0.55);
  opacity: 0.8;
  transition:
    transform 0.75s cubic-bezier(0.22, 0.8, 0.24, 1),
    filter 0.75s ease,
    opacity 0.75s ease;
  user-select: none;
  cursor: pointer;
}

.gal-card.is-active {
  filter: brightness(1);
  opacity: 1;
  z-index: 3;
  cursor: default;
  /* Purple halo on the active card — ties it to the site's
     purple identity while helping it read as the focal card
     without needing a hard border. */
  box-shadow:
    0 0 55px rgba(157, 78, 221, 0.38),
    0 28px 64px rgba(0, 0, 0, 0.65);
}

/* Caption — project name overlaid at the bottom of the active
   card only; the name on non-active cards stays hidden so it
   never competes with the caption of the card in focus.      */
.gal-caption {
  position: absolute;
  inset: auto 0 0;
  padding: 40px 16px 16px;
  background: linear-gradient(to bottom, transparent, rgba(10, 3, 20, 0.82));
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.45s ease;
}

.gal-card.is-active .gal-caption {
  opacity: 1;
}

.gal-caption h3 {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(14px, 1.3vw, 19px);
  color: var(--color-white);
  white-space: nowrap;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8);
}

/* Controls bar: [← Prev]  [View Case Study ↗]  [→ Next]
   A single row below the stage is cleaner and more compact
   than stacking the link separately, and makes this section
   read differently from the other two showcases which each
   have a standalone name+link block. */
.gal-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2.4vw, 30px);
  margin-top: clamp(14px, 2vh, 22px);
}

.gal-arrow {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(30, 12, 50, 0.70);
  color: var(--color-white);
  cursor: pointer;
  transition:
    transform 0.3s var(--ease-out),
    background-color 0.3s ease,
    border-color 0.3s ease;
}

.gal-arrow svg {
  width: 16px;
  height: 16px;
}

.gal-arrow:hover {
  transform: scale(1.1);
  background: rgba(157, 78, 221, 0.38);
  border-color: rgba(157, 78, 221, 0.8);
}

.gal-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(13px, 1.1vw, 16px);
  color: #c58bf5;
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s ease, color 0.3s ease;
}

.gal-link:hover {
  color: #dcc0ff;
  border-bottom-color: currentColor;
}

.gal-link .arrow-icon {
  width: 15px;
  height: 15px;
}


@media (max-width: 760px) {
  .gal-stage {
    height: clamp(240px, 46vh, 380px);
  }

  .gal-caption h3 {
    font-size: 13px;
    white-space: normal;
  }
}

/* =========================================================
   Experience — Envelope letter reveal (image-based)
   ========================================================= */

/* Scene grows downward when open to make room for the cards */
.ltr-scene {
  position: relative;
  padding-top: 0;
  transition: padding-top 0.72s var(--ease-out);
}

.ltr-scene.is-open {
  /* Cards emerge from envelope — only the bottom ~18% stays tucked in */
  padding-top: clamp(150px, 19vw, 260px);
}

/* Cards — absolute at top of scene, slide up on open.
   z-index 2 so they overlap the envelope image.
   Width matches the envelope (min 600px) so the outer card
   edges line up with the envelope edges. */
.ltr-cards {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(600px, 96%);
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: 0px;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%) translateY(80px);
  z-index: 2;
  transition:
    opacity 0.55s var(--ease-out),
    transform 0.65s var(--ease-out);
}

.ltr-scene.is-open .ltr-cards {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
  transition-delay: 0.12s;
}

/* Each card takes exactly 1/3 of the envelope width so all
   three together span edge-to-edge with the envelope below.
   mix-blend-mode: multiply drops the white card background on
   the dark site surface so only the text and icons show. */
.ltr-card {
  flex: 1 1 0;
  min-width: 0;
  width: calc(100% / 3);
  height: auto;
  border-radius: clamp(10px, 1.2vw, 16px);
  box-shadow: none;
  mix-blend-mode: multiply;
  transform: rotate(var(--tilt, 0deg)) translateY(20px) scale(1.08);
  opacity: 0;
  transition:
    opacity 0.5s var(--ease-out),
    transform 0.58s var(--ease-out);
  display: block;
}

.ltr-scene.is-open .ltr-card {
  opacity: 1;
  transform: rotate(var(--tilt, 0deg)) translateY(0) scale(1.08);
  transition-delay: calc(0.2s + var(--di, 0s));
}

/* Envelope button — uses the actual envelope image.
   z-index 1 so cards overlap it at the top edge. */
.ltr-env {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  width: min(600px, 96%);
  margin: 0 auto;
  border: none;
  background: none;
  padding: 0 0 clamp(16px, 2vw, 24px);
  cursor: pointer;
  outline-offset: 6px;
  transform: translateY(0);
  transition: transform 0.65s var(--ease-out);
}

.ltr-scene.is-open .ltr-env {
  transform: translateY(16px);
}

/* The envelope image fills the button width */
.ltr-env-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: clamp(12px, 1.6vw, 20px);
  transition: filter 0.35s ease, transform 0.35s var(--ease-out);
  pointer-events: none;
}

.ltr-env:hover .ltr-env-img {
  filter: brightness(1.08) drop-shadow(0 0 18px rgba(157, 78, 221, 0.55));
  transform: scale(1.012);
}

/* Hint text below the envelope image */
.ltr-hint {
  font-family: var(--font-body);
  font-size: clamp(12px, 1.1vw, 14px);
  font-weight: 400;
  color: var(--text-dim);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0;
  position: relative;
  height: 1.4em;
}

.ltr-hint-open,
.ltr-hint-close {
  display: inline-block;
  transition: opacity 0.3s ease, transform 0.3s var(--ease-out);
}

.ltr-hint-close {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%) translateY(6px);
  opacity: 0;
  white-space: nowrap;
}

.ltr-scene.is-open .ltr-hint-open {
  opacity: 0;
  transform: translateY(-6px);
}

.ltr-scene.is-open .ltr-hint-close {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@media (max-width: 760px) {
  .ltr-scene.is-open {
    padding-top: clamp(130px, 38vw, 240px);
  }
}
