/* ══════════════════════════════════════════════
   TERZA STUDIO — editorial agency site
   ══════════════════════════════════════════════ */

/* tokens.css is linked directly in every page's <head>, ahead of this file —
   an @import here would instead force the browser to fetch this stylesheet,
   parse it, and only then discover and fetch tokens.css, serialising a
   request that a plain <link> lets run in parallel */

*,
*::before,
*::after {
  box-sizing: border-box;
  corner-shape: squircle;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  color-scheme: light;
  /* Firefox and the newer standard: espresso thumb on a sand track */
  scrollbar-color: rgba(var(--color-espresso-rgb), .32) var(--color-bg-subtle);
  scrollbar-width: thin;
}

/* ── scrollbar (WebKit / Blink) ───────────────── */
::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}

::-webkit-scrollbar-track {
  background: var(--color-bg-subtle);
}

::-webkit-scrollbar-thumb {
  background: rgba(var(--color-espresso-rgb), .3);
  border-radius: var(--radius-pill);
  corner-shape: round;
  /* inset the thumb inside the track without changing the hit area */
  border: 3px solid var(--color-bg-subtle);
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--color-terracotta-rgb), .55);
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:active {
  background: var(--color-terracotta);
  background-clip: padding-box;
}

::-webkit-scrollbar-corner {
  background: var(--color-bg-subtle);
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--text-18px);
  line-height: var(--lh-1-6);
  font-weight: var(--fw-400);
  letter-spacing: var(--ls-0-005em);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

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

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

.text-link {
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0% 100%;
  background-repeat: no-repeat;
  background-size: 0% 1px;
  padding-bottom: 3px;
  transition: color 0.25s, background-color 0.25s, background-size 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}

.text-link:hover,
.text-link:focus {
  background-size: 100% 1px;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none
}

.heading {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: var(--fw-600);
  font-variation-settings: 'opsz' 120, 'SOFT' 0;
  letter-spacing: var(--ls-neg-0-02em);
  line-height: var(--lh-1);
}

.text-accent {
  font-style: italic;
  font-weight: var(--fw-400);
  color: var(--color-accent)
}

::selection {
  background: var(--color-text);
  color: var(--color-bg)
}

/* where a text-fragment link lands — the browser's default is a hard yellow
   that fights the palette. softer than ::selection because this one persists
   after arrival rather than tracking a drag */
::target-text {
  background: color-mix(in srgb, var(--color-accent) 26%, transparent);
  color: var(--color-text);
}

/* the same mark, painted by script.js where the engine did not resolve the
   text directive itself — it has to match ::target-text exactly or the two
   routes to the same link would land differently */
::highlight(text-fragment) {
  background: color-mix(in srgb, var(--color-accent) 26%, transparent);
  color: var(--color-text);
}

/* readable by assistive tech, invisible on screen */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--color-text);
  color: var(--color-bg);
  padding: 12px 18px;
  font-size: var(--text-14px);
}

.skip-link:focus {
  left: 12px;
  top: 12px
}

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

.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-padding);
}

/* ── glassmorphism (iOS-style) ───────────────── */
.glass {
  background: linear-gradient(180deg, rgba(255, 252, 246, .74), rgba(255, 250, 240, .42));
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  border: 1px solid rgba(255, 253, 248, .7);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .85),
    inset 0 -1px 0 rgba(var(--color-espresso-rgb), .05),
    0 14px 34px -14px rgba(var(--color-espresso-rgb), .2),
    0 2px 6px -2px rgba(var(--color-espresso-rgb), .06);
}

/* ── buttons ─────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-ui);
  font-weight: var(--fw-600);
  font-size: var(--text-14px);
  letter-spacing: var(--ls-neg-0-01em);
  padding: var(--btn-pad);
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  white-space: nowrap;
  /* the hover colour is a background layer parked at zero height on the
     baseline; hovering grows it up through the button like ink taking to
     paper. same mechanic .text-link uses to draw its underline in */
  background-repeat: no-repeat;
  background-position: 50% 100%;
  background-size: 100% 0;
  transition: background-size .45s var(--ease-ink), background-color .3s var(--ease-ink),
    color .3s var(--ease-ink), border-color .3s var(--ease-ink), transform .16s var(--ease-ink);
}

.btn:hover {
  background-size: 100% 100%;
}

/* pressed, not lifted: a stamp seats into the page rather than hovering off it */
.btn:active {
  transform: translateY(1px);
}

.btn-solid {
  background-color: var(--color-text);
  background-image: linear-gradient(var(--color-accent), var(--color-accent));
  color: var(--color-bg);
}

.btn-accent {
  background-color: var(--color-accent);
  background-image: linear-gradient(var(--color-text), var(--color-text));
  color: var(--color-bg);
  text-transform: uppercase;
  letter-spacing: var(--ls-0-08em);
}

.btn-ghost {
  border-color: var(--color-border);
  color: var(--color-text);
  background-image: linear-gradient(var(--color-text), var(--color-text));
}

/* the label flips as the ink passes it */
.btn-ghost:hover {
  border-color: var(--color-text);
  color: var(--color-bg);
}

/* the one place a button is not a pill: sitting flush against a field, it
   takes the field's corner so the pair reads as a single control */
.newsletter-form .btn,
.cta-form .btn {
  border-radius: var(--radius-xs);
  corner-shape: round;
}

.btn-lg {
  padding: var(--btn-pad-lg);
  font-size: var(--text-14px)
}

/* ══ NAV ══════════════════════════════════════ */
.nav {
  position: fixed;
  inset: 16px 0 auto;
  z-index: 100;
  transition: transform .5s var(--ease-out-expo);
}

/* progressive blur: five stacked layers, each blurred more than the last
   and masked to a shorter, higher band — so instead of one flat frosted
   strip with a hard edge, the blur tapers smoothly from strong at the very
   top of the viewport down to nothing by the bottom of the nav */
.nav-blur {
  position: absolute;
  inset: -16px 0 auto;
  height: 130px;
  z-index: -1;
  pointer-events: none;
}

.nav-blur span {
  position: absolute;
  inset: 0;
}

.nav-blur span:nth-child(1) {
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
  mask-image: linear-gradient(to bottom, black 0%, black 60%, transparent 80%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 60%, transparent 80%);
}

.nav-blur span:nth-child(2) {
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  mask-image: linear-gradient(to bottom, black 0%, black 45%, transparent 65%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 45%, transparent 65%);
}

.nav-blur span:nth-child(3) {
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  mask-image: linear-gradient(to bottom, black 0%, black 30%, transparent 50%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 30%, transparent 50%);
}

.nav-blur span:nth-child(4) {
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  mask-image: linear-gradient(to bottom, black 0%, black 18%, transparent 35%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 18%, transparent 35%);
}

.nav-blur span:nth-child(5) {
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  mask-image: linear-gradient(to bottom, black 0%, black 8%, transparent 20%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 8%, transparent 20%);
}

.nav-inner {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: 10px 0;
}

.nav-logo {
  display: flex;
  align-items: center;
  /* below the gutter breakpoint the margin is narrower than the lockup,
     so fall back to aligning with the content column's left edge */
  padding-inline: var(--container-padding);
}

.nav-logo img {
  /* the lockup is 375x108, so height alone sets the size */
  height: 26px;
  width: auto;
}

/* the empty margin beside the content column:
   half the leftover viewport, plus the container's own padding */
@media (min-width: 1560px) {
  .nav-logo {
    padding-inline: 0;
    width: calc((100% - var(--container-max)) / 2 + var(--container-padding));
    justify-content: center;
  }
}

/* ══ TABLE OF CONTENTS ════════════════════════ */
.toc {
  position: fixed;
  top: 50%;
  /* hidden until the hero is scrolled past — see .is-visible below */
  translate: -10px -50%;
  opacity: 0;
  pointer-events: none;
  /* narrow gutters only fit the numerals, so centre those in the padding */
  left: calc(var(--container-padding) / 2 - 4px);
  z-index: 97;
  transition: opacity .45s var(--ease-out-expo), translate .45s var(--ease-out-expo);
}

.toc.is-visible {
  translate: 0 -50%;
  opacity: 1;
  pointer-events: auto;
}

/* under ~1560px the gutter is too narrow for the rule + label to sit
   beside the content column, so show the numerals alone */
@media (max-width: 1559px) {

  .toc-rule,
  .toc-label {
    display: none;
  }
}

/* on wide screens sit in the empty gutter, like the logo above */
@media (min-width: 1560px) {
  .toc {
    left: calc(((100% - var(--container-max)) / 2 + var(--container-padding)) / 2 - 12px);
  }
}

.toc ol {
  display: grid;
  gap: var(--space-md);
}

.toc-link {
  display: flex;
  align-items: center;
  gap: 10px;
  width: fit-content;
  color: var(--color-text-subtle);
  transition: color .35s var(--ease-out-expo);
}

.toc-number {
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-0-04em);
}

.toc-rule {
  display: block;
  width: 0;
  height: 1px;
  background: currentColor;
  transition: width .45s var(--ease-out-expo);
}

.toc-label {
  font-family: var(--font-serif-small);
  font-style: italic;
  font-size: var(--text-14px);
  white-space: nowrap;
  opacity: 0;
  translate: -6px 0;
  transition: opacity .35s var(--ease-out-expo), translate .35s var(--ease-out-expo);
}

.toc-link:hover,
.toc-link.is-active {
  color: var(--color-text);
}

.toc-link:hover .toc-rule,
.toc-link.is-active .toc-rule {
  width: 26px;
}

.toc-link:hover .toc-label,
.toc-link.is-active .toc-label {
  opacity: 1;
  translate: 0 0;
}

.toc-link.is-active {
  color: var(--color-accent);
}

.nav-links {
  display: flex;
  gap: 6px;
  margin-inline: auto;
}

.nav-links a {
  font-size: var(--text-14px);
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  color: var(--color-text-muted);
  transition: color .25s, background .25s;
}

.nav-links a:hover,
.nav-links a.is-active {
  color: var(--color-text);
  background: rgba(var(--color-espresso-rgb), .07)
}

.nav-toggle {
  display: none;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-full);
  background: rgba(var(--color-espresso-rgb), .07);
  border: 0;
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}

.nav-toggle span {
  width: 16px;
  height: 1.5px;
  background: var(--color-text);
  transition: transform .3s var(--ease-out-expo)
}

.nav-toggle[aria-expanded="true"] span:first-child {
  transform: translateY(3.25px) rotate(45deg)
}

.nav-toggle[aria-expanded="true"] span:last-child {
  transform: translateY(-3.25px) rotate(-45deg)
}

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: rgba(247, 242, 233, .9);
  backdrop-filter: blur(30px) saturate(180%);
  -webkit-backdrop-filter: blur(30px) saturate(180%);
  display: grid;
  place-items: center;
}

.mobile-menu[hidden] {
  display: none;
}

.mobile-menu nav {
  display: grid;
  gap: var(--space-sm);
  text-align: center
}

.mobile-menu a {
  font-family: var(--font-serif);
  font-weight: var(--fw-600);
  font-size: var(--text-56px);
  letter-spacing: var(--ls-neg-0-02em);
}

/* ── floating CTA ────────────────────────────── */
.chat-dock {
  position: fixed;
  right: clamp(16px, 2vw, 32px);
  bottom: 24px;
  z-index: 98;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
  /* the dock is only a rail — clicks belong to the bubble and button */
  pointer-events: none;
}

.chat-dock>* {
  pointer-events: auto;
}

.floating-cta {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: var(--btn-pad);
  border-radius: var(--radius-pill);
  corner-shape: round;
  /* the primary action on the page — solid, not glass */
  background-color: var(--color-accent);
  color: var(--color-bg);
  box-shadow: 0 14px 30px -14px rgba(var(--color-espresso-rgb), .45);
  font-family: var(--font-ui);
  font-weight: var(--fw-600);
  font-size: var(--text-14px);
  opacity: 0;
  transform: translateY(20px) scale(.96);
  pointer-events: none;
  background-image: linear-gradient(var(--color-text), var(--color-text));
  background-repeat: no-repeat;
  background-position: 50% 100%;
  background-size: 100% 0;
  transition: opacity .5s var(--ease-out-expo), transform .5s var(--ease-out-expo),
    background-size .45s var(--ease-ink);
}

.floating-cta:hover {
  background-size: 100% 100%;
}

.floating-cta.is-visible {
  opacity: 1;
  transform: none;
  pointer-events: auto
}

.floating-cta:hover {
  transform: translateY(-3px)
}

.floating-cta-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  corner-shape: round;
  /* rides the button's own text colour, so it stays legible if the
     fill ever changes — olive would go muddy on terracotta */
  background: currentColor;
  box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 50%, transparent);
  animation: pulse 2.4s infinite;
}

/* a message "arrives": the button gives a short nudge */
.floating-cta.is-nudging {
  animation: nudge .7s var(--ease-out-expo);
}

@keyframes nudge {

  0%,
  100% {
    transform: none
  }

  28% {
    transform: translateY(-5px) rotate(-1.6deg)
  }

  58% {
    transform: translateY(0) rotate(1deg)
  }

  80% {
    transform: translateY(-1px) rotate(-.4deg)
  }
}

/* ── chat bubble ─────────────────────────────── */
.chat-bubble {
  max-width: min(260px, 62vw);
  padding: 11px 15px;
  /* squared-off corner points at the button, like a speech tail */
  border-radius: 16px 16px 4px 16px;
  /* opt out of the global squircle, matching the button */
  corner-shape: round;
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  font-weight: var(--fw-500);
  line-height: var(--lh-1-35);
  color: var(--color-text);
  transform-origin: bottom right;
  opacity: 0;
  translate: 0 10px;
  scale: .94;
  pointer-events: none;
  transition:
    opacity .4s var(--ease-out-expo),
    translate .4s var(--ease-out-expo),
    scale .4s var(--ease-out-expo);
}

.chat-bubble.is-visible {
  opacity: 1;
  translate: 0 0;
  scale: 1;
}

.chat-bubble-typing {
  display: none;
  align-items: center;
  gap: 4px;
  /* match a line of text so the bubble doesn't resize on swap */
  height: 1.35em;
}

.chat-bubble.is-typing .chat-bubble-typing {
  display: inline-flex;
}

.chat-bubble.is-typing .chat-bubble-text {
  display: none;
}

.chat-bubble-typing i {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  corner-shape: round;
  background: var(--color-text-subtle);
  animation: chat-dot 1.2s ease-in-out infinite;
}

.chat-bubble-typing i:nth-child(2) {
  animation-delay: .16s;
}

.chat-bubble-typing i:nth-child(3) {
  animation-delay: .32s;
}

@keyframes chat-dot {

  0%,
  62%,
  100% {
    transform: none;
    opacity: .4
  }

  31% {
    transform: translateY(-4px);
    opacity: 1
  }
}

@keyframes pulse {
  70% {
    box-shadow: 0 0 0 9px transparent
  }

  100% {
    box-shadow: 0 0 0 0 transparent
  }
}

/* ══ SECTION PRIMITIVES ═══════════════════════ */
.section {
  padding-block: clamp(80px, 11vw, var(--space-4xl));
  position: relative;
  /* clip, not hidden — keeps the section out of the scroll-container business */
  overflow: clip;
}

.section>.container {
  position: relative;
  z-index: 1;
}

/* ── parallax section backdrop ───────────────── */
.section-backdrop {
  position: absolute;
  /* overscan vertically so the drift never exposes a hard edge */
  inset: -25% 0;
  z-index: 0;
  pointer-events: none;
  will-change: translate;
}

.section-backdrop::before,
.section-backdrop::after {
  content: '';
  position: absolute;
  aspect-ratio: 1;
  border-radius: var(--radius-full);
}

.section-backdrop::before {
  left: -12%;
  top: 4%;
  width: min(760px, 72vw);
  background: radial-gradient(circle, rgba(var(--color-terracotta-rgb), .10), transparent 68%);
}

.section-backdrop::after {
  right: -10%;
  bottom: 2%;
  width: min(600px, 58vw);
  background: radial-gradient(circle, rgba(var(--color-olive-rgb), .09), transparent 68%);
}

/* alternate the tints so consecutive sections don't read as a repeat */
.section-backdrop.is-flipped::before {
  background: radial-gradient(circle, rgba(var(--color-olive-rgb), .10), transparent 68%);
}

.section-backdrop.is-flipped::after {
  background: radial-gradient(circle, rgba(var(--color-terracotta-rgb), .08), transparent 68%);
}

.section+.section {
  border-top: 1px solid var(--color-border-subtle)
}

.section-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-xl);
}

.section-number {
  font-family: var(--font-ui);
  font-weight: var(--fw-700);
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-06em);
  color: var(--color-accent);
}

.section-label {
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-18em);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  font-weight: var(--fw-400);
}

.section-title {
  font-size: var(--text-112px);
  letter-spacing: var(--ls-neg-0-025em);
  line-height: var(--lh-0-94);
  margin-bottom: var(--space-2xl);
}

.eyebrow {
  margin: 0 0 var(--space-lg);
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-2em);
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

/* ══ HERO ═════════════════════════════════════ */
.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  /* svh keeps mobile browser chrome from pushing content out of view */
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: clamp(96px, 13vh, 150px);
  /* clears the fixed nav so the optical centre sits below it */
  padding-bottom: clamp(48px, 7vw, 90px);
  overflow: hidden;
  text-align: center;
}

.hero-inner {
  position: relative;
  /* above the particle field and the glow */
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.hero-particles {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* faded in once the field has been seeded, to avoid a first-frame pop */
  opacity: 0;
  transition: opacity 1.2s var(--ease-out-expo);
}

.hero-particles.is-visible {
  opacity: 1;
}

.hero::after {
  /* soft top glow */
  content: '';
  position: absolute;
  top: -40%;
  left: 50%;
  translate: -50% 0;
  width: min(1100px, 120vw);
  aspect-ratio: 1;
  background: radial-gradient(circle, rgba(255, 251, 242, .3), transparent 62%);
  pointer-events: none;
}

.grid-lines {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: space-between;
  padding-inline: var(--container-padding);
  max-width: var(--container-max);
  margin-inline: auto;
  pointer-events: none;
}

.grid-lines i {
  width: 1px;
  background: var(--color-border-subtle)
}

.hero-title {
  position: relative;
  font-size: var(--text-128px);
  line-height: var(--lh-0-92);
  letter-spacing: var(--ls-neg-0-03em);
  font-weight: var(--fw-600);
  margin-bottom: var(--space-xs);
}

.line-mask {
  display: block;
  overflow: visible;
  /* pad the mask so serif descenders aren't clipped, then pull it back
     so the extra space doesn't open a gap between lines */
  padding-bottom: .14em;
  margin-bottom: -.14em;
}

.line-mask>span {
  display: block;
  transform: translateY(105%);
  transition: transform 1.05s var(--ease-out-expo);
}

.is-revealed .line-mask>span {
  transform: none
}

.line-mask:nth-child(2)>span {
  transition-delay: .11s
}

.hero-lede {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}

.hero-tagline {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: var(--fw-500);
  font-size: var(--text-34px);
  letter-spacing: var(--ls-neg-0-015em);
  color: var(--color-text);
}

.word-swap {
  display: inline-grid;
  position: relative;
  clip-path: inset(0 -50vw 0 0);
  overflow: visible;
  vertical-align: bottom;
  color: var(--color-accent);
  font-weight: var(--fw-600);
  transition: width .6s var(--ease-out-expo);
  text-align: left;
  justify-items: start;
}

.word-swap-text {
  grid-area: 1 / 1;
  white-space: nowrap;
  justify-self: start;
}

.word-swap-text.is-leaving {
  transform: translateY(-100%);
  opacity: 0;
  transition: transform .6s var(--ease-out-expo), opacity .6s var(--ease-out-expo);
}

.word-swap-text.is-entering {
  animation: swapIn .6s var(--ease-out-expo) both;
}

@keyframes swapIn {
  from {
    transform: translateY(100%);
    opacity: 0;
  }

  to {
    transform: none;
    opacity: 1;
  }
}

/* ══ 01 SERVICES ══════════════════════════════ */
.service-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md)
}

/* invisible above the mobile stacking breakpoint — the card behaves as if
   this wrapper wasn't there at all, so it's still a direct grid item */
.service-cards-pin {
  display: contents;
}

.service-card {
  position: relative;
  border-radius: var(--radius-md);
  padding: var(--space-xl) var(--space-lg) var(--space-lg);
  transition: transform .55s var(--ease-out-expo), border-color .4s;
}

.service-card:hover {
  transform: translateY(-8px);
  border-color: rgba(255, 253, 248, .9);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .95), 0 22px 46px -16px rgba(var(--color-espresso-rgb), .24), 0 3px 8px -3px rgba(var(--color-espresso-rgb), .08)
}

.service-card-index {
  font-family: var(--font-ui);
  font-weight: var(--fw-700);
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-06em);
  color: var(--color-text-subtle);
}

.service-card .service-title {
  font-size: var(--text-34px);
  letter-spacing: var(--ls-neg-0-015em);
  margin: var(--space-lg) 0 var(--space-sm);
}

.service-card>.service-desc {
  margin: 0 0 var(--space-lg);
  color: var(--color-text-muted);
  font-size: var(--text-18px)
}

.service-card ul {
  display: grid;
  gap: 9px;
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-md)
}

.service-card li {
  font-size: var(--text-16px);
  color: var(--color-text-subtle);
  display: flex;
  gap: 10px;
  align-items: baseline;
}

.service-card li::before {
  content: '—';
  color: var(--color-text-subtle)
}

/* ══ 02 WORK ══════════════════════════════════ */
.work-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-lg)
}

.project-card {
  display: block
}

.project-thumb {
  aspect-ratio: 4/3;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: linear-gradient(150deg, var(--color-fill), var(--color-fill-strong));
  display: grid;
  place-items: center;
  overflow: hidden;
  transition: border-color .4s;
}

.project-thumb img {
  width: 100%;
  /* taller than the frame so the parallax drift stays covered */
  height: 116%;
  object-fit: cover;
  will-change: translate;
  transition: scale .7s var(--ease-out-expo);
}

.project-card:hover .project-thumb {
  border-color: rgba(var(--color-espresso-rgb), .26)
}

.project-card:hover .project-thumb img {
  scale: 1.05;
}

.project-meta {
  padding-top: var(--space-md)
}

.project-meta .project-title {
  margin: 0;
  font-family: var(--font-serif-small);
  font-size: var(--text-24px);
  letter-spacing: var(--ls-neg-0-015em)
}

.project-category {
  margin: 0 0 6px;
  font-size: var(--text-14px);
  font-weight: var(--fw-700);
  letter-spacing: var(--ls-0-06em);
  text-transform: uppercase;
  color: var(--color-accent)
}



.project-link {
  display: inline-flex;
  gap: 8px;
  margin-top: var(--space-md);
  font-family: var(--font-ui);
  font-weight: var(--fw-600);
  font-size: var(--text-16px);
}

.project-link i {
  transition: transform .4s var(--ease-out-expo)
}

.project-card:hover .project-link i {
  transform: translateX(6px)
}

/* neither tablets nor phones reliably hover, so the reveal that normally
   waits for :hover — the thumb's border tint, the image scale, the arrow
   nudge — plays on press instead. otherwise a touch user never sees any
   of it, since :hover just never fires for them */
@media (hover: none) {
  .project-card:active .project-thumb {
    border-color: rgba(var(--color-espresso-rgb), .26)
  }

  .project-card:active .project-thumb img {
    scale: 1.05
  }

  .project-card:active .project-link i {
    transform: translateX(6px)
  }
}

/* tablet and phone: a plain list — a big stacked image-card reads as a
   lot of scrolling for not much information at these widths, and a
   thumbnail-plus-text row scans faster than a 2-column grid would. one
   treatment for both, since a list doesn't have a column count to worry
   about (no awkward odd-one-out card to special-case, unlike a grid) */
@media (max-width: 1024px) {
  .work-grid {
    display: block;
  }

  .project-card {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding-block: var(--space-md);
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .project-card:first-child {
    border-top: 1px solid var(--color-border-subtle);
  }

  .project-thumb {
    flex: 0 0 96px;
    aspect-ratio: 1;
    border-radius: var(--radius-sm);
  }

  .project-meta {
    flex: 1 1 auto;
    min-width: 0;
    padding-top: 0;
  }

  .project-category {
    margin-bottom: 4px;
  }

  /* the whole row is already the link — spelling it out again here is
     redundant once the row has enough presence of its own to read as
     tappable */
  .project-link {
    display: none;
  }
}

/* tablet gets more room than a phone does, so the list can afford to sit
   bigger — a larger thumbnail, more breathing room, a larger title —
   rather than reusing the same compact sizing all the way up from a
   phone screen */
@media (min-width: 641px) and (max-width: 1024px) {
  .project-card {
    gap: var(--space-lg);
    padding-block: var(--space-lg);
  }

  .project-thumb {
    flex-basis: 160px;
  }

  /* the extra class beats .project-meta .project-title's own rule (and
     the later font-size overrides at other breakpoints) regardless of
     which comes first in the file */
  .work-grid .project-meta .project-title {
    font-size: var(--text-28px);
  }

  .project-link {
    font-size: var(--text-16px);
  }
}

/* phone: the thumbnail is the first thing competing with the text for a
   narrow row's width — shrink it a little further */
@media (max-width: 480px) {
  .project-thumb {
    flex-basis: 72px;
  }
}

/* ══ 03 PROCESS ═══════════════════════════════ */
.steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--color-border-subtle);
  border-top: 1px solid var(--color-border-subtle);
  border-bottom: 1px solid var(--color-border-subtle);
}

.step {
  background: var(--color-bg);
  padding: var(--space-xl) var(--space-lg) var(--space-2xl);
  position: relative;
  transition: background .4s;
}

.step:hover {
  background: var(--color-bg-subtle)
}

.step-number {
  font-family: var(--font-serif);
  font-weight: var(--fw-600);
  font-size: var(--text-56px);
  letter-spacing: var(--ls-neg-0-02em);
  color: rgba(var(--color-terracotta-rgb), .32);
  display: block;
  margin-bottom: var(--space-lg);
  will-change: translate;
}

.step .step-title {
  font-size: var(--text-24px);
  letter-spacing: var(--ls-neg-0-01em);
  margin-bottom: var(--space-sm)
}

.step .step-desc {
  margin: 0;
  font-size: var(--text-18px);
  color: var(--color-text-muted)
}

/* ══ ABOUT ════════════════════════════════════ */
.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--space-3xl);
  align-items: start;
}

.about .section-title {
  margin-bottom: var(--space-lg)
}

.about-copy {
  margin: 0;
  color: var(--color-text-muted);
  max-width: 40ch
}

/* the section is a doorway into the about page, not the whole story */
.about-more {
  margin-top: var(--space-lg);
}

.member-tile {
  display: block;
  color: inherit;
}

.member-tile .member-name {
  transition: color .3s var(--ease-out-expo);
}

.member:hover .member-name {
  color: var(--color-accent);
}

.team {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-lg)
}

.member-avatar {
  display: block;
  width: 100%;
  /* the width/height attributes set a height presentational hint; without
     this the hint wins and aspect-ratio below never applies */
  height: auto;
  object-fit: cover;
  aspect-ratio: 1;
  /* square, matching the collective portraits on the about page */
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  transition: transform .55s var(--ease-out-expo), border-color .4s;
}

.member:hover .member-avatar {
  transform: translateY(-6px);
  border-color: var(--color-text-subtle);
  color: var(--color-text)
}

.member .member-name {
  font-size: var(--text-24px);
  margin-top: var(--space-md);
  letter-spacing: var(--ls-neg-0-01em)
}

.member .member-role {
  margin: 4px 0 0;
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-14em);
  text-transform: uppercase;
  color: var(--color-text-subtle)
}

.member .member-desc {
  margin: 12px 0 0;
  font-size: var(--text-16px);
  color: var(--color-text-muted);
}

/* ══ FOOTER ═══════════════════════════════════ */
.footer {
  padding-block: clamp(80px, 11vw, var(--space-4xl)) var(--space-xl);
  border-top: 1px solid var(--color-border);
  background: linear-gradient(180deg, var(--color-bg), var(--color-bg-subtle));
}

.footer-headline {
  /* lead text on the left half, the swappable word on the right */
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.25em;
  align-items: start;
  font-family: var(--font-serif);
  font-weight: var(--fw-600);
  /* capped so the lead lines fit their half column without re-wrapping:
     "something" runs ~4em, and half the container has to hold it */
  font-size: var(--text-112px);
  line-height: var(--lh-0-94);
  letter-spacing: var(--ls-neg-0-03em);
  margin-bottom: var(--space-xl);
}

.footer-headline-lead .line-mask {
  /* two lines, always — never let the column re-break them */
  white-space: nowrap;
}

.footer-headline-pick {
  /* drop by exactly one line so the word starts on "something", less the
     half-leading the taller roll line-height adds above the first baseline */
  margin-block-start: .835em;
  margin-block-start: calc(1lh - .105em);
  min-width: 0;
}

/* stacked on narrow screens — half a column is too little to set in */
@media (max-width: 1200px) {
  .footer-headline {
    grid-template-columns: 1fr;
  }

  .footer-headline-pick {
    margin-block-start: 0;
  }

  /* the button is nested inside .footer-headline-lead, ahead of the
     "loud." pick in the DOM — display:contents drops the wrapper's own
     box so its children (the two lines and the button) join .pick as
     direct siblings in the stack, and `order` alone can then push the
     button after all of them instead of between the lines and the word */
  .footer-headline-lead {
    display: contents;
  }

  /* the extra class beats .footer-cta-wrapper's own base rule (same
     specificity otherwise) regardless of which comes first in the file */
  .footer-headline .footer-cta-wrapper {
    order: 1;
    justify-content: flex-start;
    /* grid items stretch to fill the column by default; without this the
       wrapper (and the button riding along with it) would span the full
       width instead of sizing to its own content */
    justify-self: start;
    /* .word-pick-hint's own margin-bottom (below) plus the grid gap above
       already add up to real space here; anything on the wrapper itself
       stacks on top of both */
    margin-top: 0;
  }

  /* its margin-bottom sits inside .footer-headline-pick's own box, ahead
     of the button's grid cell — left at --space-lg it was one more
     stacked contribution to that gap */
  .word-pick-hint {
    margin-bottom: var(--space-sm);
  }
}

/* ── the swappable word ──────────────────────── */
.word-pick {
  /* a button, but it has to sit in the sentence like plain text */
  font: inherit;
  letter-spacing: var(--ls-inherit);
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  position: relative;
  display: inline-block;
  text-align: left;
  /* never wider than the headline column: a word too long for the
     remaining space drops to the next line, and one too long for a
     whole line breaks inside itself instead of running off the edge */
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  hyphens: auto;
}

.word-pick-text {
  font-style: italic;
  font-weight: var(--fw-400);
  color: var(--color-accent);
  align-content: start;
  transition:
    opacity .22s var(--ease-out-expo),
    translate .22s var(--ease-out-expo);
}

/* ── odometer roll: each letter rolls to the next ── */
.word-roll {
  display: inline-flex;
  flex-wrap: wrap;
  vertical-align: baseline;
  overflow: visible;
  /* room for the glyphs: at the headline's .94 leading the box is
     shorter than an italic ascender-to-descender run, which is what
     was slicing the letters */
  line-height: var(--lh-1-15);
}

.word-roll-cell {
  position: relative;
  display: block;
  /* clips top and bottom at the box edge, but the negative side insets
     let italic glyphs overhang their advance width instead of being cut */
  clip-path: inset(0 -.32em);
}

/* a cell whose letter is leaving for good needs more sideways room,
   because the incoming face is empty and gives the cell no width */
.word-roll-cell.is-vanishing {
  clip-path: inset(0 -.32em);
}

.word-roll-face {
  display: block;
  white-space: pre;
}

/* the outgoing letter is taken out of flow, so the incoming one sets the
   cell width from the very first frame — nothing shifts sideways mid-roll */
.word-roll-out {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
}

.word-roll-in {
  translate: 0 100%;
}

.word-roll-out,
.word-roll-in {
  transition: translate .62s var(--ease-out-expo);
}

/* the cell resizes only once the roll is under way, so pinning the
   outgoing width beforehand doesn't animate from nothing */
.word-roll.is-rolling .word-roll-cell {
  transition: width .62s var(--ease-out-expo);
}

.word-roll.is-rolling .word-roll-out {
  translate: 0 -100%;
}

.word-roll.is-rolling .word-roll-in {
  translate: 0 0;
}

/* a small nudge that the word is clickable, shown until first use.
   it sits outside the headline because .line-mask clips its overflow */
.word-pick-hint {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: var(--space-md) 0 var(--space-lg);
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  font-weight: var(--fw-500);
  letter-spacing: var(--ls-0-04em);
  color: var(--color-text-subtle);
  pointer-events: none;
  transition: opacity .4s var(--ease-out-expo);
}

.word-pick-hint.is-used {
  opacity: 0;
}

.word-pick-hint-mark {
  color: var(--color-accent);
  animation: hint-breathe 2.6s ease-in-out infinite;
}

@keyframes hint-breathe {

  0%,
  100% {
    opacity: .5;
    translate: 0 2px
  }

  50% {
    opacity: 1;
    translate: 0 -2px
  }
}

.footer-cta-wrapper {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-md);
}

.footer-cta-text {
  transition: opacity .22s var(--ease-out-expo);
}

.footer-cta.is-swapping .footer-cta-text {
  opacity: 0;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-lg);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border);
}

.footer-column {
  display: grid;
  gap: 8px;
  align-content: start
}

.footer-column .footer-col-title {
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-18em);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  font-weight: var(--fw-400);
  margin-bottom: 6px;
}

.footer-column a,
.footer-column .footer-col-desc {
  margin: 0;
  font-size: var(--text-16px);
  color: var(--color-text-muted);
  transition: color .25s;
}

.footer-column a {
  justify-self: start;
}

.footer-column a:hover {
  color: var(--color-text)
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-top: var(--space-3xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--text-14px);
  color: var(--color-text-subtle);
}

/* ══ REVEAL ═══════════════════════════════════ */
/* the slide rides `translate`, not `transform`. as a shorthand, `transform`
   wipes out whatever the element was already doing with it — which is how a
   revealed button lost its press. the two are separate properties, so both
   can run on their own timing */
.reveal {
  opacity: 0;
  translate: 0 26px;
  transition: opacity .85s var(--ease-out-expo), translate .85s var(--ease-out-expo);
}

.reveal.is-visible {
  opacity: 1;
  translate: none
}

/* .reveal declares a transition, and a transition list replaces rather than
   merges — so any component with one of its own loses it the moment it is
   also revealed. restate both here: the entry on `translate`, the ink and
   the press on their own */
.btn.reveal {
  transition: opacity .85s var(--ease-out-expo), translate .85s var(--ease-out-expo),
    background-size .45s var(--ease-ink), background-color .3s var(--ease-ink),
    color .3s var(--ease-ink), border-color .3s var(--ease-ink),
    transform .16s var(--ease-ink);
}

/* same fix as .btn.reveal above: without this, .reveal's own transition
   replaces the card's, so the hover lift's `transform` never actually
   transitions — it was jumping straight to translateY(-8px) instead of
   easing into it */
.service-card.reveal {
  transition: opacity .85s var(--ease-out-expo), translate .85s var(--ease-out-expo),
    transform .55s var(--ease-out-expo), border-color .4s;
}

/* ══ PAGE WIPE ════════════════════════════════ */
/* five panels that lift away on arrival and sweep back up on the way out,
   so one navigation reads as a single continuous upward motion.

   the lift is a plain CSS animation with no JS in the loop: if script.js
   never runs — error, blocked, still downloading — the panels still clear
   themselves and the page is never left stranded under a cover */
.page-wipe {
  position: fixed;
  inset: 0;
  /* over the nav and the floating CTA, under the custom cursor so the
     cursor is never the one thing hidden by its own page's chrome */
  z-index: 9999;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  pointer-events: none;
}

.page-wipe span {
  background: var(--color-espresso);
  transform-origin: top;
  /* `both` matters: during a delay the panel has to hold the from-state,
     otherwise it snaps to full height for a frame before its turn */
  animation: wipe-lift .62s var(--ease-out-expo) both;
  /* each panel scales on its own compositing layer, and adjacent layers
     can leave a hairline seam of the page showing through even though the
     grid tracks touch exactly — bleeding a panel a hair into the next
     column covers that seam with more panel instead */
  margin-right: -1px;
}

.page-wipe span:last-child {
  margin-right: 0;
}

/* the stagger is what makes it read as panels rather than one curtain */
.page-wipe span:nth-child(2) {
  animation-delay: .05s;
}

.page-wipe span:nth-child(3) {
  animation-delay: .1s;
}

.page-wipe span:nth-child(4) {
  animation-delay: .15s;
}

.page-wipe span:nth-child(5) {
  animation-delay: .2s;
}

@keyframes wipe-lift {
  from {
    transform: scaleY(1);
  }

  to {
    transform: scaleY(0);
  }
}

/* leaving: the same panels grow back, from the bottom edge this time */
.page-wipe.is-leaving span {
  transform-origin: bottom;
  animation: wipe-cover .42s var(--ease-out-expo) both;
}

/* re-stated because the shorthand above resets the delay for every panel */
.page-wipe.is-leaving span:nth-child(2) {
  animation-delay: .045s;
}

.page-wipe.is-leaving span:nth-child(3) {
  animation-delay: .09s;
}

.page-wipe.is-leaving span:nth-child(4) {
  animation-delay: .135s;
}

.page-wipe.is-leaving span:nth-child(5) {
  animation-delay: .18s;
}

@keyframes wipe-cover {
  from {
    transform: scaleY(0);
  }

  to {
    transform: scaleY(1);
  }
}

/* no motion means no cover at all — navigation stays instant */
@media (prefers-reduced-motion: reduce) {
  .page-wipe {
    display: none;
  }
}

/* ══ CURSOR ═══════════════════════════════════ */
/* mouse only. a coarse pointer has no cursor to replace, and `hover: hover`
   alone still catches stylus and some TV remotes — hence `pointer: fine` */
@media (hover: hover) and (pointer: fine) {

  /* .has-cursor is set by script.js once the dot and ring actually exist, so a
     script that never runs can never leave the page with no cursor at all */
  html.has-cursor,
  html.has-cursor * {
    /* !important because buttons, chips and the team-map silhouettes each set
       their own `cursor: pointer` at a higher specificity than this */
    cursor: none !important;
  }

  /* text entry keeps the system caret — losing the I-beam costs more than the
     effect is worth. the ring stands down over these instead */
  html.has-cursor :is(input, textarea, select, [contenteditable="true"]) {
    cursor: auto !important;
  }

  .cursor-dot,
  .cursor-ring,
  .cursor-hand {
    position: fixed;
    left: 0;
    top: 0;
    /* above every overlay on the page — including the lightbox veil — so the
       cursor is never the one thing left invisible on top of its own UI */
    z-index: 10010;
    pointer-events: none;
    border-radius: var(--radius-full);
    corner-shape: round;
    opacity: 0;
    transition: opacity .3s var(--ease-out-expo);
  }

  .cursor-dot {
    width: 6px;
    height: 6px;
    /* centred on the hotspot, so the offset lives here and `translate` stays
       free for the per-frame position */
    margin: -3px 0 0 -3px;
    background: var(--color-text);
    transition: opacity .3s var(--ease-out-expo), scale .3s var(--ease-out-expo);
  }

  .cursor-ring {
    width: 30px;
    height: 30px;
    margin: -15px 0 0 -15px;
    border: 1px solid color-mix(in srgb, var(--color-text) 45%, transparent);
    transition: opacity .3s var(--ease-out-expo), scale .35s var(--ease-out-expo),
      background-color .35s var(--ease-out-expo), border-color .35s var(--ease-out-expo);
  }

  /* stands in for the ring over anything draggable — a lightbox image once
     zoomed past 1x — since `cursor: grab` itself is defeated by the blanket
     `cursor: none !important` above. styled as the same kind of badge as the
     ring (border + tinted fill) so it reads as one cursor system, not a
     separate icon bolted on */
  .cursor-hand {
    width: 34px;
    height: 34px;
    margin: -17px 0 0 -17px;
    display: grid;
    place-items: center;
    border-radius: var(--radius-full);
    border: 1px solid var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 10%, transparent);
    color: var(--color-accent);
    scale: .7;
    transition: opacity .25s var(--ease-out-expo), scale .3s var(--ease-out-expo),
      background-color .3s var(--ease-out-expo);
  }

  .cursor-hand svg {
    width: 15px;
    height: 15px;
  }

  .is-awake .cursor-dot,
  .is-awake .cursor-ring {
    opacity: 1;
  }

  /* over anything clickable the ring opens and takes the accent; the dot gets
     out of the way so the target stays readable underneath */
  .is-hot .cursor-ring {
    scale: 1.7;
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
    border-color: var(--color-accent);
  }

  .is-hot .cursor-dot {
    scale: 0;
  }

  .is-pressed .cursor-ring {
    scale: .82;
  }

  .is-hot.is-pressed .cursor-ring {
    scale: 1.45;
  }

  /* over a text field the system caret is showing, so stand down entirely */
  .is-text .cursor-dot,
  .is-text .cursor-ring {
    opacity: 0;
  }

  /* set by the lightbox itself (not the pointermove hot/text detection above)
     since it already knows exactly when zoom and drag state change */
  .is-grabbable .cursor-dot,
  .is-grabbable .cursor-ring {
    opacity: 0;
  }

  .is-grabbable .cursor-hand,
  .is-grabbing .cursor-hand {
    opacity: 1;
    scale: 1;
  }

  /* the same badge tightens and deepens in tone on grip, echoing how the
     ring itself answers a press elsewhere in this cursor */
  .is-grabbing .cursor-hand {
    scale: .84;
    background: color-mix(in srgb, var(--color-accent) 24%, transparent);
  }
}

/* ══ CONTEXT MENU ═════════════════════════════ */
/* the native menu cannot be styled by a page, so this replaces it outright.
   shift + right-click still falls through to the browser's own menu, which is
   where Inspect, View Source and any extension items continue to live */
.ctx-menu {
  position: fixed;
  z-index: 9998;
  min-width: 216px;
  max-width: 280px;
  padding: 6px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xs);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .7) inset,
    0 22px 46px -18px rgba(var(--color-espresso-rgb), .42),
    0 3px 10px -4px rgba(var(--color-espresso-rgb), .16);
  /* opens from the pointer, so the origin is set inline as the menu is placed */
  scale: .96;
  opacity: 0;
  transition: opacity .16s var(--ease-out-expo), scale .16s var(--ease-out-expo);
}

.ctx-menu.is-open {
  scale: 1;
  opacity: 1;
}

.ctx-menu[hidden] {
  display: none;
}

/* the menu itself takes DOM focus so arrow keys reach its keydown handler,
   but that's a wiring detail, not a state the pointer-triggered open should
   announce with a ring around the whole box */
.ctx-menu:focus {
  outline: none;
}

.ctx-group+.ctx-group {
  margin-top: 5px;
  padding-top: 5px;
  border-top: 1px solid var(--color-border-subtle);
}

/* back / reload / forward ride together as one row of icons, the way a browser
   groups them, so the labelled list below stays about the page and its content */
.ctx-nav {
  display: flex;
  gap: 2px;
  margin-bottom: 5px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--color-border-subtle);
}

.ctx-nav-btn {
  display: grid;
  place-items: center;
  flex: 1;
  height: 34px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--color-text);
  transition: background-color .18s var(--ease-out-expo), color .18s var(--ease-out-expo);
}

.ctx-nav-btn:hover:not(:disabled),
.ctx-nav-btn:focus-visible:not(:disabled) {
  background: var(--color-fill);
  outline: none;
}

.ctx-nav-btn:active:not(:disabled) {
  background: var(--color-fill-strong);
}

/* the Navigation API can tell us there is nowhere to go — say so rather than
   offering a button that does nothing */
.ctx-nav-btn:disabled {
  color: var(--color-text-subtle);
  opacity: .4;
}

.ctx-nav-btn svg {
  display: block;
}

.ctx-item {
  display: flex;
  align-items: center;
  /* icon · label · shortcut — the label takes the slack so the hint stays right */
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  text-align: left;
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  color: var(--color-text);
  transition: background-color .18s var(--ease-out-expo), color .18s var(--ease-out-expo);
}

.ctx-item:hover,
.ctx-item:focus-visible {
  background: var(--color-fill);
  outline: none;
}

.ctx-item:active {
  background: var(--color-fill-strong);
}

.ctx-item-label {
  flex: 1;
}

/* the icons sit back a step so the labels still lead the eye, then come up to
   the accent on whichever row is actually under the pointer */
.ctx-item>svg {
  flex: none;
  color: var(--color-text-subtle);
  transition: color .18s var(--ease-out-expo);
}

.ctx-item:hover>svg,
.ctx-item:focus-visible>svg {
  color: var(--color-accent);
}

.ctx-hint {
  flex: none;
  font-size: var(--text-12px);
  letter-spacing: var(--ls-0-04em);
  color: var(--color-text-subtle);
}

/* ══ QR PANEL ═════════════════════════════════ */
.qr-veil {
  position: fixed;
  inset: 0;
  z-index: 9997;
  display: grid;
  place-items: center;
  padding: var(--space-lg);
  background: rgba(var(--color-espresso-rgb), .34);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity .22s var(--ease-out-expo);
}

.qr-veil.is-open {
  opacity: 1;
}

.qr-card {
  display: grid;
  justify-items: center;
  gap: var(--space-md);
  padding: var(--space-lg);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 32px 64px -24px rgba(var(--color-espresso-rgb), .5);
  scale: .97;
  transition: scale .22s var(--ease-out-expo);
}

.qr-veil.is-open .qr-card {
  scale: 1;
}

/* white behind the symbol regardless of the page's sand ground — scanners
   want the quiet zone to read as paper */
.qr-code {
  display: block;
  background: #fff;
  border-radius: var(--radius-xs);
}

.qr-url {
  margin: 0;
  max-width: 264px;
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  line-height: var(--lh-1-4);
  text-align: center;
  word-break: break-all;
  color: var(--color-text-muted);
}

.qr-close {
  padding: var(--btn-pad-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background-color: transparent;
  background-image: linear-gradient(var(--color-fill), var(--color-fill));
  background-repeat: no-repeat;
  background-position: 50% 100%;
  background-size: 100% 0;
  cursor: pointer;
  font-family: var(--font-ui);
  font-weight: var(--fw-600);
  font-size: var(--text-14px);
  color: var(--color-text);
  transition: background-size .45s var(--ease-ink), border-color .3s var(--ease-ink);
}

.qr-close:hover,
.qr-close:focus-visible {
  background-size: 100% 100%;
  border-color: var(--color-text-subtle);
}

/* ══ RESPONSIVE ═══════════════════════════════ */
@media (max-width: 1200px) {

  .service-cards {
    grid-template-columns: repeat(2, 1fr)
  }

  .toc {
    display: none
  }

  .steps {
    grid-template-columns: repeat(2, 1fr)
  }

  /* was --space-2xl: with .about-intro flattened below, this gap now
     lands between every pair of stacked items (title/copy/team/button),
     not just once between the old two columns, so it reads much taller
     than before unless it's brought down */
  .about-grid {
    grid-template-columns: 1fr;
    gap: var(--space-lg)
  }

  /* the button is nested inside .about-intro, ahead of .team in the DOM —
     display:contents drops the wrapper's own box so the title/copy/button
     join .team as direct siblings in the stack, and `order` alone can then
     push the button below the team grid instead of above it */
  .about-intro {
    display: contents;
  }

  /* the title's own margin-bottom would otherwise stack on top of the
     grid gap above, doubling that one gap versus the others */
  .about .section-title {
    margin-bottom: 0;
  }

  .about-more {
    order: 1;
    /* grid items stretch to fill the column by default; without this the
       button (now a direct item, not nested in a shrink-to-fit block)
       would span the full width instead of sizing to its own label */
    justify-self: start;
    /* same reasoning as the title above — the grid gap already provides
       the space before this */
    margin-top: 0;
  }

  .footer-grid {
    grid-template-columns: repeat(2, 1fr);
    row-gap: var(--space-xl)
  }

  .service-cards .service-card:last-child {
    grid-column: 1/-1
  }
}

@media (max-width: 1200px) {

  .nav-links,
  .nav-cta {
    display: none
  }

  .nav-toggle {
    display: flex
  }

  .nav-inner {
    padding: 8px 0
  }

  .nav-logo {
    margin-inline-end: auto
  }

  .grid-lines i:nth-child(n+3) {
    display: none
  }

  .hero-lede {
    gap: var(--space-lg)
  }

}

@media (max-width: 1200px) {

  .service-cards,
  .team,
  .steps {
    grid-template-columns: 1fr
  }

  /* stays pinned for as long as the section (title + the tall scrub zone
     below it) is being scrolled through — close above the card stack
     (which centers at 50vh - 215px) rather than parked up near the nav
     with a big gap between them. max() keeps it from sticking above the
     viewport entirely on short screens, where 50vh alone isn't enough */
  .services .section-title {
    position: sticky;
    top: max(24px, calc(50vh - 300px));
    z-index: 5;
  }

  /* below this width script.js drives `translate` on both of these every
     scroll frame (see the service-cards-pin scrub below). a lingering
     transition on that property — inherited from .reveal, or restated by
     .service-card.reveal above — would ease each frame's update rather than
     applying it immediately, so the title and the last card (or the cards
     and each other) would visibly lag apart instead of moving as one.
     dropping `translate` here costs the reveal's entrance slide — it still
     fades in, just without the small upward drift — which is a fair trade
     for the scrub itself tracking the scroll exactly */
  .services .section-title.reveal {
    transition: opacity .85s var(--ease-out-expo);
  }

  .service-card.reveal {
    transition: opacity .85s var(--ease-out-expo), transform .55s var(--ease-out-expo), border-color .4s;
  }

  /* the deck starts already stacked (each card peeking a little further
     back than the one in front) as soon as the section comes into view.
     script.js then reads scroll progress through this tall "scrub" zone
     and slides each card away in turn — card 1 first, then 2, then 3 —
     which is what actually reveals the one behind it. CSS sticky can fake
     part of this but its release timing isn't independently controllable
     per sibling, so the motion itself is driven from script.js; this block
     only sets up the pinned stage and the resting z-order/geometry */
  .service-cards {
    position: relative;
    /* the scrub distance — how much scrolling it takes to unstack all
       three cards */
    height: 260vh;
  }

  .service-cards-pin {
    display: block;
    position: sticky;
    /* pinned centered in the viewport, not tucked under the nav */
    top: calc(50vh - 215px);
    height: 430px;
  }

  .service-card {
    position: absolute;
    inset: 0 0 auto 0;
    width: 100%;
  }

  .service-card:nth-child(1) {
    z-index: 3;
  }

  .service-card:nth-child(2) {
    z-index: 2;
  }

  .service-card:nth-child(3) {
    z-index: 1;
  }

  /* How We Help, as a plain list: the same number/title/description
     treatment as the desktop four-column grid, just stacked in one column
     with a hairline rule between steps instead of the columns themselves.
     dividers only sit between steps, not around the list as a whole — the
     desktop grid's own top/bottom border is cancelled here rather than
     doubling up with the first and last step's own rule below */
  .steps {
    display: block;
    border: none;
  }

  .step {
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .step:last-child {
    border-bottom: none;
  }

  .team {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-sm)
  }

  .footer-grid {
    grid-template-columns: 1fr
  }

  .floating-cta {
    right: 16px;
    bottom: 16px
  }
}

/* script.js skips the scrub animation on reduced motion, so the pinned,
   overlapping layout above would otherwise just leave cards 2 and 3 hidden
   behind card 1 for a long, empty-feeling scroll — fall back to a plain
   stacked list instead */
@media (max-width: 1200px) and (prefers-reduced-motion:reduce) {

  .service-cards {
    height: auto;
  }

  .service-cards-pin {
    display: contents;
  }

  .service-card {
    position: relative;
    inset: auto;
  }

  .service-card+.service-card {
    margin-top: var(--space-md);
  }
}

@media (prefers-reduced-motion:reduce) {
  html {
    scroll-behavior: auto
  }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important
  }

  .reveal,
  .line-mask>span {
    opacity: 1;
    transform: none;
    translate: none
  }
}

/* ══ TYPE SCALE — breakpoint steps ════════════ */

@media (max-width: 1200px) {
  .hero-title {
    font-size: var(--text-96px);
  }

  .section-title {
    font-size: var(--text-88px);
  }

  .footer-headline {
    font-size: var(--text-80px);
  }

  .step-number {
    font-size: var(--text-48px);
  }

  /* 150% of the desktop size (34/18/16px) for every text piece in the
     card, not just the title — tablet's narrower column has fewer words
     per line, so scaling the whole block together keeps it reading as one
     piece of type rather than a big heading over small-print body text */
  .service-card .service-title {
    font-size: var(--text-48px);
  }

  .service-card>.service-desc {
    font-size: var(--text-28px);
  }

  .service-card li {
    font-size: var(--text-24px);
  }

  /* the bigger type above needs more room than the original card padding
     gives it, but the pinned stack is meant to stay close to its original
     footprint rather than grow with the text — so the spacing around it is
     trimmed back and the pin box only gets a modest allowance, not a
     proportional one */
  .service-cards-pin {
    height: 480px;
  }

  .service-card {
    padding: var(--space-lg) var(--space-lg) var(--space-md);
  }

  .service-card .service-title {
    margin: var(--space-md) 0 8px;
  }

  .service-card>.service-desc {
    margin: 0 0 var(--space-md);
  }

  .service-card ul {
    gap: 6px;
    padding-top: var(--space-sm);
  }

  .hero-tagline {
    font-size: var(--text-24px);
  }

  .project-meta .project-title {
    font-size: var(--text-20px);
  }
}

@media (max-width: 900px) {
  .hero-title {
    font-size: var(--text-72px);
  }

  .section-title {
    font-size: var(--text-64px);
  }

  .footer-headline {
    font-size: var(--text-64px);
  }

  .step-number {
    font-size: var(--text-34px);
  }

  .service-card .service-title {
    font-size: var(--text-24px);
  }

  /* the 1200px tier above scales these up for tablet's wider card; below
     900px reverts them back to the original desktop-base sizes rather than
     inheriting that tablet-only bump */
  .service-card>.service-desc {
    font-size: var(--text-18px);
  }

  .service-card li {
    font-size: var(--text-16px);
  }

  /* same reasoning as the font-size reverts above: the trimmed spacing and
     taller pin box exist only to fit the tablet-tier text increase, so
     both revert here rather than carrying down to the original phone
     layout, which already fit comfortably as it was */
  .service-cards-pin {
    height: 430px;
  }

  .service-card {
    padding: var(--space-xl) var(--space-lg) var(--space-lg);
  }

  .service-card .service-title {
    margin: var(--space-lg) 0 var(--space-sm);
  }

  .service-card>.service-desc {
    margin: 0 0 var(--space-lg);
  }

  .service-card ul {
    gap: 9px;
    padding-top: var(--space-md);
  }

  .hero-tagline {
    font-size: var(--text-22px);
  }
}

@media (max-width: 600px) {
  body {
    font-size: var(--text-16px);
  }

  .hero-title {
    font-size: var(--text-56px);
  }

  .section-title {
    font-size: var(--text-48px);
  }

  .footer-headline {
    font-size: var(--text-48px);
  }
}

/* ══ SHARED: slim footer bar ═════════════════ */
/* ══ FOOTER BAR ═══════════════════════════════ */
.footer-bar {
  padding-block: var(--space-md);
  background: var(--color-fill);
  font-size: var(--text-14px);
  color: var(--color-text-muted);
}

.footer-bar-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.footer-bar-links {
  display: flex;
  gap: var(--space-md);
}

.footer-bar-links a {
  transition: color .25s var(--ease-out-expo);
}

.footer-bar-links a:hover {
  color: var(--color-accent);
}

/* ══ SHARED: back link under the logo ════════ */
.nav-back {
  display: flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  margin-top: 2px;
  padding-inline: var(--container-padding);
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  letter-spacing: var(--ls-0-08em);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  transition: color .3s var(--ease-out-expo);
}

.nav-back:hover {
  color: var(--color-accent);
}

.nav-back-arrow {
  transition: translate .3s var(--ease-out-expo);
}

.nav-back:hover .nav-back-arrow {
  translate: -3px 0;
}

@media (min-width: 1560px) {
  .nav-back {
    width: calc((100% - var(--container-max)) / 2 + var(--container-padding));
    padding-inline: 0;
    justify-content: center;
  }
}

/* ══ SHARED: newsletter band ═════════════════ */
.newsletter {
  padding-block: clamp(64px, 8vw, var(--space-3xl));
  background: var(--color-bg-subtle);
  border-top: 1px solid var(--color-border-subtle);
}

.newsletter-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.newsletter-eyebrow {
  margin-bottom: var(--space-sm);
  color: var(--color-accent);
  font-weight: var(--fw-600);
}

.newsletter-title {
  font-size: var(--text-34px);
  line-height: var(--lh-1-16);
  max-width: 16ch;
  margin-bottom: var(--space-md);
}

.newsletter-desc {
  margin: 0 0 var(--space-lg);
  max-width: 56ch;
  font-size: var(--text-18px);
  color: var(--color-text-muted);
}

.newsletter-form {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
}

.newsletter-input {
  min-width: min(300px, 76vw);
  padding: 13px 18px;
  font-family: var(--font-sans);
  font-size: var(--text-14px);
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xs);
  corner-shape: round;
  transition: border-color .3s var(--ease-out-expo);
}

/* the confirmation only exists once there is something to confirm, so it is
   out of flow until then — an empty live region would otherwise reserve a
   line of space under every newsletter form on the site */
.newsletter-status {
  display: none;
  margin: var(--space-sm) 0 0;
  max-width: 46ch;
  font-family: var(--font-ui);
  font-size: var(--text-14px);
  line-height: var(--lh-1-6);
  color: var(--color-text-muted);
}

.newsletter-status.is-shown {
  display: block;
  animation: newsletter-status-in .45s var(--ease-out-expo) both;
}

@keyframes newsletter-status-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .newsletter-status.is-shown {
    animation: none;
  }
}

.newsletter-input::placeholder {
  color: var(--color-text-subtle);
}

.newsletter-input:focus-visible {
  border-color: var(--color-accent);
  outline-color: var(--color-accent);
}

/* ══ CUSTOM SMOOTH SCROLL ═════════════════════ */
html.has-smooth-scroll {
  scroll-behavior: auto !important;
}