/* ==========================================================================
   DaveDoesDesigns — Design tokens (from Figma Color collection)
   ========================================================================== */
:root {
  /* Color/bg */
  --bg-default: #0e011c;
  --bg-surface: #1e0c32;
  --bg-surface-2: #3d3350;

  /* Color/fg */
  --fg-default: #ffffff;
  --fg-on-light: #1e0c32;
  --fg-subtle: #978aaa;
  --fg-muted: #b3a0c8;

  /* Color/primary */
  --primary-default: #8200ff;
  --primary-contrast: #ffffff;

  /* Accents */
  --amber: #ffc20a;
  --amber-700: #a87a00;
  --rose: #f43f5e;
  --neutral-200: #ded7e6;
  --violet-100: #e7d2ff; /* Figma violet/100 */
  --violet-200: #cfa6ff; /* Figma violet/200: hero eyebrow + body lead sentence (2026-09-06) */
  --violet-300: #b679ff; /* Figma Color collection: stat numerals (712:2857) */
  --violet-50: #f4e9ff; /* Figma violet/50: Impact chip fill (light) */
  --menu-hover-bg: color-mix(in srgb, var(--accent) 14%, transparent); /* nav row hover tint: the accent (amber here) at a whisper (Dave 2026-09-07) */
  --violet-600: #6a00d1; /* Figma violet/600 */
  --violet-700: #52009e; /* Figma violet/700: Impact chip text (light) */
  --violet-800: #3a006e; /* Figma violet/800: Impact chip fill (dark) */
  --neutral-700: #3d3350;
  --neutral-900: #1e0c32; /* Figma neutral/900: the header Menu button (Dave 2026-09-07); same in both themes */
  --neutral-100: #edeaf1; /* Figma neutral/100: the Menu button's fill over light backdrops (Dave 2026-09-07) */
  --neutral-300: #c7bcd4; /* Figma neutral/300: its hover / focus fill */

  /* Color/hero (gradient overlay endpoints) */
  --hero-from: rgba(14, 1, 28, 0.45); /* was 0.6 (Dave 2026-09-07: more of the still through the nav scrim; the header buttons carry their own frosted fill) */
  --hero-to: rgba(14, 1, 28, 0);

  /* Theme-dependent chrome */
  --divider: rgba(151, 138, 170, 0.25);
  --divider-strong: rgba(151, 138, 170, 0.35);
  --ghost-border: rgba(255, 255, 255, 0.5);
  --input-bg: rgba(0, 0, 0, 0.2);

  /* Accent: amber in dark mode, primary purple in light mode */
  --accent: var(--amber);
  --accent-contrast: #1e0c32;
  --stat-icon: var(--amber-700);
  --tooltip-bg: rgba(255, 194, 10, 0.75);

  /* Type */
  --font-display: 'Gloock', serif;
  --font-body: 'Hanken Grotesk', sans-serif;

  /* Motion */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --cascade-duration: 600ms;
  --cascade-step: 200ms;

  --nav-height: 96px;
  --content-max: 1200px;
  /* Space between the hero's two CTAs (Dave 2026-09-07). The header pair
     sits in its own frame at an 8px measure, see .site-nav--menu. */
  --button-gap: 16px;
}

/* --- Light mode (values from the Figma Desktop - Light frame) ------------- */
:root[data-theme='light'] {
  --menu-hover-bg: var(--violet-50); /* nav row hover tint: the lightest violet step (Dave 2026-09-07) */
  --bg-default: #fbfafd;
  --bg-surface: #ffffff;
  --bg-surface-2: #f3f0f8;

  --fg-default: #1e0c32;
  --fg-muted: #5e5072;

  --primary-default: #6a00d1;

  --neutral-200: #3d3350;
  --neutral-700: #e9e5f0;

  --divider: rgba(30, 12, 50, 0.12);
  --divider-strong: rgba(30, 12, 50, 0.2);
  --ghost-border: rgba(30, 12, 50, 0.4);
  --input-bg: rgba(30, 12, 50, 0.04);

  /* Light mode: everything amber becomes primary purple */
  --accent: #8200ff;
  --accent-contrast: #ffffff;
  --stat-icon: #978aaa; /* solid muted purple (Figma light stat icons) */
  --tooltip-bg: rgba(130, 0, 255, 0.75);
}

/* --- Light-mode content card: Impact onwards sits in a white rounded
   shell spanning the full viewport width; invisible in dark mode -------- */
.content-shell {
  background: var(--bg-default);
}

:root[data-theme='light'] .content-shell {
  margin: 0 0 16px;
  border-radius: 30px;
  background: var(--bg-default);
  box-shadow: 0 24px 48px rgba(30, 12, 50, 0.18);
}

/* Light mode frame: the footer's near-black purple fills the gaps between
   the card edges, the footer, and the viewport so the rounded transition
   into the footer reads as one continuous surface */
html[data-theme='light'],
html[data-theme='light'] body {
  background: #0e011c;
}

/* Section grounds down the homepage (Dave 2026-09-06): Results / Select
   Work / About + Skills / Contact sit on the default ground; Case
   Studies and the In Their Words + In Good Company pair sit on
   surface-2 (the pair reads as one validation band), in both themes.
   The light-mode shell clips so a surface-2 band can never spill past
   its rounded corners (clip, not hidden: the sticky case cards keep
   working). */
.case-studies,
.quotes,
.trusted { background: var(--band-bg); }

/* The band ground: surface-2 pulled 60% of the way back toward the
   default, so the step reads as a tint rather than a block (Dave
   2026-09-06: "slightly lighter, close to the default"). Resolves per
   theme because both tokens do. */
:root { --band-bg: color-mix(in srgb, var(--bg-surface-2) 40%, var(--bg-default)); }

:root[data-theme='light'] .content-shell { overflow: clip; }

/* Contrast fix for cards that would blend into the white shell */
:root[data-theme='light'] .cs-card { box-shadow: 0 16px 32px rgba(30, 12, 50, 0.08); }

/* Impact in light mode (Figma light frame): white result cards with the
   border token, dark plum metric, light-primary purple CTA (violet-300 is
   2.3:1 on white), muted subheading */
:root[data-theme='light'] .impact {
  --impact-purple: var(--primary-default);
  --impact-metric: var(--fg-on-light);
  --impact-context: var(--fg-muted); /* body copy (Dave 2026-09-06), 7.2:1 on white */
  --impact-source: color-mix(in srgb, var(--fg-muted) 50%, transparent); /* subtle footnote (Dave 2026-09-06), ~2.3:1 on the page */
  --impact-chip-bg: var(--violet-50);
  --impact-chip-fg: var(--violet-700); /* 8.9:1 on violet-50 */
}
:root[data-theme='light'] .impact-card { background: var(--bg-surface); }
:root[data-theme='light'] .impact__sub { color: var(--fg-muted); }

/* Brief cross-fade while the theme switches (class added by JS) */
html.theme-fade,
html.theme-fade body,
html.theme-fade *:not(video):not(img) {
  transition: background-color 300ms ease, color 300ms ease,
    border-color 300ms ease, fill 300ms ease !important;
}

/* ==========================================================================
   Page transitions — a fixed veil over everything fades internal
   navigations out and page loads in (driven by main.js). --bg-default
   makes it white-ish in light mode and near-black in dark mode.
   ========================================================================== */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: var(--bg-default);
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms ease;
}

/* Pre-paint state from the inline head script: the page arrives fully
   veiled; main.js drops the class after first paint to fade in */
html.page-enter body::after {
  opacity: 1;
  transition: none;
}

/* Exit: set on internal link clicks; swallows input while fading */
html.page-leave body::after {
  opacity: 1;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  body::after { display: none; }
}

/* ==========================================================================
   Base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* Anchor scrolling is handled by an eased rAF scroller in main.js
   (no CSS scroll-behavior, which would fight it) */

/* All font sizes are in rem, so scaling the root bumps every text size
   proportionally on smaller screens — friendlier reading on tablet/mobile
   without touching px-based layout (paddings, heights, gaps stay put) */
html { background: var(--bg-default); font-size: 16px; }

@media (max-width: 1024px) {
  html { font-size: 17px; } /* tablet: +6.25% */
}

@media (max-width: 640px) {
  html { font-size: 17.5px; } /* mobile: +9.4% */
}

body {
  margin: 0;
  background: var(--bg-default);
  color: var(--fg-default);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
}

body.menu-open { overflow: hidden; }

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

/* Every interactive element gets a pointer: nav links, CTAs, cards,
   tiles, social links, copy-email, hamburger, resume download.
   No mobile tap-highlight flash on any of them. */
a, button {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* Native hash jumps mirror the JS scroller: 96px landing offset
   (Case Studies / Select Work land 24px deeper — no previous-section peek) */
section[id], footer[id] { scroll-margin-top: 96px; }
#case-studies, #select-work { scroll-margin-top: 72px; }

/* ==========================================================================
   Sticky navigation
   ========================================================================== */
.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* viewport-fit=cover pages (the homepage): the nav starts below the
     status bar / Dynamic Island; its scrims still paint behind it.
     env() is 0 everywhere else, so this is inert on other pages. */
  padding-top: env(safe-area-inset-top, 0px);
  z-index: 100;
  opacity: 0;
  transform: translateY(-12px);
  transition: opacity 400ms ease-out, transform 400ms ease-out;
  pointer-events: none;
}

/* The top scrims are gone (Dave 2026-09-07): the nav sits bare over the
   hero and the buttons carry their own treatment. */

/* Fade in from top on page load, fade back in on scroll up */
.site-nav.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* After the load animation, scroll show/hide runs at 600ms both ways */
.site-nav.is-loaded {
  transition: opacity 600ms ease-out, transform 600ms ease-out;
}

/* Scroll down: slide up and out while fading (reverse of the load animation).
   Removing this class slides it back down from the top while fading in. */
.site-nav.is-hidden {
  opacity: 0;
  transform: translateY(-100%);
  pointer-events: none;
}

.site-nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  height: var(--nav-height);
  padding: 0 48px;
}

/* Bare gradient D in its own frosted tile — the glass adapts to the
   backdrop (via .on-light below), so one mark serves both themes. Squared
   off at 16px against the pill-shaped capsules, and flex centering keeps
   the D on the same midline as the links. */
.site-nav__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 68px;
  height: 68px;
  flex-shrink: 0;
  border-radius: 16px;
  background: rgba(30, 12, 50, 0.85); /* plum frost over dark backdrops; .on-light swaps in the white frost (Dave 2026-09-07) */
  border: 1px solid rgba(255, 255, 255, 0.12);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  transition: background 400ms ease, border-color 400ms ease,
    backdrop-filter 400ms ease, -webkit-backdrop-filter 400ms ease;
}

.site-nav__logo img {
  display: block;
  height: 38px;
  width: auto;
  transition: height 400ms ease, filter 400ms ease;
}

/* Logo far left, links centered, Contact far right */
/* Frosted glass capsules: a 999px radius (greater than the height, so it
   renders as a full pill) plus backdrop blur keeps the text legible over
   busy backdrops. The links group hugs its content — the inner flexbox's
   space-between splits the leftover space evenly, so it stays centered
   between the logo and the actions cluster. */
.site-nav__links,
.site-nav__actions {
  border-radius: 999px;
  background: rgba(30, 12, 50, 0.35);
  border: 1px solid rgba(255, 255, 255, 0.12);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  transition: background 400ms ease, border-color 400ms ease,
    backdrop-filter 400ms ease, -webkit-backdrop-filter 400ms ease;
}

/* Frosted white over light sections — same signal as the link colors */
.site-nav.on-light .site-nav__logo,
.site-nav.on-light .site-nav__links,
.site-nav.on-light .site-nav__actions,
.site-nav.on-light .nav-toggle {
  background: rgba(255, 255, 255, 0.55);
  border-color: rgba(30, 12, 50, 0.1);
}

/* The logo tile goes to a 65% white frost over light sections */
.site-nav.on-light .site-nav__logo { background: rgba(255, 255, 255, 0.65); }

/* Over a hero marked data-nav-bare (the homepage, the three case studies
   and the LWF page; main.js toggles .is-over-hero and each header bakes
   it in for the first frame): the mark rides bare, stepped up to 46px as
   it originally did, while the Contact / Menu frame stays. Below the hero
   the tile returns and adapts (Dave 2026-09-07). */
.site-nav.is-over-hero .site-nav__logo {
  background: transparent;
  border-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

.site-nav.is-over-hero .site-nav__logo img { height: 46px; }

/* The mark keeps its full-colour gradient in every state (Dave 2026-09-07);
   the tile's glass flips for contrast, the mark itself does not. */
.site-nav__logo img { filter: none; }

.site-nav__links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 44px;
  height: 56px;
  padding: 0 36px;
}

/* Theme toggle + Contact share one capsule */
.site-nav__actions {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 5px 5px 9px; /* 44px controls + 10px + 2px border = the links capsule's 56px */
  flex-shrink: 0;
}

/* --- Nav link with mask-reveal hover ------------------------------------ */
/* Nav color follows what's BEHIND the nav (hero video is always dark),
   not the global theme — .on-light flips it over light sections */
.nav-link {
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  color: #ffffff;
  text-decoration: none;
  transition: color 300ms ease;
}

.nav-link__mask {
  display: inline-flex;
  flex-direction: column;
  height: 1.4em;
  overflow: hidden;
}

.nav-link__text {
  display: block;
  transition: transform 350ms var(--ease-out);
}

/* Current text slides up and out; duplicate slides up from below */
.nav-link:hover .nav-link__text,
.nav-link:focus-visible .nav-link__text {
  transform: translateY(-100%);
}

.nav-link__text--hover {
  color: var(--amber);
}

/* Contact styled as an outlined button, per the design */
.nav-link--contact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  padding: 0 24px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 999px; /* capsule, nests concentric inside the actions pill */
  flex-shrink: 0;
  transition: border-color 300ms ease, color 300ms ease;
}

/* Button-style hover: text slides up like the other links (revealed copy
   goes amber like the rest of the nav), border brightens */
.nav-link--contact:hover { border-color: var(--fg-default); }

/* --- Active section link ------------------------------------------------- */
/* The link for the section currently in view (clicked or scrolled to)
   holds purple; Home holds amber. Placed after the .on-light overrides
   below would tie on specificity, so both backdrops are covered here. */
.site-nav .nav-link.is-active,
.site-nav.on-light .nav-link.is-active { color: var(--primary-default); }
.site-nav .nav-link.is-active[href='#hero'],
.site-nav.on-light .nav-link.is-active[href='#hero'] { color: var(--amber); }
.site-nav .nav-link--contact.is-active,
.site-nav.on-light .nav-link--contact.is-active { border-color: var(--primary-default); }

/* --- Nav on light backgrounds: dark plum text --------------------------- */
.site-nav.on-light .nav-link { color: var(--fg-on-light); }
.site-nav.on-light .nav-link__text--hover { color: var(--accent); }
.site-nav.on-light .nav-link--contact { border-color: rgba(30, 12, 50, 0.5); }
.site-nav.on-light .nav-toggle__bar { background: var(--fg-on-light); }
.site-nav--menu.on-light .nav-toggle__bar { background: var(--fg-on-light); } /* the white Menu button carries dark bars over light backdrops */

/* Open mobile menu in light mode has a white panel behind the X — the
   theme toggle beside it needs the same flip or it goes white-on-white */
:root[data-theme='light'] .nav-toggle.is-open .nav-toggle__bar {
  background: var(--fg-on-light);
}

/* The homepage X is a ghost button over the panel: dark bars on the white
   light-mode panel, white on the dark one */
:root[data-theme='light'] .site-nav--menu .nav-toggle.is-open .nav-toggle__bar { background: var(--fg-on-light); }

:root[data-theme='light'] body.menu-open .theme-toggle {
  color: var(--fg-on-light);
}

/* --- Theme toggle -------------------------------------------------------- */
/* Sits beside Contact on desktop; when the links and Contact drop out at
   1024 it lands beside the hamburger, and the icon grows to match its 26px */
.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #ffffff; /* over dark backgrounds; .on-light flips it */
  cursor: pointer;
  flex-shrink: 0;
  transition: color 300ms ease, transform 200ms var(--ease-out);
}

.site-nav.on-light .theme-toggle { color: var(--fg-on-light); }

.theme-toggle:hover,
.theme-toggle:focus-visible {
  color: var(--amber);
  transform: scale(1.08);
}

.site-nav.on-light .theme-toggle:hover,
.site-nav.on-light .theme-toggle:focus-visible { color: var(--primary-default); }

.theme-toggle__icon { width: 22px; height: 22px; }

/* Show the icon of the mode you would switch TO */
.theme-toggle__icon--sun { display: none; }
:root[data-theme='dark'] .theme-toggle__icon--sun { display: block; }
:root[data-theme='dark'] .theme-toggle__icon--moon { display: none; }

/* Tooltip hangs below the button, since the nav is pinned to the top */
.theme-toggle[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% - 2px);
  right: 0;
  padding: 7px 10px;
  border-radius: 6px;
  background: #0e011c;
  color: #ffffff;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  box-shadow: 0 4px 12px rgba(14, 1, 28, 0.35);
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .theme-toggle:hover::after,
  .theme-toggle:focus-visible::after {
    opacity: 1;
    transform: translateY(0);
    transition-delay: 300ms; /* same dwell as the gallery chips */
  }
}

/* --- Hamburger toggle ---------------------------------------------------- */
/* Frosted glass disc matching the nav capsules (mobile only — display:none
   here, flex at the 1024 breakpoint) */
.nav-toggle {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 999px;
  background: rgba(30, 12, 50, 0.35);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  cursor: pointer;
  flex-shrink: 0;
  transition: background 400ms ease, border-color 400ms ease,
    backdrop-filter 400ms ease, -webkit-backdrop-filter 400ms ease;
}

/* No color shifts on tap/focus/hover — only the hamburger <-> X motion */
.nav-toggle:hover,
.nav-toggle:focus,
.nav-toggle:active {
  outline: none;
}

.nav-toggle__bar {
  display: block;
  width: 26px;
  height: 2px;
  border-radius: 2px;
  background: #ffffff; /* over dark backgrounds; .on-light flips it */
  /* Closed-state transition = the CLOSING leg: un-rotate the X first,
     then glide the bars back apart (middle bar fades in at the end) */
  transition: rotate 200ms var(--ease-out), translate 200ms var(--ease-out) 160ms,
    opacity 160ms ease 200ms, background-color 300ms ease;
}

/* Hamburger -> X in two beats: outer bars glide to center, THEN rotate.
   translate/rotate are separate properties (not one transform) so each leg
   runs on its own clock — a combined transform swings the bars on a curve. */
.nav-toggle.is-open .nav-toggle__bar {
  transition: translate 200ms var(--ease-out), rotate 200ms var(--ease-out) 160ms,
    opacity 160ms ease, background-color 300ms ease;
}

.nav-toggle.is-open .nav-toggle__bar:nth-child(1) { translate: 0 9px; rotate: 45deg; }
.nav-toggle.is-open .nav-toggle__bar:nth-child(2) { opacity: 0; }
.nav-toggle.is-open .nav-toggle__bar:nth-child(3) { translate: 0 -9px; rotate: -45deg; }

/* --- Homepage header: logo, Contact, Menu (Figma 801:716, Dave 2026-09-07)
   .site-nav--menu scopes the layout to the homepage; the inner pages keep
   the links + theme toggle header above. Two 44px buttons, 8px radius,
   16px apart (--button-gap), 17px semibold white, 16px side padding, each
   as wide as its label. Contact: 10% plum glass, 50% white stroke, 8px
   blur. Menu: solid neutral-900 (Dave 2026-09-07, was the frame's
   violet-700), hamburger before the label. Phones drop
   Contact and the label and keep the violet icon button (the <=640
   block). Gutters tighten so the logo and the pair sit nearer the edges. */
.site-nav--menu .site-nav__inner { padding: 0 24px; }

/* The pair's frame: the logo tile's frosted glass (same fill, stroke,
   blur and a concentric 16px radius). One 8px measure everywhere inside
   it: between the two buttons and on all four sides (Dave 2026-09-07:
   "equally padded"; this pair no longer shares --button-gap with the hero
   CTAs). The frame is on everywhere, heroes included (Dave 2026-09-07:
   one nav system); only its tint, stroke and foreground flip with the
   backdrop via .on-light. The padding is pulled back with a negative
   margin so the buttons sit where they did without it. */
.site-nav--menu .site-nav__actions,
.site-nav--menu.on-light .site-nav__actions {
  gap: 8px;
  margin: -8px;
  padding: 8px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 16px;
  background: rgba(30, 12, 50, 0.35);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
}

.site-nav--menu.on-light .site-nav__actions {
  background: rgba(255, 255, 255, 0.55);
  border-color: rgba(30, 12, 50, 0.1);
}

body.menu-open .site-nav--menu .site-nav__actions {
  border-color: transparent;
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

.site-nav--menu .nav-link--contact,
.site-nav--menu .nav-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 8px;
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  color: #ffffff;
  transition: border-color 300ms ease, color 300ms ease, background 300ms ease,
    padding 250ms var(--ease-out), gap 250ms var(--ease-out), transform 250ms var(--ease-out);
}

.site-nav--menu .nav-link--contact {
  border: 1px solid rgba(255, 255, 255, 0.3); /* a quiet stroke (Dave 2026-09-07; was 0.5), full white on hover */
  background: transparent; /* the frame carries the glass (Dave 2026-09-07) */
}

.site-nav--menu .nav-toggle {
  flex-direction: row;
  gap: 12px;
  width: auto;
  border: 1px solid rgba(255, 255, 255, 0.3); /* the same stroke as Contact over dark backdrops (Dave 2026-09-07) */
  background: var(--neutral-900);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* Hover: labels roll up like the nav links (same mask markup) and the
   label + bars change colour; the stroke and fill stay EXACTLY as at rest
   in every state (Dave 2026-09-07). These rules pin the rest values so the
   generic .nav-link--contact:hover stroke brighten never applies here. */
.site-nav--menu .nav-link--contact:hover { border-color: rgba(255, 255, 255, 0.3); }

.site-nav--menu .nav-toggle:hover,
.site-nav--menu .nav-toggle:focus-visible {
  border-color: rgba(255, 255, 255, 0.3);
  background: var(--neutral-900);
}

.site-nav--menu .nav-toggle .nav-link__text--hover { color: var(--amber); }

/* The hamburger bars go amber with the label (Dave 2026-09-07); the X in
   the open state keeps its own colour */
.site-nav--menu .nav-toggle:hover .nav-toggle__bar,
.site-nav--menu .nav-toggle:focus-visible .nav-toggle__bar { background: var(--amber); }

/* Over light backdrops the rollover matches Contact's: label and bars in
   the same purple Contact's text rolls to (Dave 2026-09-07) */
.site-nav--menu.on-light .nav-toggle .nav-link__text--hover { color: var(--accent); }
.site-nav--menu.on-light .nav-toggle:hover .nav-toggle__bar,
.site-nav--menu.on-light .nav-toggle:focus-visible .nav-toggle__bar { background: var(--accent); }

.site-nav--menu .nav-toggle.is-open:hover .nav-toggle__bar,
.site-nav--menu .nav-toggle.is-open:focus-visible .nav-toggle__bar { background: #ffffff; }

:root[data-theme='light'] .site-nav--menu .nav-toggle.is-open:hover .nav-toggle__bar,
:root[data-theme='light'] .site-nav--menu .nav-toggle.is-open:focus-visible .nav-toggle__bar { background: var(--fg-on-light); }

.site-nav--menu .nav-toggle:hover .nav-link__text,
.site-nav--menu .nav-toggle:focus-visible .nav-link__text { transform: translateY(-100%); }

.site-nav--menu .nav-link--contact:focus-visible,
.site-nav--menu .nav-toggle:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
}

/* Must follow the .on-light block above (same specificity, source order).
   Over light backdrops the pair flips together (Dave 2026-09-07): Menu
   becomes a white button with the dark plum label and bars, Contact reads
   dark. Over dark backdrops Menu keeps its neutral-900 fill and white. */
.site-nav--menu.on-light .nav-toggle {
  border: 1px solid rgba(30, 12, 50, 0.25); /* the same stroke as Contact over light backdrops (Dave 2026-09-07) */
  background: var(--neutral-100); /* neutral/100 fill so the tile reads against white sections (Dave 2026-09-07: 300 was too heavy; was #ffffff) */
  color: var(--fg-on-light);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.site-nav--menu.on-light .nav-toggle:hover,
.site-nav--menu.on-light .nav-toggle:focus-visible {
  border-color: rgba(30, 12, 50, 0.25);
  background: var(--neutral-100); /* unchanged on hover (Dave 2026-09-07) */
}

.site-nav--menu.on-light .nav-link--contact {
  border-color: rgba(30, 12, 50, 0.25); /* quiet over light backdrops too (was 0.5) */
  color: var(--fg-on-light);
}

.site-nav--menu.on-light .nav-link--contact:hover { border-color: rgba(30, 12, 50, 0.25); } /* stroke stays put on hover (Dave 2026-09-07) */

/* Hamburger beside the label: 18px bars, 4px apart (so the open-state X
   below folds the outer bars 6px to the centre, not the 9px of the 26px
   disc version) */
.nav-toggle__bars {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.site-nav--menu .nav-toggle__bar { width: 18px; }

.site-nav--menu .nav-toggle.is-open .nav-toggle__bar:nth-child(1) { translate: 0 6px; }
.site-nav--menu .nav-toggle.is-open .nav-toggle__bar:nth-child(3) { translate: 0 -6px; }

/* The label collapses (max-width, not display) so it can tween away */
.nav-toggle__label {
  display: block;
  max-width: 80px;
  overflow: hidden;
  transition: max-width 250ms var(--ease-out), opacity 150ms ease;
}

/* Panel open (Dave 2026-09-07): Contact fades and is hidden (it lives in
   the panel meanwhile), Menu slides left to the panel's padding edge
   (main.js measures the distance into --nav-shift) and its label folds
   away so only the bars remain, already crossed into the X by the rules
   above.
   Every step is a transition, so closing plays the same thing backwards;
   the reduced-motion block at the end of the file zeroes them. */
body.menu-open .site-nav--menu .nav-link--contact {
  opacity: 0;
  visibility: hidden;
  transition: opacity 150ms ease, visibility 0s linear 150ms;
}

body.menu-open .site-nav--menu .nav-toggle {
  gap: 0;
  padding: 0 12px; /* 18px bars + 24 + 2px stroke = the 44px square */
  border: 1px solid rgba(255, 255, 255, 0.3); /* ghost X, no fill, a light stroke (Dave 2026-09-07) */
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transform: translateX(calc(-1 * var(--nav-shift, 0px)));
}

body.menu-open .site-nav--menu .nav-toggle:hover,
body.menu-open .site-nav--menu .nav-toggle:focus-visible {
  border-color: rgba(255, 255, 255, 0.3);
  background: transparent;
}

/* Over the white light-mode panel the ghost X goes dark */
:root[data-theme='light'] body.menu-open .site-nav--menu .nav-toggle {
  border-color: rgba(30, 12, 50, 0.25);
  color: var(--fg-on-light);
}

:root[data-theme='light'] body.menu-open .site-nav--menu .nav-toggle:hover,
:root[data-theme='light'] body.menu-open .site-nav--menu .nav-toggle:focus-visible { border-color: rgba(30, 12, 50, 0.25); }

body.menu-open .site-nav--menu .nav-toggle__label {
  max-width: 0;
  opacity: 0;
}

/* ==========================================================================
   Mobile menu: full-screen plum, slides down from top (inner pages);
   the homepage adds .mobile-menu--panel below and slides in from the right
   ========================================================================== */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--bg-surface);
  transform: translateY(-100%);
  visibility: hidden;
  transition: transform 450ms var(--ease-out), visibility 0s linear 450ms;
  display: flex;
  /* The list scrolls if an expanded sub-list outgrows a short phone; the
     nav's auto margins keep it centred while it fits */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Bleed the panel past the viewport top/bottom so iOS Safari's translucent
   status bar and URL bar sample the panel, not the page behind it. Rides
   inside the panel (moves with the slide), behind its own background.
   main.js pairs this with a theme-color swap for the solid chrome tint. */
.mobile-menu::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -120px;
  bottom: -120px;
  z-index: -1;
  background: var(--bg-surface);
}

.mobile-menu.is-open {
  transform: translateY(0);
  visibility: visible;
  transition: transform 450ms var(--ease-out), visibility 0s;
}

.mobile-menu__nav {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  width: 100%;
  margin: auto;
  padding: 112px 24px 48px;
}

/* One entry per item: a plain link, or a row (link + chevron) with a
   collapsible sub-list under it. The item carries the cascade. */
.mobile-menu__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 400ms ease-out, transform 400ms var(--ease-out);
}

/* Cascade in, staggered top to bottom */
.mobile-menu.is-open .mobile-menu__item {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(150ms + var(--i) * 70ms);
}

.mobile-menu__link {
  font-family: var(--font-display);
  font-size: 2.125rem;
  letter-spacing: 0.8px;
  color: var(--fg-default);
  text-decoration: none;
  transition: color 200ms ease;
}

.mobile-menu__link:hover { color: var(--accent); }

/* Row: the label stays centred like its siblings; the chevron hangs off
   its right edge as a 44px tap target */
.mobile-menu__row {
  position: relative;
  display: flex;
  align-items: center;
}

/* Accordion rows are BUTTONS (Dave 2026-09-07): the whole row, label
   and chevron, toggles the sub-list in place and never navigates. Reset
   the button to read exactly like the plain links beside it. */
button.mobile-menu__row {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

button.mobile-menu__row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 4px;
}

/* The chevron is the row's affordance, not a control of its own: it
   turns with the row's aria-expanded */
.mobile-menu__row[aria-expanded='true'] .mobile-menu__expand { color: var(--accent); }
.mobile-menu__row[aria-expanded='true'] .mobile-menu__chevron { transform: rotate(180deg); }

.mobile-menu__expand {
  position: absolute;
  left: 100%;
  top: 50%;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 2px;
  padding: 0;
  border: 0;
  border-radius: 22px;
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
  transition: color 200ms ease;
}

.mobile-menu__row:hover .mobile-menu__expand { color: var(--accent); }

.mobile-menu__chevron { transition: transform 300ms var(--ease-out); }

/* Sub-list: a 0fr -> 1fr grid row so it opens and closes with a height
   tween; inert while closed so nothing inside can take focus */
.mobile-menu__sub {
  display: grid;
  grid-template-rows: 0fr;
  width: 100%;
  transition: grid-template-rows 320ms var(--ease-out);
}

.mobile-menu__sub.is-open { grid-template-rows: 1fr; }

.mobile-menu__sub-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 0;
  overflow: hidden;
}

.mobile-menu__sub-inner > :first-child { margin-top: 12px; }
.mobile-menu__sub-inner > :last-child { margin-bottom: 4px; }

/* Sub-links: body face, 44px tall, centred */
.mobile-menu__sublink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 16px;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
  color: var(--fg-muted);
  text-decoration: none;
  transition: color 200ms ease;
}

.mobile-menu__sublink:hover,
.mobile-menu__sublink:focus-visible { color: var(--accent); }
/* Active state (Dave 2026-09-07): where you are, at every width and in
   both themes, in the accent with a 600 weight and a 3px bar in the
   gutter beside the row (colour is never the only signal); an active
   sub-item adds a 6px dot and lights its parent row so the group reads
   active while collapsed. main.js sets it on the homepage as sections
   pass the same offset the scroll rides use; inner pages carry it in the
   markup (aria-current="page" on their own entry). Declared after the
   hover rules so hovering a row never hides it. */
.mobile-menu__link.is-active {
  color: var(--accent);
  font-weight: 600;
}

.mobile-menu__link.is-active::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--menu-gutter, 32px));
  /* Runs the full row
     (the row link carries the vertical padding now, so the bar is simply
     top 0 to bottom 0) (Dave 2026-09-07) */
  top: 0;
  bottom: 0;
  width: 4px;
  border-radius: 2px;
  background: var(--accent);
}

.mobile-menu__sublink.is-active,
.mobile-menu__sublink[aria-current='page'] { color: var(--accent); }

.mobile-menu__sublink.is-active::before,
.mobile-menu__sublink[aria-current='page']::before {
  content: '';
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background: var(--accent);
}

/* "All Case Studies" / "All Select Work": the parent-level action, first
   in each list, a shade firmer than the entries under it with a hairline
   beneath (Dave 2026-09-07) */
.mobile-menu__sublink--all {
  width: 100%;
  margin-bottom: 4px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--divider);
  font-weight: 600;
  color: var(--fg-default);
}

/* Unpublished pieces: listed, muted, not tappable, with the tiles'
   "Coming soon" pill (same shape, theme tokens instead of the on-image
   glass so it reads on the menu's surface) */
.mobile-menu__sublink.is-soon {
  flex-wrap: wrap; /* a long title keeps its line; the pill drops under it */
  justify-content: center;
  row-gap: 2px;
  color: var(--fg-subtle);
  pointer-events: none;
  cursor: default;
}

.mobile-menu__soon {
  padding: 2px 8px;
  border: 1px solid var(--divider-strong);
  border-radius: 64px;
  background: var(--bg-surface-2);
  color: var(--fg-muted);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  line-height: 1.5;
  white-space: nowrap;
}

/* --- Slide-out panel (homepage, Dave 2026-09-07) -------------------------
   Right-side panel at every width; the page stays visible behind a dim
   overlay. Items stack left-aligned and slide in from the right; the
   destinations sit at the top and the theme switch in its own divided
   group at the bottom. The nav (z 100) rides over the panel (z 90), so
   the panel pads down past it. */
.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 85;
  background: rgba(14, 1, 28, 0.55);
  opacity: 0;
  visibility: hidden;
  transition: opacity 450ms ease, visibility 0s linear 450ms;
}

.menu-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 450ms ease, visibility 0s;
}

.mobile-menu--panel {
  inset: 0 0 0 auto;
  width: min(560px, calc(100% - 48px)); /* both panels share this width (Dave 2026-09-07; was 460) */
  flex-direction: column;
  transform: translateX(100%);
  box-shadow: -24px 0 48px rgba(14, 1, 28, 0.35);
  transition: transform 450ms var(--ease-out), visibility 0s linear 450ms;
}

/* The slide-down menu's status-bar bleed (::before, 120px past the top
   and bottom) is pointless on the side panel and, being absolutely
   positioned inside a scroll container, it added 120px of phantom scroll
   even when the content fit. Off here, so the panels only scroll when
   their content really is taller than the viewport (Dave 2026-09-07). */
.mobile-menu--panel::before { content: none; }

.mobile-menu--panel.is-open {
  transform: translateX(0);
  transition: transform 450ms var(--ease-out), visibility 0s;
}

/* Title row: fills the nav-height band, so "Menu" sits on the X's line
   (the X is the header's button, riding over the panel at the panel's
   32px padding edge). The panel's headline: display face at the h3 step
   the modal titles use (Dave 2026-09-07: serif for headlines only, never
   for navigation items). Shared by the menu and contact panels. */
.mobile-menu__head {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  box-sizing: border-box;
  width: 100%;
  height: calc(var(--nav-height) + env(safe-area-inset-top, 0px));
  padding: env(safe-area-inset-top, 0px) 32px 0;
}

.mobile-menu__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.625rem;
  line-height: 1; /* tight box, so its centre is the glyphs' centre, on the X's line */
  color: var(--fg-default);
}

.mobile-menu--panel .mobile-menu__nav {
  align-items: flex-start;
  gap: 0;
  margin: 0;
  padding: 12px 32px 32px;
  --menu-gutter: 32px; /* where the active bar sits (16px on phones) */
}

/* Each item is a 14px-padded row with a light hairline between rows
   (Dave 2026-09-07: dividers in place of the open 28px gaps) */
.mobile-menu--panel .mobile-menu__nav > .mobile-menu__item {
  padding: 0; /* the row link carries the 14px (Dave 2026-09-07: the hover tint fills the row) */
  border-top: 1px solid var(--divider);
}

.mobile-menu--panel .mobile-menu__nav > .mobile-menu__item:first-child { border-top: 0; }

.mobile-menu--panel .mobile-menu__item {
  align-items: flex-start;
  transform: translateX(24px);
}

.mobile-menu--panel.is-open .mobile-menu__item { transform: translateX(0); }

/* Navigation items in the body face, 1.25rem regular (Dave 2026-09-07:
   down from the 1.625rem/600 tile-title step; the full-screen menu on the
   inner pages keeps its serif items) */
.mobile-menu--panel .mobile-menu__link {
  font-family: var(--font-body);
  position: relative; /* anchors the active bar */
  font-size: 1.25rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
}

/* The active weight has to outrank the 400 above (same specificity, later source) */
.mobile-menu--panel .mobile-menu__link.is-active { font-weight: 600; }

/* Accordion rows fill the panel width: label left, chevron at the right edge */
.mobile-menu--panel .mobile-menu__row {
  width: 100%;
  justify-content: space-between;
}

.mobile-menu--panel .mobile-menu__expand {
  position: static;
  /* The 44px target hangs 9px above and below the 26px label line, so
     accordion rows stand as tall as the plain rows */
  margin: -9px -10px -9px 0;
}

/* Children of an accordion row (Dave 2026-09-07): indented one 16px
   step (12px on phones) and one weight step lighter than the parent
   row's 400, so they read as nested. "All ..." keeps a step over its
   siblings at 400. Hanken Grotesk 300 is requested in the font link for
   this. The indent is padding on the list, so every target keeps its
   full height. */
.mobile-menu--panel .mobile-menu__sub-inner {
  align-items: flex-start;
  --menu-indent: 16px;
  padding-left: var(--menu-indent);
}

.mobile-menu--panel .mobile-menu__sublink {
  padding: 0;
  text-align: left;
  font-weight: 300;
}

.mobile-menu--panel .mobile-menu__sublink--all { font-weight: 400; }

.mobile-menu--panel .mobile-menu__sublink.is-soon { justify-content: flex-start; }

/* Row hover / press (Dave 2026-09-07): every row, accordion parents and
   the children inside them included, tints edge to edge in the panel so
   it reads as one target. --menu-hover-bg is the interactive colour as a
   tint (violet/50 in light, the amber at 14% in dark); the label colour
   is untouched. Rows bleed into the gutters with negative margins and
   matching padding, so the 44px targets and the text edge stay where
   they are. The active bar (left: 0 now that the box starts at the
   panel edge) paints above the tint, so hover never erases it. */
.mobile-menu--panel .mobile-menu__link {
  display: flex;
  align-items: center;
  box-sizing: border-box;
  width: calc(100% + 2 * var(--menu-gutter, 32px));
  padding-top: 14px;
  padding-bottom: 14px;
  margin-left: calc(-1 * var(--menu-gutter, 32px));
  margin-right: calc(-1 * var(--menu-gutter, 32px));
  padding-left: var(--menu-gutter, 32px);
  padding-right: var(--menu-gutter, 32px);
  transition: color 200ms ease, background-color 200ms ease;
}

.mobile-menu--panel .mobile-menu__sub {
  width: calc(100% + 2 * var(--menu-gutter, 32px));
  margin: 0 calc(-1 * var(--menu-gutter, 32px));
}

.mobile-menu--panel .mobile-menu__sub-inner {
  padding-left: calc(var(--menu-gutter, 32px) + var(--menu-indent, 16px));
  padding-right: var(--menu-gutter, 32px);
}

.mobile-menu--panel .mobile-menu__sublink {
  display: flex;
  box-sizing: border-box;
  width: calc(100% + 2 * var(--menu-gutter, 32px) + var(--menu-indent, 16px));
  margin-left: calc(-1 * (var(--menu-gutter, 32px) + var(--menu-indent, 16px)));
  margin-right: calc(-1 * var(--menu-gutter, 32px));
  padding-left: calc(var(--menu-gutter, 32px) + var(--menu-indent, 16px));
  padding-right: var(--menu-gutter, 32px);
  transition: color 200ms ease, background-color 200ms ease;
}

.mobile-menu--panel .mobile-menu__link:hover,
.mobile-menu--panel .mobile-menu__link:active,
.mobile-menu--panel .mobile-menu__sublink:hover,
.mobile-menu--panel .mobile-menu__sublink:active { background-color: var(--menu-hover-bg); }

.mobile-menu--panel .mobile-menu__link::before { left: 0; }

.mobile-menu--panel button.mobile-menu__row:focus-visible {
  outline-offset: -2px;
  border-radius: 0;
}

/* Same ring on the link rows and children (the browser default showed
   through on the first-focused Home row) */
.mobile-menu--panel a.mobile-menu__link:focus-visible,
.mobile-menu--panel .mobile-menu__sublink:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Contact panel (Dave 2026-09-07): the footer block, left-aligned. The
   footer centres everything and pins dark tokens; here the theme tokens
   apply, so the panel matches the menu panel in both modes. Multi-line
   copy balances at a text-relative measure, like the hero copy. */
.mobile-menu__contact-body {
  align-items: stretch;
  box-sizing: border-box;
  width: 100%;
  --menu-gutter: 32px; /* the rows below bleed into it (16px on phones) */
  padding: 24px 32px calc(32px + env(safe-area-inset-bottom, 0px)); /* the title row above carries the nav-height band */
}

.mobile-menu--contact .contact-inner {
  max-width: none;
  margin: 0;
  align-items: flex-start;
  text-align: left;
}

.mobile-menu--contact .contact__headline {
  max-width: 20ch;
  text-wrap: balance;
}

.mobile-menu--contact .contact__desc,
.mobile-menu--contact .contact__note {
  max-width: none; /* full content width, the panel's 32px either side (Dave 2026-09-07) */
  text-wrap: balance;
}

/* Regular weight for the availability line in the panel (Dave 2026-09-07; the footer copy keeps its 600) */
.mobile-menu--contact .contact__note { font-weight: 400; }

.mobile-menu--contact .contact-email { width: 100%; }

.mobile-menu--contact .contact-actions {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
  width: 100%;
}

/* Link rows (LinkedIn, Behance, Instagram, Download Resume): the purple
   disc keeps the icon, the label sits beside it in the body face, and a
   light hairline divides the rows (Dave 2026-09-07). Scoped to the panel
   so it outranks the footer's .contact-social circle, which is declared
   later in the file. */
.mobile-menu--contact .contact-social--row {
  width: auto;
  height: auto;
  justify-content: flex-start;
  gap: 12px;
  /* Bleeds into the panel gutters like the nav rows, so the hairlines and
     the hover tint run edge to edge (Dave 2026-09-07) */
  box-sizing: border-box;
  width: calc(100% + 2 * var(--menu-gutter, 32px));
  margin: 0 calc(-1 * var(--menu-gutter, 32px));
  padding: 10px var(--menu-gutter, 32px);
  border-top: 1px solid var(--divider);
  border-radius: 0;
  background: transparent;
  color: var(--fg-default);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: none;
  transition: color 200ms ease, background-color 200ms ease;
}

/* Link rows (not the email row, which is a div): the same tint as the nav
   rows, label to the accent as before, and the pressed state for touch */
.mobile-menu--contact a.contact-social--row:hover,
.mobile-menu--contact a.contact-social--row:active {
  transform: none;
  background: var(--menu-hover-bg);
  color: var(--accent);
}

/* The panel takes focus on open so the reader starts at its top; no ring
   on the container itself (its controls keep theirs) */
.mobile-menu--panel:focus { outline: none; }

.contact-social__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--primary-default);
  color: var(--primary-contrast);
}

.contact-social__icon svg {
  width: 18px;
  height: 18px;
}

.mobile-menu--contact .contact-social--row:first-child { border-top: 0; }

/* Email row in the contact panel (Dave 2026-09-07): the same disc + label
   shape as the link rows, with the input + copy box in the label's
   place. It sits first in the list but still takes a hairline, because
   the availability note sits above it. Not a link: no hover colour. */
.mobile-menu--contact .contact-actions .contact-email-row {
  border-top: 1px solid var(--divider);
  cursor: default;
}

.mobile-menu--contact .contact-email-row .contact-email {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
}

/* Panel email row (Dave 2026-09-08): the same box + primary "Email Dave"
   group as the footer, in the label slot beside the envelope disc. The
   box flexes, the button keeps its size; the <=640 block stacks them. */
.mobile-menu--contact .contact-email-group {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  margin: 0;
  justify-content: flex-start;
}

.mobile-menu--contact .contact-email-group .contact-email {
  flex: 1 1 220px;
  width: auto;
  margin: 0;
}

.mobile-menu--contact .contact-email__mail {
  flex: 0 0 auto;
  padding: 0 16px;
}

/* The email row keeps its rest look on hover (the base .contact-social
   hover fill must not reach the div now that the link rows tint) */
.mobile-menu--contact .contact-email-row:hover,
.mobile-menu--contact .contact-email-row:active {
  transform: none;
  background: transparent;
  color: inherit;
}

/* Settings group: pinned to the bottom on purpose (a setting, not a
   destination), with a firm divider so the space above it reads as
   separation rather than a gap, and a tighter row (Dave 2026-09-07) */
.mobile-menu__settings {
  align-items: stretch;
  box-sizing: border-box;
  margin-top: auto;
  padding: 12px 32px calc(20px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--divider-strong);
}

/* Theme switch row: glyph, label, then a switch track that follows
   aria-checked (main.js). Reuses .theme-toggle so the id wiring holds. */
.theme-toggle--switch {
  width: 100%;
  height: 44px;
  justify-content: flex-start;
  gap: 12px;
  color: var(--fg-default);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
}

.theme-toggle--switch:hover,
.theme-toggle--switch:focus-visible {
  color: var(--accent);
  transform: none;
}

.theme-toggle--switch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
}

/* The switch keeps the moon in both modes: the row is the setting itself,
   not a "switch to" affordance like the icon-only nav toggle */
:root .theme-toggle--switch .theme-toggle__icon--moon { display: block; }
:root .theme-toggle--switch .theme-toggle__icon--sun { display: none; }

.theme-toggle__text {
  flex: 1;
  text-align: left;
}

.theme-toggle__track {
  position: relative;
  flex-shrink: 0;
  width: 40px;
  height: 22px;
  border: 1px solid var(--divider-strong);
  border-radius: 999px;
  background: var(--bg-surface-2);
  transition: background 200ms ease, border-color 200ms ease;
}

.theme-toggle__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--fg-default);
  transition: transform 200ms var(--ease-out), background 200ms ease;
}

.theme-toggle--switch[aria-checked='true'] .theme-toggle__track {
  border-color: var(--accent);
  background: var(--accent);
}

.theme-toggle--switch[aria-checked='true'] .theme-toggle__knob {
  transform: translateX(18px);
  background: var(--accent-contrast);
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 100vh;
  min-height: 100svh;
  overflow: hidden;
  /* The hero stays dark in both themes (dark video background);
     light mode only applies from the Impact section onwards */
  --bg-default: #0e011c;
  --bg-surface: #1e0c32;
  --fg-default: #ffffff;
  --fg-muted: #b3a0c8;
  --fg-subtle: #978aaa;
  --ghost-border: rgba(255, 255, 255, 0.5);
  /* Pinned so the stat chip stroke (divider-strong) and card stroke
     (neutral-700) keep their dark-theme values over the dark hero when
     the site is in light mode */
  --divider: rgba(151, 138, 170, 0.25);
  --divider-strong: rgba(151, 138, 170, 0.35);
  --neutral-700: #3d3350;
  --neutral-200: #ded7e6;
  --primary-default: #8200ff;
  --accent: #ffc20a;
  --accent-contrast: #1e0c32;
  /* Desktop scrim: solid up to the stat band's top edge (1440 to 1799
     value; the desktop breakpoint blocks override per range). Only the
     >=1025 .hero__gradient rule reads it. */
  --hero-band-top: 155px;
}

.hero__media {
  position: absolute;
  /* Bleed 1px past the side AND bottom edges: iOS Safari can rasterize
     the layer a hair short of the viewport at fractional DPRs, leaving a
     bare hairline on the right — and at the bottom seam with the next
     section, where the still image peeks out from under the gradient as a
     bright 1px line. The hero's overflow:hidden clips the bleed, so this
     has zero visual effect beyond closing those seams. */
  inset: 0 -1px -1px;
  background: var(--bg-default);
}

/* Two absolutely-stacked stills swap roles for the whole sequence.
   The incoming layer fades in ON TOP of the still-opaque outgoing layer
   (see z-index classes below) so the crossfade never dips to the dark
   background mid-transition. main.js owns each layer's transform
   (parallax translate, then Ken Burns scale — in that order) and src.
   isolation on the wrapper caps the layers' z-indexes inside it, so they
   can never paint above the gradient that follows in DOM order. */
.hero__stills {
  position: absolute;
  inset: 0;
  isolation: isolate;
}

/* Hold-to-pause: pressing and holding on the media layer (never the nav,
   hero copy, or CTAs — those live in .hero__content, painted above) eases
   the sequence to a stop; releasing eases it back into motion. Default
   cursor on purpose: pointer would imply navigation. */
.hero__media { cursor: default; }

.hero__media:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
}

/* Rotation control (Dave 2026-09-07): a 28px ring inline with the still
   caption, after the text, pinned at the right so it never moves. Shows the
   pause glyph while the sequence runs and the play glyph while it is
   stopped (by the button or by a hold on the media). A thin progress arc
   runs clockwise just outside the button over each still's time on screen
   (main.js drives its dash offset from the slideshow's own virtual clock,
   so a hold or the button freezes it and it resets when the next still
   starts entering). Hidden with the caption on phones and under reduced
   motion, where the sequence is a single frame. */
.hero__pause {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--divider-strong);
  border-radius: 50%;
  background: transparent;
  color: var(--neutral-200);
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease;
}

.hero__pause:hover {
  color: var(--fg-default);
  border-color: var(--fg-default);
}

.hero__pause:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Progress arc: a 1.5px stroke 3px outside the button's own stroke, drawn
   from 12 o'clock (pathLength 100, dash offset 100 = empty, 0 = full) */
.hero__pause-ring {
  position: absolute;
  inset: -4px;
  width: calc(100% + 8px);
  height: calc(100% + 8px);
  transform: rotate(-90deg);
  pointer-events: none;
  overflow: visible;
}

.hero__pause-ring circle {
  fill: none;
  stroke: var(--neutral-200);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  opacity: 0.45; /* quiet by default (Dave 2026-09-07), fuller on hover */
  transition: opacity 200ms ease;
}

.hero__pause:hover .hero__pause-ring circle {
  stroke: var(--fg-default);
  opacity: 0.85;
}

.hero__pause-icon--play { display: none; }
.hero__pause.is-paused .hero__pause-icon--play { display: block; }
.hero__pause.is-paused .hero__pause-icon--pause { display: none; }

.hero__still {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* Ken Burns origin: main.js lerps --kb-ox/oy per layer — toward the
     cursor while a fine pointer is active, toward the slide's assigned
     drift direction otherwise */
  transform-origin: var(--kb-ox, 50%) var(--kb-oy, 50%);
}

.hero__still.is-visible {
  opacity: 1;
  will-change: transform; /* the live layers only; the standby has neither class */
}

.hero__still.is-entering {
  z-index: 2;
  opacity: 1;
  transition: opacity 1.2s ease;
  will-change: transform;
}

.hero__still.is-leaving { z-index: 1; }

/* Reduced motion: single static frame, no zoom/fade */
.hero__still.is-static {
  opacity: 1;
  transition: none;
}

/* Bottom gradient keeps hero text readable (values from Figma
   Color/hero/from -> Color/hero/to); the top gradient lives on the nav */
.hero__gradient {
  position: absolute;
  /* Same 1px side + bottom bleed as .hero__media: the gradient is the
     visible purple, so it must be the layer that reaches past the
     rounding — including at the seam with the section below. */
  inset: 0 -1px -1px;
  background: linear-gradient(180deg, var(--hero-to) 50%, var(--bg-default) 93.7%);
}

/* Figma hero (1920-768): a fluid wrapping row anchored to the bottom.
   .hero__main (name + copy, each 560px) takes the remaining space and
   wraps internally; the 546px CTA row wraps below it on narrow widths */
.hero__content {
  position: relative;
  width: 100%;
  padding: calc(var(--nav-height) + 24px + env(safe-area-inset-top, 0px)) 48px 48px;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  align-content: flex-end;
  gap: 36px 64px;
}

.hero__main {
  flex: 1 1 560px;
  min-width: min(560px, 100%);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 24px 164px;
}

.hero__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 7rem;
  line-height: 1;
  letter-spacing: 0.018em;
  color: var(--fg-default);
  white-space: nowrap;
  width: 560px;
  max-width: 100%;
}

/* Copy column: baseline of the body copy aligns with the name's baseline.
   flex-end aligns box bottoms; the margin compensates for the difference
   between the name's descender depth and the copy's descender+half-leading */
.hero__copy {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 560px;
  max-width: 100%;
  /* Raise the copy so its last baseline matches the name's baseline:
     the name's descender (~0.21em of the clamped name size) reaches
     deeper than the copy's descender + half-leading (~8px) */
  margin-bottom: clamp(8px, 0.7vw, 13px);
}

.hero__sub {
  margin: 0;
  width: 100%; /* own row in the wrapping tablet stack (it sits above the name at every width) */
  font-size: 1.625rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--violet-200); /* Dave 2026-09-06 (was violet-100 earlier that day) */
}

/* Round separator dot in the title ("UX/UI  Product Design"): drawn in CSS
   so it is a true circle in every font (0.22em, was 0.28), knocked back to 55% of the
   title colour so it reads quieter than the words (Dave 2026-09-06) */
.hero__sub-dot {
  display: inline-block;
  width: 0.22em;
  height: 0.22em;
  margin: 0 0.4em;
  position: relative;
  top: -0.08em;
  vertical-align: middle;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}

/* Abbreviated title + shortened body copy used only on phones (swapped in
   the <=640 block); the short spans are aria-hidden so screen readers
   always get the full versions */
.hero__sub-short,
.hero__desc-short { display: none; }

.hero__desc {
  margin: 0;
  max-width: 66ch; /* text-relative, so the balance holds if the size changes (Dave 2026-09-07) */
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
  text-wrap: balance; /* even line lengths, ragged right (Dave 2026-09-07: was pretty; the first two lines ran three words past the third) */
}

/* Lead sentence of the body (Figma 727:783: Hanken Grotesk Black, white);
   hidden on phones in the <=640 block. Its own single line since
   2026-09-08 (Dave): block + nowrap, and it fits at every width that shows
   it (540px in the 628px desktop column, 574px in 577px at the 641px
   tablet floor). The body below it balances into three even lines, so no
   widow or orphan. */
.hero__desc-lead {
  display: block;
  white-space: nowrap;
  font-weight: 900;
  color: var(--fg-default); /* white per the frame (Dave tried violet-200 on 2026-09-06 and went back) */
}

/* Still caption: hidden on phones; desktop and tablet blocks place it */
.hero__caption { display: none; }

/* Starts invisible; main.js adds .is-filled as the first still enters
   (with the still's own 1.2s fade), then each crossfade drops it out in
   500ms and brings the new name in over the remaining 700ms */
.hero__caption-inner {
  display: inline-block;
  opacity: 0;
  transition: opacity 700ms ease;
}

.hero__caption-inner.is-filled { opacity: 0.85; }

.hero__caption-inner.is-out {
  opacity: 0;
  transition-duration: 500ms;
}

/* Client and separator in the Mist stand-in, product in white */
.hero__caption-client,
.hero__caption-sep { color: var(--neutral-200); }

.hero__caption-product { color: var(--fg-default); }

/* The caption links to the piece's page when it has one (main.js sets
   the href per slide). Same type and place; a quiet hover (underline,
   client name to white) says it is clickable, and keyboard focus gets the
   accent ring. Without an href it is plain text again. */
.hero__caption-link {
  color: inherit;
  text-decoration: none;
  border-radius: 2px;
}

.hero__caption-link[href]:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--neutral-200);
}

.hero__caption-link[href]:hover .hero__caption-client,
.hero__caption-link[href]:hover .hero__caption-sep { color: var(--fg-default); }

.hero__caption-link[href]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Desktop wrapped state (copy stacked under the name, CTA to the right):
   the copy centers in its band — 24px from the name above, 24px to the
   button line below. Above ~1750px the copy sits beside the name on the
   shared baseline instead, so the nudge doesn't apply there. */
@media (min-width: 769px) and (max-width: 1750px) {
  .hero__copy { margin-bottom: 24px; }
}

/* Buttons + scroll hint share one row that wraps as a unit: buttons
   side-by-side, hint inline to the right */
.hero__actions {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 36px;
  width: max-content;
  max-width: 100%;
}

.hero__ctas {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--button-gap);
}

/* A step up from the base button (Dave 2026-09-07: more presence, still
   under the name and the stat band): 18px type, 28px side padding, width
   from the label with Figma's 180px as the floor */
.hero__ctas .btn {
  min-width: 180px;
  padding: 0 28px;
  font-size: 1.125rem;
  white-space: nowrap;
}

/* --- Buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  min-width: 180px;
  padding: 0 24px;
  border-radius: 8px;
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: none;
  transition: background 250ms ease, border-color 250ms ease, transform 250ms var(--ease-out);
}

.btn:hover { transform: translateY(-2px); }

.btn--primary {
  background: var(--primary-default);
  color: var(--primary-contrast);
}

.btn--primary:hover { background: #9226ff; }

.btn--ghost {
  border: 1px solid var(--ghost-border);
  color: var(--fg-default);
}

.btn--ghost:hover { border-color: var(--fg-default); }

/* --- Scroll prompt -------------------------------------------------------- */
.hero__scroll {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  white-space: nowrap;
  gap: 16px;
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--amber);
  text-decoration: none;
  transition: color 250ms ease;
}

/* Scale lives on the span: the link itself and the arrow both have
   animations pinning their transform (cascade fill / bounce) */
.hero__scroll span {
  display: inline-block;
  transition: transform 250ms var(--ease-out);
}

.hero__scroll:hover,
.hero__scroll:focus-visible {
  color: var(--accent);
}

.hero__scroll:hover span,
.hero__scroll:focus-visible span {
  transform: scale(1.04);
}

.hero__scroll-arrow {
  color: var(--amber);
  animation: arrow-bounce 1s ease-in-out infinite;
}

@keyframes arrow-bounce {
  0%, 100% { transform: translateY(-3px); }
  50% { transform: translateY(3px); }
}

/* --- Hero stat cards ---------------------------------------------------------
   The hero band (.stat-cards--hero): four compact proof points, DESKTOP
   ONLY, pinned to the bottom of the hero. The Impact section's eight
   result cards are a separate, lighter component (.impact-card, in the
   Impact section further down). Card: eyebrow chip + numeral on row 1,
   caption on row 2; at 1800px and up the hero cards go single-row (chip,
   numeral, caption).
   Tokens: neutral-700 surface, 1px divider-strong border, 8px radius, 16px
   padding, 3rem semibold numeral in violet-300, 17px/24px caption in
   neutral-200. The hero pins its dark tokens, so the band reads the same
   in both themes there.

   Hover / focus-visible: the numeral and caption exit first (150ms, drift
   up), then the CTA line springs in beside the chip with an overshoot
   (starts at the 150ms mark). Roll-off is the resting-state transition
   below: CTA gone in 100ms, numeral and caption back 100ms later. The
   surface darkens to --bg-surface so the gold CTA carries contrast; the
   chip never moves. Reduced motion: no transitions at all (see the
   reduced-motion block at the end of the file). */
.stat-cards {
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
}

.stat-cards__item { display: flex; }

.stat-card {
  flex: 1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 16px;
  row-gap: 8px;
  padding: 12px;
  /* Surface sits a step above the hero's near-black (bg-surface), with the
     neutral-700 outline, so the band recedes behind the copy. On
     hover/focus the surface goes to bg-default, darker again, to lift the
     gold CTA; the outline stays (Dave). */
  border: 1px solid var(--neutral-700);
  border-radius: 8px;
  background: var(--bg-surface);
  color: inherit;
  text-decoration: none;
  transition: background-color 150ms ease, border-color 250ms ease, transform 250ms var(--ease-out);
}

/* Eyebrow chip (Figma 727:783): 13px uppercase semibold hugging its
   label in a 4px-padded, 1px bordered, 4px-radius tile at 80% (Dave
   2026-09-05: hug, not a fixed width) */
.stat-card__eyebrow,
.impact-card__chip {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  margin: 0;
  padding: 4px;
  border: 1px solid var(--divider-strong);
  border-radius: 4px;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--fg-default);
  opacity: 0.8;
}

.stat-card__value {
  grid-column: 2;
  grid-row: 1;
  margin: 0;
  font-size: 2.5rem; /* frame: 40px */
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--violet-300);
}

/* Caption: 14px (frame), sized so it holds to two lines in every card */
.stat-card__label {
  grid-column: 1 / -1;
  grid-row: 2;
  align-self: start;
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.3;
  color: var(--neutral-200);
  text-wrap: pretty;
}

/* Roll-off timing for the pair that returns */
.stat-card__value,
.stat-card__label {
  transition: opacity 150ms ease 100ms, transform 150ms ease 100ms;
}

/* CTA line: beside the chip, centered over the numeral + caption area.
   Always in the DOM (and the accessibility tree) so the link name ends
   with its destination whether or not anyone hovers. */
.stat-card__hint {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 24px;
  color: var(--accent);
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 100ms ease, transform 100ms ease; /* roll-off */
}

.stat-card__arrow {
  display: inline-block;
  vertical-align: middle;
  width: 20px;
  height: 20px;
  margin-top: -3px;
  color: inherit;
}

/* Roll-on: numeral + caption leave first... */
.stat-card:hover .stat-card__value,
.stat-card:focus-visible .stat-card__value,
.stat-card:hover .stat-card__label,
.stat-card:focus-visible .stat-card__label {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 150ms ease-in, transform 150ms ease-in;
}

/* ...then the CTA springs in after that beat (overshoot curve) */
.stat-card:hover .stat-card__hint,
.stat-card:focus-visible .stat-card__hint {
  opacity: 1;
  transform: none;
  transition: opacity 200ms ease-out 150ms, transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1) 150ms;
}

.stat-card:hover,
.stat-card:focus-visible {
  background: var(--bg-default);
}

.stat-card:hover { transform: translateY(-2px); }

/* Focus ring in the accent, offset off the card, independent of the
   darkened surface */
.stat-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Hero band numeral: its digits stand exactly as tall as the two-line
   caption beside it (Dave 2026-09-06). Hanken Grotesk 600 digits measure
   0.716em; the caption block is 14px x 1.3 x 2 = 36.4px, so 36.4 / 0.716
   = 50.8px -> 3.2rem. line-height 1 keeps the box tight so the centred
   caption and the centred digits share top and bottom to within 1px. */
.stat-cards--hero .stat-card__value {
  font-size: 3.2rem;
  line-height: 1;
}

/* Hero band below the single-row layout (1025 to 1799): the card breaks
   into chip on line one, then numeral and caption side by side on line
   two (Dave 2026-09-06). The hover CTA takes the second line. The Impact
   clones on tablet and phone keep their own stack (see the <=1024 block). */
@media (min-width: 1025px) and (max-width: 1799px) {
  .stat-cards--hero .stat-card__eyebrow {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  .stat-cards--hero .stat-card__value {
    grid-column: 1;
    grid-row: 2;
  }

  .stat-cards--hero .stat-card__label {
    grid-column: 2;
    grid-row: 2;
    align-self: center;
  }

  .stat-cards--hero .stat-card__hint {
    grid-column: 1 / -1;
    grid-row: 2;
  }
}

/* Narrow desktops (1025 to 1199): cards are ~210px wide, so the caption
   steps to 12px beside the numeral, and the four-line captions make the
   band its tallest (198px top edge at 1025) */
@media (min-width: 1025px) and (max-width: 1199px) {
  .stat-cards--hero .stat-card__label { font-size: 0.75rem; }
  .hero { --hero-band-top: 210px; }
}

/* 1200 to 1439: three-line captions, band top at 162px */
@media (min-width: 1200px) and (max-width: 1439px) {
  .hero { --hero-band-top: 175px; }
}

/* Frame layout: chip, numeral, caption on one row. Below this the caption
   would run past two lines beside the numeral, so it drops under the chip
   and numeral instead (still two lines). */
@media (min-width: 1800px) {
  .hero { --hero-band-top: 125px; } /* single-row cards: band top at 113px */

  .stat-cards--hero .stat-card {
    grid-template-columns: auto auto minmax(0, 1fr);
    grid-template-rows: auto;
  }

  .stat-cards--hero .stat-card__label {
    grid-column: 3;
    grid-row: 1;
    align-self: center;
  }

  .stat-cards--hero .stat-card__hint {
    grid-column: 2 / span 2;
    grid-row: 1;
  }
}

/* Touch: press feedback matches the Select Work tiles */
@media (hover: none) {
  .stat-card:active {
    transform: scale(0.98);
    opacity: 0.9;
  }
}

/* --- Desktop hero (2026-09-05 spec) -------------------------------------------
   One left-aligned column anchored to the bottom of the hero: name (one
   line), title, body, button row, then the full-width stat band with the
   scroll arrow in its own 20px lane at the right end. Two-column grid so
   the lane can sit beside the band; .hero__main and .hero__actions become
   display:contents so their children place directly on the grid and the
   tablet/phone DOM order is untouched. The name and title are pulled
   together into one lockup. Primary CTA first (DOM order). Everything at
   1024px and below is the live site's hero, unchanged. */
@media (min-width: 1025px) {
  .hero__content {
    display: grid;
    /* col 1 = eyebrow / name / body (auto: sized by the 660px body), col 2
       = the CTA pair 64px to the right of the body (32px column gap + 32px
       margin), col 3 = the 20px scroll lane. Dave 2026-09-06: buttons
       beside the body, on its baseline, not flush with the band's edge. */
    grid-template-columns: auto minmax(0, 1fr) 20px;
    grid-template-rows: auto auto auto auto auto;
    align-items: end;
    justify-items: start;
    row-gap: 0;
    column-gap: 32px;
    padding-bottom: 32px; /* was 48 (Dave 2026-09-06: less below the band, more above it) */
  }

  .hero__main,
  .hero__actions { display: contents; }

  /* Frame 727:783: 64px, one line, 0.018em tracking. Row 2 since
     2026-09-06: the title became an eyebrow ABOVE the name (Dave). */
  .hero__name {
    grid-column: 1;
    grid-row: 2;
    width: auto;
    margin-bottom: 16px; /* the old copy lockup (12px margin + 4px padding); lives here so row 3 is exactly the body's height */
    font-size: 4rem;
  }

  .hero__name-break { display: none; }

  /* The copy wrapper dissolves here so the title and body can take their
     own grid rows either side of the name; tablet/phone keep the wrapper
     (title under the name, as on the live site). Its old 12px margin +
     4px padding lockup is carried by .hero__desc's margin-top below. */
  .hero__copy { display: contents; }

  /* Eyebrow above the name: 1.625rem (was 2.125rem below the name),
     8px down to the name box (Dave 2026-09-06: "slightly smaller") */
  .hero__sub {
    grid-column: 1;
    grid-row: 1;
    width: auto;
    margin: 0 0 8px;
    font-size: 1.625rem;
    line-height: 1.2;
  }

  .hero__desc {
    grid-column: 1;
    grid-row: 3;
    align-self: end;
    /* Three lines, balanced (Dave 2026-09-07): 64ch is the exact
       three-line step for this copy at 17px and 66ch gives it 2ch of
       slack; the balanced lines measure 574 / 609 / 555px, 12 / 11 / 12
       words, and the box is 628px (was 640), so a little more still shows */
    max-width: 66ch;
    text-wrap: balance;
    margin-top: 0;
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--neutral-200); /* frame: Mist */
  }

  /* Button pair directly under the body, left-aligned with the text
     column (Dave 2026-09-07: out of the middle of the frame, where the
     cycling stills decided their legibility). Each button carries its
     own .cascade step (--i 3 and 4). */
  .hero__ctas {
    grid-column: 1;
    grid-row: 4;
    justify-self: start;
    align-self: end;
    margin: 24px 0 0;
  }

  .stat-cards--hero {
    grid-column: 1 / 3;
    grid-row: 5;
    justify-self: stretch;
    margin-top: 40px; /* Dave 2026-09-06: 40px from the copy line (32 felt tight, 48 too wide) */
  }

  /* Still caption: the button row's right-hand cell, pushed to the band's
     right edge and sitting on the buttons' bottom line, so it anchors to
     the band corner rather than floating mid-frame now that the buttons
     live under the body (Dave 2026-09-07). It spans both columns and
     ends at the band's edge, so its one line is never squeezed by the
     narrow right-hand column on small desktops (the buttons in column 1
     leave 500px or more free beside them). The inner span carries the
     fade so it never fights the load cascade's animation fill on the
     outer element. */
  .hero__caption {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    grid-column: 1 / 3;
    grid-row: 4;
    justify-self: end;
    align-self: end;
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    text-align: right;
  }


  /* Arrow lane: the scroll link stretches to the band's height; the
     arrow travels top to bottom of it on a loop, clear of the last card */
  .hero__scroll {
    grid-column: 3;
    grid-row: 5;
    align-self: stretch;
    justify-self: stretch;
    position: relative;
    display: block;
    width: 20px;
    min-height: 20px;
    margin-top: 40px;
  }

  .hero__scroll span { display: none; }

  .hero__scroll-arrow {
    position: absolute;
    top: 0;
    left: 0;
    animation: arrow-lane 1.6s ease-in-out infinite alternate;
  }

  @keyframes arrow-lane {
    from { top: 0; }
    to { top: calc(100% - 20px); }
  }

  /* Figma 727:783 background fills (Dave's 2026-09-05 revision), decoded
     from their gradient transforms. Bottom linear: clear at 75.2%, solid
     near-black at 89.4%. Radial: the surface plum (#1e0c32) stepping
     1 -> 0.8 -> 0.6 -> 0.4 -> 0 across its radius, centred at 14.8% /
     87.0% of the frame, ellipse 441 x 784px on 1920 with the
     long axis at -70.7deg. CSS cannot rotate a radial-gradient, so the
     pool is painted on a pseudo-element rotated to match, sized in the
     frame's PIXELS (the text stack is the same pixel height on a 750px
     laptop as on the 1080px frame); the hero's overflow:hidden clips the
     overhang. The frame's top scrim is not reproduced: the nav has its
     own. */
  /* Ramp in px from the bottom because the copy stack is bottom-anchored.
     Solid through the whole stat band zone (Dave 2026-09-07: the band held
     a consistent contrast floor only for its bottom 80px; the gaps between
     cards and the top half of the band sat on the ramp, so the floor
     changed with each still). The band's top edge is 32px bottom padding
     + the card height, which steps with the caption wrapping: measured
     144px at 1440, 162 at 1200 to 1300, 198 at 1025, 113 at 1800 and up.
     --hero-band-top defaults on .hero (155px, the 1440 to 1799 case) and
     the range blocks above override it with a few px to spare; above it
     a 290px ramp (0.65 at 150px, clear 140px later; was 180 / 340 before
     Dave 2026-09-07: "slightly shorter", the band itself stays put). */
  /* Tilted 6deg (Dave 2026-09-07, a trial; 3deg read as too slight): the
     ramp's edge runs ~150px higher at the left, under the copy, than at
     the right, where more of the still shows. Stops are measured along
     the tilted line from the bottom-left corner, so the solid stop
     carries 160px extra to keep the stat band covered at the low right
     end. Revert: "to top" with the stops at +0 / +150 / +290. */
  .hero__gradient {
    background: linear-gradient(6deg,
      var(--bg-default) calc(var(--hero-band-top) + 160px),
      rgba(14, 1, 28, 0.65) calc(var(--hero-band-top) + 310px),
      rgba(14, 1, 28, 0) calc(var(--hero-band-top) + 450px));
  }

  /* No radial pool any more (Dave 2026-09-06: "remove the radial gradient
     and make the bottom scrim a little taller"). The corner ellipses and
     the Figma rotated pool are documented in the project memory notes. */
  .hero__gradient::before { content: none; }
}

/* The narrow-desktop CTA stack and the 1440 to 1599 two-line caption
   are gone (Dave 2026-09-07): with the buttons under the body the
   caption has the whole right-hand column to itself. */

/* ==========================================================================
   Cascade-in on page load (fade up from below, overlapping stagger)
   ========================================================================== */
/* The hidden state lives in the keyframes with a backwards fill, so an
   element is normal again once it lands (hover transforms such as the
   buttons' 2px lift keep working; a fill-forwards transform would pin them) */
.cascade {
  animation: cascade-in var(--cascade-duration) ease-out backwards;
  animation-delay: calc(200ms + var(--i) * var(--cascade-step));
}

@keyframes cascade-in {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Home hero: eleven steps (eyebrow, name, body, each button, caption,
   the four stat cards, the arrow), 120ms apart so they overlap, a long
   soft ease-out so each one settles rather than snaps (Dave 2026-09-06) */
.hero .cascade {
  animation-duration: 900ms;
  animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
  animation-delay: calc(150ms + var(--i) * 120ms);
}

/* ==========================================================================
   Impact section
   ========================================================================== */
.impact {
  /* Section-local colour calls (light mode overrides at the top of the
     file): the CTA purple, the metric (dark plum on the white light-mode
     card, the white foreground on the dark one), the context copy
     (FG/Subtle in both themes, Dave 2026-09-06), the source note (also
     the colour of each card's project / industry line) */
  --impact-purple: var(--violet-300);
  --impact-metric: var(--fg-default);
  --impact-context: var(--fg-muted); /* body copy, 9.9:1 on the card */
  --impact-source: color-mix(in srgb, var(--fg-muted) 42%, transparent); /* subtle footnote (Dave 2026-09-06), ~2.3:1 on the page */
  --impact-chip-bg: var(--violet-800); /* dark counterpart of the violet-100 chip */
  --impact-chip-fg: var(--violet-200); /* 7.5:1 on violet-800 */
  padding: 96px 48px 72px; /* Dave 2026-09-06: less air above the eyebrow (was 144) */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 72px;
  scroll-margin-top: var(--nav-height);
}

.impact__card {
  width: 100%;
  max-width: var(--content-max);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 48px;
  /* Fades in first when the section enters the viewport */
  opacity: 0;
  transition: opacity 600ms ease-out;
}

.impact.in-view .impact__card { opacity: 1; }

.impact__header {
  max-width: 680px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
}

.impact__eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.impact__headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.impact__sub {
  margin: -8px 0 0;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-default);
}

/* Grid + dots + source note as one group, so the note's own margin sets
   its distance from the cards instead of the section's 48px gap */
.impact__results {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* ==========================================================================
   Shared carousel system (Dave 2026-09-06): Results, In Their Words and
   the tablet / phone Select Work row are variants of ONE component. A
   section supplies a .carousel-track (the finite snap scroller) inside a
   [data-carousel] wrapper with a .carousel__footer holding the
   .carousel__arrows; main.js wires arrows, mouse drag and keys once for
   every wrapper. Per-section variables: --car-cols (cards in the
   container), --car-peek (extra slice of the next card), --car-gap,
   --car-gutter (the breakout on each side, also the edge-fade width).
   ========================================================================== */

/* Control row under the track, inside the container (the track bleeds
   past it; this row does not): arrows grouped left, any note right,
   centred on each other. 8px on top of the group's 16px gap = 24px
   clear of the cards. Wraps on phones (see the <=640 block). */
.carousel__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px 24px;
  width: 100%;
  margin-top: 8px;
}

.carousel__arrows {
  display: flex;
  gap: 8px;
}

/* Arrow: the theme toggle's 44px disc with the divider stroke; accent on
   hover / focus; muted, inert at either end of the track */
.carousel__arrow {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--divider-strong);
  border-radius: 22px;
  background: transparent;
  color: var(--fg-default);
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease, opacity 200ms ease, transform 200ms var(--ease-out);
}

.carousel__arrow:hover {
  color: var(--accent);
  border-color: var(--accent);
  transform: translateY(-1px);
}

.carousel__arrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.carousel__arrow:disabled {
  opacity: 0.3;
  cursor: default;
  pointer-events: none;
  transform: none;
}

/* The track: a finite CSS snap scroller at EVERY width (Dave 2026-09-06).
   It breaks out of its container by --car-gutter on each side: the first
   card still starts on the container's left edge (scroll padding =
   gutter), and the scroller's box extends into the gutter so the next
   card peeks in from the right. On desktop the gutter is the bleed left
   between the 1200px container and the 48px page margin (capped at
   180px, minus 8px of scrollbar slack); on tablet and phone it is the
   section's own 32 / 16px gutter. Card width = (container - gaps -
   peek) / --car-cols. Scrollbar hidden; the arrows, mouse drag, swipe
   and the arrow keys (main.js) stand in for it. Nothing moves on a
   timer, nothing loops. The 4px vertical padding keeps the hover lift
   and focus ring inside the clip. */
.carousel-track {
  --car-gutter: min(180px, max(0px, calc((100vw - var(--content-max)) / 2 - 56px)));
  --car-gap: 24px;
  --car-peek: 0px;
  --car-cols: 3;
  margin: 0 calc(-1 * var(--car-gutter));
  padding: 4px var(--car-gutter);
  list-style: none;
  display: flex;
  gap: var(--car-gap);
  width: calc(100% + 2 * var(--car-gutter));
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--car-gutter);
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  cursor: grab;
  /* Edge fade, the In Good Company ticker's mask (see .logo-marquee):
     cards dissolve across the gutter / bleed on each side instead of
     clipping. The fade width IS the gutter, so a snapped first card
     (which starts after the gutter) is never dimmed at rest. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--car-gutter), #000 calc(100% - var(--car-gutter)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--car-gutter), #000 calc(100% - var(--car-gutter)), transparent);
}

.carousel-track::-webkit-scrollbar { display: none; }

.carousel-track:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 8px;
}

/* While the mouse drags the track, snapping is off (it fights the drag)
   and nothing inside can be selected or natively dragged; main.js
   re-snaps to the nearest card on release */
.carousel-track.is-dragging {
  scroll-snap-type: none;
  scroll-behavior: auto;
  cursor: grabbing;
  user-select: none;
}

.carousel-track > * {
  flex: 0 0 calc((100% - (var(--car-cols) - 1) * var(--car-gap) - var(--car-peek)) / var(--car-cols));
  scroll-snap-align: start;
  -webkit-user-drag: none;
}

/* Narrow desktops have no bleed to peek into: one card fewer plus a 48px
   peek inside the container, and the track borrows 24px of the 48px
   page padding on each side so the edge fade has room */
@media (min-width: 1025px) and (max-width: 1299px) {
  .carousel-track { --car-gutter: 24px; --car-peek: 48px; }
}

/* Results: 4 in the container from 1300 up, 3 + peek below that */
.impact__grid { --car-cols: 4; }

@media (min-width: 1025px) and (max-width: 1299px) {
  .impact__grid { --car-cols: 3; }
}

/* A track can extend past its section's padding into the page margin;
   never past the viewport (clip, not scroll) */
.impact,
.quotes,
.select-work { overflow-x: clip; }

/* --- Impact result cards -----------------------------------------------------
   The deeper evidence: eight cards, each chip / metric / context /
   project and industry, nothing else (attribution lives in the case
   studies; the note under the grid says so). Same surface tokens as the
   hero stat cards (bg-surface, neutral-700 stroke, 8px radius) but a
   lighter, scannable dashboard tile: chip (the taxonomy: SIMPLIFIED /
   ACCELERATED / SCALED / IMPROVED), the whole metric phrase as the
   strongest element (one size for every card, wrapping as it must), body
   copy naming the product and Dave's contribution, then a permanent
   "View Case Study" footer with the arrow at the far right. Every card
   is a whole-card link; hover (fine pointers) and keyboard focus lift the
   card 2px, tint the border purple and nudge the arrow 4px right.
   One finite snap carousel at every width (see .impact__grid above).
   Items stretch to the tallest card, and the footer is pinned to the
   card's bottom, so every card shares a height and a footer line. */
.impact__item { display: flex; }
.impact-card { -webkit-user-drag: none; }

.impact-card {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Vertical rhythm is set per element (Dave 2026-09-06): chip, 16px,
     metric, 6px, body, then the footer pinned to the bottom */
  gap: 0;
  padding: 20px;
  border: 1px solid var(--neutral-700);
  border-radius: 8px;
  background: var(--bg-surface);
  color: inherit;
  text-decoration: none;
  transition: transform 250ms var(--ease-out), border-color 200ms ease;
}

/* Chip: the hero chip's size and shape (shared rule, see
   .stat-card__eyebrow) as a filled tile: violet-50 with violet-700 text
   in light mode, violet-800 with violet-200 text in dark (Dave
   2026-09-06) */
.impact-card__chip {
  border-color: var(--impact-chip-bg);
  background: var(--impact-chip-bg);
  color: var(--impact-chip-fg);
  opacity: 1;
}

/* Metric: one style for all eight (Dave 2026-09-06), dark plum in light
   mode, wrapping at word or hyphen boundaries; never per-card sizes */
.impact-card__metric {
  margin: 16px 0 0;
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--impact-metric);
  text-wrap: pretty;
}

/* Context: body-copy size, tight under the metric so the pair reads as
   one unit; colour per theme above (Dave 2026-09-06) */
.impact-card__context {
  margin: 6px 0 0;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--impact-context);
  text-wrap: pretty;
}

/* Footer: the CTA only, pinned to the card's bottom */
.impact-card__foot {
  width: 100%;
  margin-top: auto;
  padding-top: 16px;
}

/* CTA: label left, arrow at the far right. Rests in the metric's dark
   plum (white in dark mode) and turns the section purple on hover /
   focus, text and arrow together (the arrow is currentColor). Dave
   2026-09-06. */
.impact-card__cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--impact-metric);
  transition: color 200ms ease;
}

.impact-card__arrow {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: inherit;
  transition: transform 220ms var(--ease-out);
}

/* Hover (fine pointers) and keyboard focus: a 2px lift, the stroke tints
   toward the purple, the arrow nudges 4px right. Restrained on purpose. */
.impact-card:focus-visible {
  outline: 2px solid var(--impact-purple);
  outline-offset: 2px;
  border-color: color-mix(in srgb, var(--impact-purple) 55%, var(--neutral-700));
}

.impact-card:focus-visible .impact-card__cta { color: var(--impact-purple); }
.impact-card:focus-visible .impact-card__arrow { transform: translateX(4px); }

@media (hover: hover) and (pointer: fine) {
  .impact-card:hover {
    transform: translateY(-2px);
    border-color: color-mix(in srgb, var(--impact-purple) 55%, var(--neutral-700));
  }

  .impact-card:hover .impact-card__cta { color: var(--impact-purple); }
  .impact-card:hover .impact-card__arrow { transform: translateX(4px); }
}

/* One line for the whole section, a quiet footnote on the control row's
   right edge, well below the card text (fg-muted at 42% dark / 50%
   light; ~2.3:1, Dave chose subtle over AA on 2026-09-06) */
.impact__source {
  margin: 0 0 0 auto;
  font-size: 0.875rem;
  line-height: 1.4;
  text-align: right;
  color: var(--impact-source);
}

/* --- Scroll-triggered cascade (delays set per element via --d) ----------- */
[data-cascade] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 600ms ease-out, transform 600ms ease-out;
  transition-delay: var(--d, 0ms);
}

[data-cascade-fade] {
  opacity: 0;
  transition: opacity 600ms ease-out;
  transition-delay: var(--d, 0ms);
}

.in-view [data-cascade] {
  opacity: 1;
  transform: translateY(0);
}

.in-view [data-cascade-fade],
.in-view[data-cascade-fade] { opacity: 1; }

/* ==========================================================================
   Case Studies section
   ========================================================================== */
.case-studies {
  padding: 72px 0;
  display: flex;
  flex-direction: column;
  gap: 48px;
  scroll-margin-top: var(--nav-height);
}

.cs-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  padding: 0 48px;
}

.cs-header__eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.cs-header__headline {
  margin: 0;
  max-width: 680px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.cs-header__sub {
  margin: -8px 0 0;
  max-width: 680px;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
}

/* --- Sticky stack --------------------------------------------------------- */
.cs-stack {
  display: flex;
  flex-direction: column;
  gap: 48px;
  padding: 0 48px;
  /* Cards cap at 1600px on wide screens (1600 + 2*48 side padding, border-box) */
  width: 100%;
  max-width: 1696px;
  margin-inline: auto;
}

/* Card wrapper: fades in on first view (kept separate from the parallax
   styles, which are applied to .cs-card by JS) */
.cs-item {
  opacity: 0;
  transition: opacity 600ms ease-out;
}

.cs-item.in-view { opacity: 1; }

.cs-card {
  display: flex;
  border-radius: 20px;
  overflow: hidden;
  background: var(--bg-surface);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  /* will-change / transform-origin moved to .cs-card__stage (Dave 2026-09-07) */
}

/* Every breakpoint: cards pin and get covered by the next one
   (scale/blur recession applied by JS, tied to scroll position) */
.cs-item {
  position: sticky;
  top: 16px;
}

/* Client / info / stats sit as one centered group, matching the case study
   hero's .csp-hero__details. `center` (not space-between) is what keeps the
   stats tight under the category tags instead of pinned to the card floor —
   see the desktop block for the 36px gap that completes the match. On
   tablet/mobile the panel is content-height, so this is a no-op there. */
.cs-card__details {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 24px;
  padding: 36px;
  background: var(--bg-surface);
}

.cs-card__client {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-muted);
}

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

.cs-card__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.125rem, 3.5vw, 3.5rem);
  line-height: 1.05;
  letter-spacing: 1px;
  color: var(--fg-default);
}

.cs-card__desc {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--neutral-200);
}

.cs-card__tags {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.cs-card__tags li {
  border: 1.5px solid var(--neutral-700);
  border-radius: 64px;
  padding: 4px 8px;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.5;
  color: var(--fg-subtle);
  white-space: nowrap;
}

.cs-card__stats {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cs-card__stats li {
  position: relative;
  padding-left: 16px;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--fg-subtle);
}

.cs-card__stats li::before {
  content: '\2022';
  position: absolute;
  left: 0;
  color: var(--primary-default);
}

.cs-card__media {
  flex: 1;
  min-width: 0;
  position: relative;
}

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

/* --- Hero carousel: stacked slides cross-fade via .is-active (JS-timed).
   All slides are absolute, so the media container keeps its own size
   (desktop stretch / mobile aspect-ratio) and the card footprint is
   unchanged --------------------------------------------------------------- */
.cs-carousel img {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1000ms ease;
}

.cs-carousel img.is-active { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* JS never rotates slides; the first stays visible with no fade */
  .cs-carousel img { transition: none; }
}

/* --- Mouse-tracking tooltip (created by JS, desktop pointers only) -------- */
.cs-tooltip {
  position: fixed;
  z-index: 200;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--tooltip-bg);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--accent-contrast);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  /* Position is driven by a rAF lerp in JS via transform (compositor-only,
     no per-frame layout) — only opacity transitions in CSS */
  left: 0;
  top: 0;
  will-change: transform;
  transition: opacity 200ms ease;
}

.cs-tooltip.is-shown { opacity: 1; }

/* --- Desktop: alternating layout + sticky stack --------------------------- */
@media (min-width: 1025px) {
  .cs-card { height: 768px; }

  /* Details panel LEFT / image RIGHT; odd cards flip, so the stack opens
     image-left (1st: image left, 2nd: image right, 3rd: image left) */
  .cs-item:nth-child(odd) .cs-card { flex-direction: row-reverse; }

  /* 420px wide on a 36px gap — the same panel metrics as the case study
     hero's .csp-hero__details, so a card and the hero it opens read alike */
  .cs-card__details {
    width: 420px;
    flex-shrink: 0;
    gap: 36px;
  }

  /* Fixed-height cards center in the viewport when pinned */
  .cs-item { top: max(24px, calc(50vh - 384px)); }
}

/* --- Tablet / mobile: single column, tap feedback, no sticky -------------- */
@media (max-width: 1024px) {
  .cs-stack { padding: 0 32px; }
  .cs-header { padding: 0 32px; }

  .cs-card { flex-direction: column; }

  /* Image on top, details below */
  .cs-card__media {
    order: -1;
    aspect-ratio: 16 / 10;
    flex: none;
  }

  /* Category tags are desktop-only */
  .cs-card__tags { display: none; }
}

/* Tap feedback is instant (no transition — it would fight the per-frame
   parallax transforms now applied on every breakpoint) */
@media (hover: none) {
  .cs-card:active { transform: scale(0.98); opacity: 0.9; }
}

/* ==========================================================================
   Select Work section
   ========================================================================== */
/* ==========================================================================
   In Their Words (testimonials): header on the Impact header pattern,
   cards on the Impact / case-study quote card pattern, track + arrows on
   the shared carousel system. Copy comes from the evidence-library
   workbook (sheet "In Their Words"), quotes verbatim.
   ========================================================================== */
.quotes {
  padding: 0 48px 72px; /* shares the tinted band with In Good Company above; the hairline below carries the top 72 */
  scroll-margin-top: var(--nav-height);
}

/* Subtle hairline between In Good Company and In Their Words (same band,
   Dave 2026-09-06): container-wide, on the divider token, at the seam's
   midpoint with 72px of air above (In Good Company's bottom padding) and
   72 below */
.quotes__inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding-top: 72px;
  border-top: 1px solid var(--divider);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 48px;
}

.quotes__header {
  max-width: 960px; /* headline on one line at 44px (771px); the sub keeps a 680px measure */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
}

.quotes__eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.quotes__headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.quotes__sub {
  margin: -8px 0 0;
  max-width: 680px;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
}

.quotes__carousel {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* 3 cards in the container on desktop (2 + peek from 1025 to 1299 and on
   tablet, 1 + peek on phones: see the shared breakpoints) */
.quotes__track { --car-cols: 3; }

@media (min-width: 1025px) and (max-width: 1299px) {
  .quotes__track { --car-cols: 2; }
}

.quotes__item { display: flex; }

/* Card: the Impact card's surface (bg-surface, neutral-700 stroke, 8px
   radius) with the case-study quote card's anatomy (glyph, quote, who).
   The quote is the dominant element; the who block pins to the bottom so
   attributions line up across a row. Not a link. */
.quote-card {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  margin: 0;
  padding: 24px;
  border: 1px solid var(--neutral-700);
  border-radius: 8px;
  background: var(--bg-surface);
}

/* Product identifier, the site's eyebrow voice in the muted colour */
.quote-card__project {
  margin: 0 0 16px;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* FA "quote-left" glyph, as on the case studies, in the section purple */
.quote-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-bottom: 12px;
  color: var(--violet-300);
}

.quote-card__icon svg {
  width: 32px;
  height: 32px;
}

.quote-card__text {
  margin: 0;
  padding: 0;
}

.quote-card__text p {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 400;
  line-height: 1.5;
  color: var(--fg-default);
  text-wrap: pretty;
}

.quote-card__who {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: auto;
  padding-top: 24px;
}

.quote-card__name {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 1.56px; /* the case-study quote label style */
  text-transform: uppercase;
  color: var(--fg-default);
}

.quote-card__role {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--fg-muted);
}

:root[data-theme='light'] .quote-card__icon { color: var(--primary-default); }

.select-work {
  padding: 72px 48px; /* between Case Studies and In Their Words: 144px seams both sides */
  scroll-margin-top: var(--nav-height);
}

.sw-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 48px;
}

.sw-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  max-width: 680px;
}

.sw-header__eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.sw-header__headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.sw-header__sub {
  margin: -8px 0 0;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
}

/* Tablet / phone only: "6 Selected Projects" under the sub (main.js keeps
   the number equal to the tile count) */
.sw-header__count {
  display: none;
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* --- Tile grid ------------------------------------------------------------
   The same .sw-grid is the shared .carousel-track; on desktop the track
   rules are overridden back to the 3 x 2 grid (six pieces visible at once,
   Dave 2026-09-06) and the arrows are hidden. At 1024 and below the
   carousel rules stand: 2 tiles + peek on tablet, 1 + peek on phones. */
.sw-carousel {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

@media (min-width: 1025px) {
  .sw-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 24px;
    width: 100%;
    margin: 0;
    padding: 0;
    overflow: visible;
    scroll-snap-type: none;
    cursor: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .sw-grid > * { flex: none; }

  .sw-carousel__footer { display: none; }
}

@media (max-width: 1024px) {
  .sw-header__count { display: block; }
}

/* Tiles are <button>s while the work is unpublished, so reset the UA
   button chrome (border, font, centering) back to the card look */
.sw-tile {
  --pill-offset: 48px; /* pill height + gap; how far the text slides up */
  --fg-default: #ffffff; /* text sits on a darkened image in both themes */
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 300px;
  padding: 12px;
  border: 0;
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg-surface);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  filter: drop-shadow(0 16px 8px rgba(0, 0, 0, 0.1));
}

/* Live tiles link straight to their page: no Coming-soon badge, but they
   keep the same View-work rollover as every other tile (Dave 2026-08-25) */

.sw-tile__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.sw-tile__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0) 65%, rgba(0, 0, 0, 0.6) 100%);
}

.sw-tile__content {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.sw-tile__client {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--fg-default);
}

.sw-tile__title {
  margin: 0;
  font-size: 1.625rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--fg-default);
}

/* "Coming soon" badge — pinned to the tile's top-right, above image + scrim */
.sw-tile__badge {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 4px 10px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 64px;
  background: rgba(30, 12, 50, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--fg-default);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  line-height: 1.5;
  white-space: nowrap;
}

/* "View work" pill — live tiles roll over fully branded: amber text +
   arrow at 100% on an amber 25% oval; coming-soon tiles stay greyed on
   the white glass (Dave 2026-08-26) */
.sw-tile__pill {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  border-radius: 24px;
  background: rgba(255, 194, 10, 0.25);
  color: #ffc20a;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
}

/* unpublished tiles: greyed-out rollover */
.sw-tile[data-soon] .sw-tile__pill {
  background: rgba(255, 255, 255, 0.25);
  color: #ded7e6;
}

.sw-tile__pill svg {
  width: 16px;
  height: 16px;
}

/* Desktop: text sits at the tile bottom; on hover it slides up and the
   pill fades into the space below it */
@media (hover: hover) and (pointer: fine) {
  .sw-tile__content {
    transition: transform 300ms var(--ease-out);
  }

  .sw-tile__pill {
    opacity: 0;
    transition: opacity 200ms ease-out;
  }

  .sw-tile:hover .sw-tile__content,
  .sw-tile:focus-visible .sw-tile__content {
    transform: translateY(calc(-1 * var(--pill-offset)));
  }

  .sw-tile:hover .sw-tile__pill,
  .sw-tile:focus-visible .sw-tile__pill {
    opacity: 1;
  }
}

/* Touch: pill always visible, so the text permanently sits above it */
@media (hover: none) {
  .sw-tile__content {
    transform: translateY(calc(-1 * var(--pill-offset)));
  }

  .sw-tile {
    transition: transform 150ms ease, opacity 150ms ease;
  }

  .sw-tile:active {
    transform: scale(0.98);
    opacity: 0.9;
  }
}

/* --- Responsive ------------------------------------------------------------ */
@media (max-width: 1024px) {
  .select-work { padding: 72px 32px; }
  .sw-tile { height: 260px; }
}

@media (max-width: 640px) {
  .select-work { padding: 40px 16px; }
  .sw-inner { gap: 32px; }
  .sw-tile { height: 200px; --pill-offset: 38px; }
  .sw-tile__title { font-size: 1.0625rem; }
  .sw-tile__pill { font-size: 0.8125rem; padding: 6px 12px; }
  .sw-tile__badge { top: 10px; right: 10px; font-size: 0.6875rem; padding: 3px 8px; }
  .sw-header__sub { font-size: 1.0625rem; }
}

/* Phone widths: with no hover, badge + client + title + pill all stacked on
   the image at once cover most of it. Keep only the title (and the badge
   where it applies) — the whole tile is the tap target, so the pill's
   affordance is redundant, and the existing bottom scrim keeps the title
   readable. Must stay after the (hover: none) block: it undoes that
   block's permanent pill-offset translate. */
@media (max-width: 480px) {
  .sw-tile__pill,
  .sw-tile__client { display: none; }
  .sw-tile__content { transform: none; }
}

/* ==========================================================================
   About + Skills section
   ========================================================================== */
.about {
  padding: 72px 48px; /* after In Good Company now: 144px seam above, 144 to Contact */
  scroll-margin-top: var(--nav-height);
}

.about-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 144px;
}

/* --- About: image left, details right ------------------------------------- */
.about-main {
  display: grid;
  /* Desktop is a 5:7 split (Dave 2026-09-07): the four-paragraph bio needs
     the wider column, and the narrower portrait stays the column's height. */
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: 48px;
  align-items: stretch;
}

.about-media {
  overflow: hidden;
  border-radius: 12px;
  box-shadow: 0 16px 16px rgba(0, 0, 0, 0.1);
}

.about-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

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

.about-eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.about-headline {
  margin: 0;
  text-wrap: pretty; /* no orphans/widows in the About block (Dave 2026-09-07) */
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.about-sub {
  margin: -8px 0 0;
  text-wrap: pretty;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
}

.about-bio {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 20px;
}

.about-bio p {
  margin: 0;
  text-wrap: pretty;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--fg-subtle);
}

.about-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 20px;
}

/* Both About buttons are ghosts of one width: a step narrower than the
   180px site default, and equal because neither label fills 156px
   (Dave 2026-09-07). The phone block below lets them share the row. */
.about-ctas .btn { min-width: 156px; }

/* --- Skills ---------------------------------------------------------------- */
.skills {
  display: flex;
  flex-direction: column;
  gap: 64px;
  scroll-margin-top: var(--nav-height);
}

.skills-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  max-width: 720px;
  margin: 0 auto;
}

/* Four balanced columns on desktop (Dave 2026-09-06, resume-aligned
   categories), two on tablet, one on phones (see the responsive blocks) */
.skills-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 32px;
}

.skill-card__name {
  margin: 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--divider-strong);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-subtle);
}

.skill-card ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.skill-card li {
  padding: 8px 0;
  border-bottom: 1px solid var(--divider);
  font-size: 1rem;
  line-height: 1.4;
  color: var(--fg-subtle);
}

/* --- Responsive ------------------------------------------------------------ */
@media (max-width: 1024px) {
  .about { padding: 72px 32px 96px; }
  .about-inner { gap: 144px; }

  /* Single column: image on top, details below.
     Figma tablet frame: full-width x 362px (wide rectangle) */
  .about-main { grid-template-columns: 1fr; }
  .about-media { height: 362px; }
  /* Letterbox the square portrait so the whole face fits the 362px band,
     flush to the left edge. transform: none disables the parallax zoom
     here — it would re-crop the contained image; main.js skips the band
     in this range. */
  .about-media {
    position: relative;
    background: #000;
  }
  .about-media img {
    object-fit: contain;
    object-position: left center;
    transform: none;
  }

  /* Black veil over the empty right side, fading out leftward: solid to
     the 50% mark, gone by 70%, so it clears the face before the nose. */
  .about-media::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to left, #000 50%, rgba(0, 0, 0, 0) 70%);
    pointer-events: none;
  }

  .skills-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 32px;
  }
}

@media (max-width: 640px) {
  .about { padding: 40px 16px 96px; }
  .about-inner { gap: 96px; }
  .about-sub { font-size: 1.0625rem; }

  /* Figma mobile frame: full-width square */
  .about-media { height: auto; aspect-ratio: 1 / 1; }
  /* Square crop again: undo the tablet band's letterbox (contain, flush-left,
     black veil and no transform all cascade down). No parallax on phones
     (main.js skips ≤1024 too), so no scale-up bleed — the square portrait
     sits static at its natural crop. */
  .about-media { background: none; }
  .about-media::after { content: none; }
  .about-media img {
    object-fit: cover;
    object-position: 50% 50%;
    transform: none;
  }
  .skills-grid { grid-template-columns: 1fr; }
  .about-ctas { width: 100%; }
  /* max-content keeps each button at least as wide as its label, so
     "Download Resume" never breaks across two lines; the pair sits side by
     side while both fit and wraps to a stacked full-width pair when it
     doesn't (roughly below 430px) */
  .about-ctas .btn {
    flex: 1 1 0; /* a zero basis so the pair splits the row equally (Dave 2026-09-07) */
    min-width: max-content;
    white-space: nowrap;
  }
}

/* ==========================================================================
   Trusted By logo cloud
   ========================================================================== */
/* Top padding completes the 144px section rhythm against .about's 72px
   bottom padding; the larger bottom padding is the content shell's closing
   inset, giving the footer room to read as a separate block */
.trusted {
  padding: 72px 48px; /* first half of the tinted band; In Their Words follows under the hairline */
  scroll-margin-top: var(--nav-height);
}

.trusted-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 64px;
}

.trusted-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  max-width: 960px; /* headline on one line at 44px (927px); the sub keeps its 720px measure */
  margin: 0 auto;
}

.trusted-header .about-sub { max-width: 720px; }

/* In Good Company headline breaks after "Dave" by design (Dave 2026-09-06);
   the forced break stands down on phones, where the measure wraps it anyway */
@media (max-width: 640px) {
  .about-headline__break { display: none; }
}

/* --- Marquee: two identical groups slide left; when the first group has
   fully exited (-50% of the track) the loop restarts invisibly ----------- */
.logo-marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}

.logo-marquee__track {
  display: flex;
  width: max-content;
  will-change: transform;
}

/* Only scroll once JS has cloned the group; see main.js */
.logo-marquee.is-looping .logo-marquee__track {
  animation: logo-marquee 45s linear infinite;
}

/* Draggable ticker (every device): main.js drives the transform instead
   (same speed, but a finger or mouse can drag the strip); the keyframe
   animation would fight it. pan-y leaves vertical page scrolling native
   while we own horizontal. The grab cursor only shows for a mouse. */
.logo-marquee.is-drag-ticker {
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.logo-marquee.is-drag-ticker .logo-marquee__track { animation: none; }
.logo-marquee.is-drag-ticker img { -webkit-user-drag: none; }
@media (hover: hover) and (pointer: fine) {
  .logo-marquee.is-drag-ticker { cursor: grab; }
  .logo-marquee.is-drag-ticker.is-dragging { cursor: grabbing; }
}

.logo-marquee:hover .logo-marquee__track {
  animation-play-state: paused;
}

.logo-marquee__group {
  display: flex;
  align-items: center;
  gap: 72px;
  padding: 0 36px;
  margin: 0;
  list-style: none;
}

/* Grayscale always, in both themes. Works on SVG and PNG alike.

   grayscale() rather than brightness(0), because a flat silhouette erases
   any detail inside the mark: knockouts fill in and two-part logos merge
   into a blob. Grayscale keeps the tonal separation, so a knocked-out
   letterform still reads.

   Most logos ship as dark art for light backgrounds, so dark mode adds
   invert() to flip them light while keeping those internal relationships
   intact (a dark mark with a white knockout becomes a light mark with a
   dark knockout, which is the same drawing). Light mode needs no flip and
   uses opacity to settle the art at mid gray without crushing blacks. */
.logo-marquee img {
  display: block;
  /* Per-logo optical sizing: square marks need more height than wide
     wordmarks to carry equal weight. Override with style="--logo-h: 54px"
     on the <li>. */
  /* --logo-scale bumps every per-logo height together (1.2 = "a little
     bigger" on desktop and tablet, Dave 2026-09-06) so the optical balance
     between marks holds; phones stay at 1 (see the <=640 block) */
  height: calc(var(--logo-h, 44px) * var(--logo-scale, 1.2));
  width: auto;
  filter: grayscale(1) invert(1) brightness(1.08);
  opacity: 0.72;
}

:root[data-theme='light'] .logo-marquee img {
  filter: grayscale(1);
  opacity: 0.55;
}

/* Escape hatch for artwork that is already white/light: it needs the
   flip in light mode instead of dark. Set data-art="light" on the img */
.logo-marquee img[data-art='light'] {
  filter: grayscale(1);
}

:root[data-theme='light'] .logo-marquee img[data-art='light'] {
  filter: grayscale(1) invert(1) brightness(0.92);
}

/* Empty slot: declared in markup, or applied by main.js when a slot's
   file cannot be found in any supported format */
.logo-marquee li.is-empty {
  display: grid;
  place-items: center;
  width: 160px;
  height: 44px;
  border: 2px dashed var(--divider-strong);
  border-radius: 8px;
}

.logo-marquee li.is-empty img { display: none; }

.logo-marquee li.is-empty::after {
  content: 'SLOT ' attr(data-slot);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 2.5px;
  color: var(--fg-subtle);
}

@keyframes logo-marquee {
  to { transform: translateX(-50%); }
}

@media (max-width: 1024px) {
  .trusted { padding: 72px 32px; }
  .quotes { padding: 0 32px 72px; }
}

@media (max-width: 640px) {
  /* Mobile rhythm: 40 + 40 = 80px seams, like Case Studies -> Select Work */
  .trusted { padding: 40px 16px; }
  .trusted-inner { gap: 40px; }
  .quotes { padding: 0 16px 40px; }
  .quotes__inner { gap: 32px; padding-top: 40px; }
  .logo-marquee img { height: 36px; } /* phones keep the original size */
  .logo-marquee__group { gap: 48px; padding: 0 24px; }
}

/* ==========================================================================
   Contact footer
   ========================================================================== */
.contact {
  padding: 72px 48px;
  background: var(--bg-default);
  scroll-margin-top: var(--nav-height);
  /* The footer stays dark in both themes */
  --bg-default: #0e011c;
  --bg-surface: #1e0c32;
  --bg-surface-2: #3d3350;
  --fg-default: #ffffff;
  --fg-muted: #b3a0c8;
  --fg-subtle: #978aaa;
  --primary-default: #8200ff;
  --accent: #ffc20a;
  --accent-contrast: #1e0c32;
  --input-bg: rgba(0, 0, 0, 0.2);
  --ghost-border: rgba(255, 255, 255, 0.5); /* ghost strokes stay the muted white in light mode too (Dave 2026-09-08) */
  --divider: rgba(151, 138, 170, 0.25);
}


.contact-inner {
  max-width: 720px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
}

/* Footer logo doubles as a home link (fade-exit on case pages, scroll-to-top
   on the home page); the margin rides on the link so the hit area stays the
   logo itself */
.contact__logo-link {
  display: block;
  line-height: 0;
  margin-bottom: 24px;
}

.contact__logo {
  width: 53px;
  height: 64px;
}

.contact__eyebrow {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 1.56px;
  text-transform: uppercase;
  color: var(--accent);
}

.contact__headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.8px;
  color: var(--fg-default);
}

.contact__desc {
  margin: -8px 0 0;
  max-width: 720px; /* room to break naturally (was 620) */
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-muted);
  orphans: 2;
  widows: 2;
  /* orphans/widows only bind in fragmented contexts (print/columns);
     text-wrap: pretty is what stops a lone word on the last line here */
  text-wrap: pretty;
}

/* --- Email box with copy button ------------------------------------------- */
.contact-email {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  width: 300px;
  max-width: 100%;
  margin-top: 24px;
  padding: 0 12px;
  border: 2px solid var(--primary-default);
  border-radius: 4px;
  background: var(--input-bg);
}

.contact-email input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--fg-default);
  font-family: var(--font-body);
  font-size: 1rem;
  outline: none;
}

.contact-email__copy {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border: 0;
  background: transparent;
  color: var(--fg-default);
  transition: color 200ms ease;
}

.contact-email__copy:hover { color: var(--accent); }

.contact-email__copy svg {
  width: 18px;
  height: 18px;
}

.contact-email__toast {
  position: absolute;
  top: -38px;
  left: 50%;
  transform: translateX(-50%) translateY(6px);
  padding: 4px 12px;
  border-radius: 6px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease, transform 200ms ease;
}

.contact-email__toast.is-shown {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* --- Email group (Dave 2026-09-08) ------------------------------------------
   The copy box plus a separate "Email Dave" mailto button beside it: copy for
   webmail users, the button for anyone with a configured mail client. The
   group carries the box's old 24px top margin; the ghost button matches the
   box's 44px height. Phones stack the two full width (<=640 block). */
.contact-email-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 24px;
  max-width: 100%;
}

.contact-email-group .contact-email { margin-top: 0; }

.contact-email__mail {
  gap: 8px;
  min-width: 0;
  padding: 0 20px;
}

.contact-email__mail svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

/* "Copy email" tooltip above the copy icon: the nav theme toggle's tooltip,
   hung upward. Hover (fine pointers) and keyboard focus show it; it hides
   while the Copied toast is up so the two never stack. */
.contact-email__copy { position: relative; }

.contact-email__copy[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  right: -4px;
  padding: 7px 10px;
  border-radius: 6px;
  background: #0e011c;
  color: #ffffff;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  box-shadow: 0 4px 12px rgba(14, 1, 28, 0.35);
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .contact-email__copy:hover::after {
    opacity: 1;
    transform: translateY(0);
    transition-delay: 300ms;
  }
}

.contact-email__copy:focus-visible::after {
  opacity: 1;
  transform: translateY(0);
}

.contact-email.is-copied .contact-email__copy::after { opacity: 0; }

.contact__note {
  margin: 16px 0 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--fg-muted);
}

/* --- Social links + resume -------------------------------------------------- */
.contact-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 24px;
  margin-top: 16px;
}

.contact-social {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: var(--primary-default);
  color: var(--primary-contrast);
  transition: transform 200ms var(--ease-out), background 200ms ease;
}

.contact-social:hover {
  transform: scale(1.05);
  background: #9226ff;
}

.contact-social svg {
  width: 20px;
  height: 20px;
}

/* Footer hierarchy (Dave 2026-09-08): Email Dave is the one primary purple
   button; the social circles and Download Resume are secondary ghost
   buttons, a muted white stroke with bright white icons and text. Scoped to
   the footer so the contact panel's purple discs and rows are untouched. */
.contact .contact-social {
  border: 1px solid var(--ghost-border);
  background: transparent;
  color: var(--fg-default);
  transition: transform 200ms var(--ease-out), border-color 250ms ease;
}

.contact .contact-social:hover,
.contact .contact-social:focus-visible {
  background: transparent;
  border-color: var(--fg-default);
}

.contact__copyright {
  margin: 32px 0 0;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.5;
  color: var(--fg-muted);
}

/* Privacy policy link on the copyright line (every page) */
.contact__legal {
  margin-left: 8px;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color 200ms ease;
}

.contact__legal:hover,
.contact__legal:focus-visible { color: var(--accent); }

@media (max-width: 640px) {
  .contact { padding: 72px 16px; }
  .contact__desc { font-size: 1.0625rem; }
  .contact-email { width: 100%; }
  .contact-email-group {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
  }
  .contact-email__mail { width: 100%; }
  /* Column direction turns the panel box's 220px flex-basis into a height,
     so reset it here (it stretched to 440px tall on phones, 2026-09-08) */
  .mobile-menu--contact .contact-email-group .contact-email { flex: 0 0 auto; }
  /* Contact panel: the address has 278px in the phone-width panel, so the
     input steps down a size to show every character */
  .mobile-menu--contact .contact-email input { font-size: 0.9375rem; }
  .contact-actions .btn { width: 100%; }
}

/* ==========================================================================
   Copyright tooltip — shown near the cursor when right-clicking an image.
   pointer-events: none so it never blocks the browser context menu.
   ========================================================================== */
.copyright-tip {
  position: fixed;
  /* above everything, including the case-study gallery lightbox (500) */
  z-index: 600;
  max-width: 320px;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid rgba(130, 0, 255, 0.55); /* purple accent */
  background: rgba(30, 12, 50, 0.92);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #ffffff;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.45;
  box-shadow: 0 12px 32px rgba(14, 1, 28, 0.35);
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 250ms ease, transform 250ms ease;
}

.copyright-tip.is-shown {
  opacity: 1;
  transform: translateY(0);
}

/* ==========================================================================
   Site dialog shell (.resume-modal__*). Named for the resume format chooser
   it was built for; that chooser is gone (one resume, direct download), and
   the shell now only dresses the Select Work "coming soon" dialog below.
   ========================================================================== */
.resume-modal {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.resume-modal[hidden] { display: none; }

.resume-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(14, 1, 28, 0.6);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 200ms ease-out;
}

.resume-modal__card {
  position: relative;
  width: 100%;
  max-width: 480px;
  padding: 48px 40px 40px;
  background: var(--bg-surface);
  border: 1px solid var(--neutral-700);
  border-radius: 12px;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 200ms ease-out, transform 200ms ease-out;
}

.resume-modal.is-open .resume-modal__backdrop { opacity: 1; }

.resume-modal.is-open .resume-modal__card {
  opacity: 1;
  transform: scale(1);
}

.resume-modal__close {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--fg-default);
  transition: color 200ms ease;
}

.resume-modal__close:hover { color: var(--accent); }

.resume-modal__close svg {
  width: 20px;
  height: 20px;
}

.resume-modal__title {
  margin: 0 0 28px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.625rem;
  line-height: 1.2;
  color: var(--fg-default);
  text-align: center;
}

.resume-modal__options {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.resume-modal__option {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.resume-modal__option .btn { width: 100%; }

.resume-modal__hint {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--fg-subtle);
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .resume-modal__card {
    transform: none;
    transition: opacity 200ms ease;
  }
}

/* --- Select Work "coming soon" variant ------------------------------------
   Shares the resume modal's shell; only the stacked eyebrow/title/date and
   the single dismiss button differ */
.soon-modal__eyebrow {
  margin: 0 0 8px;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
}

.soon-modal__eyebrow[hidden] { display: none; }

.soon-modal__title { margin: 0 0 12px; }

.soon-modal__ok {
  width: 100%;
  margin-top: 28px;
}

/* ==========================================================================
   Slide controls (Dave 2026-09-07): the hero's caption + pause + progress
   ring, plus prev / next arrows, as one glass capsule pinned to a corner
   of a carousel (homepage case study cards, the three case study heroes).
   Built by slide-controls.js. The buttons reuse the hero's .hero__pause
   rules; the capsule pins the dark-band tokens so it reads white-on-dark
   in both themes. The buttons anchor to the outer edge (order: pause,
   prev, next, then the caption growing inward), so captions can change
   length without moving them. Desktop and tablet only.
   ========================================================================== */
.slide-controls {
  --fg-default: #ffffff;
  --neutral-200: #ded7e6;
  --divider-strong: rgba(255, 255, 255, 0.35);
  position: absolute;
  bottom: 20px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 8px;
  box-sizing: border-box;
  max-width: calc(100% - 40px);
  height: 44px;
  padding: 0 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 22px;
  background: var(--hero-from);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  backdrop-filter: blur(12px) saturate(1.2);
  color: var(--fg-default);
}

.slide-controls--left { left: 20px; }
.slide-controls--right { right: 20px; }

/* Visual order: buttons at the pinned edge, caption growing away from them */
.slide-controls__pause { order: 0; }
.slide-controls__prev { order: 1; }
.slide-controls__next { order: 2; }
.slide-controls__caption { order: 3; }
.slide-controls--right .slide-controls__caption { order: -1; }
.slide-controls--right .slide-controls__pause { order: 3; }

.slide-controls__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--divider-strong);
  border-radius: 50%;
  background: transparent;
  color: var(--neutral-200);
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease;
}

.slide-controls__arrow:hover {
  color: var(--fg-default);
  border-color: var(--fg-default);
}

.slide-controls__arrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.slide-controls__caption {
  margin: 0;
  min-width: 0;
  padding: 0 6px 0 2px;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--fg-default);
  opacity: 0.9;
}

.slide-controls--right .slide-controls__caption { padding: 0 2px 0 6px; }
.slide-controls__caption[hidden] { display: none; }

/* Case study heroes (Dave 2026-09-07): hosted on the hero itself (a
   sticky, so positioned, box) and CENTRED, directly above the floating
   page nav, which is a fixed 52px bar 24px off the viewport bottom; at
   desktop the hero fills the viewport, so 96px up (24 + 52 + 20) keeps
   the two apart. Buttons first, caption growing right; a min-width so
   short captions do not shrink the capsule between slides. Anchored to
   the hero, so it scrolls away with it while the page nav stays. */
.csp-hero > .slide-controls,
.slide-controls--center {
  left: 50%;
  right: auto;
  bottom: 96px;
  transform: translateX(-50%);
  min-width: 320px;
  max-width: calc(100% - 48px);
}

.slide-controls--center .slide-controls__caption { flex: 1 1 auto; }

/* Tablet: the hero stacks with the image on top, so the capsule sits
   inside the image, 16px above its bottom edge (the hosts write the
   image height into --csp-media-h) */
@media (min-width: 641px) and (max-width: 1024px) {
  .csp-hero > .slide-controls {
    top: calc(var(--csp-media-h, 0px) - 60px);
    bottom: auto;
  }
}

/* Homepage cards: the stage wraps the card link and hosts the controls
   outside it; it carries the stacking recession transform (main.js) so
   the controls recede with the card */
.cs-card__stage {
  position: relative;
  will-change: transform, filter;
  transform-origin: center top;
}

/* Tablet (641 to 1024): the card stacks with its 16:10 image on top, so
   the capsule pins to the IMAGE's bottom-LEFT corner on every card,
   never over the copy below (Dave 2026-09-07). An absolutely positioned
   box resolves percentage margins against its containing block's WIDTH,
   so margin-top 62.5% is exactly the image's height; 60px = the 44px
   capsule + a 16px inset. Buttons first (pause, prev, next), caption
   last growing rightward, whichever side the desktop layout uses. */
@media (min-width: 641px) and (max-width: 1024px) {
  .cs-card__stage > .slide-controls,
  .cs-card__stage > .slide-controls--left,
  .cs-card__stage > .slide-controls--right {
    top: 0;
    bottom: auto;
    left: 16px;
    right: auto;
    margin-top: calc(62.5% - 60px);
  }

  .cs-card__stage > .slide-controls .slide-controls__pause { order: 0; }
  .cs-card__stage > .slide-controls .slide-controls__prev { order: 1; }
  .cs-card__stage > .slide-controls .slide-controls__next { order: 2; }
  .cs-card__stage > .slide-controls .slide-controls__caption {
    order: 3;
    padding: 0 6px 0 2px;
  }
}

@media (max-width: 640px) {
  /* Phones: the slideshow runs as before, no controls, no caption */
  .slide-controls { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .slide-controls__arrow,
  .slide-controls__caption { transition: none; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 1024px) {
  .site-nav__links,
  .site-nav__actions > .nav-link--contact { display: none; }

  .impact { padding: 96px 32px 72px; }
  .nav-toggle { display: flex; }

  /* Carousels at 1024 and below: every track re-keys to the section's
     32px gutter, 16px gaps and a 48px peek of the next card; each
     section sets its own card count (Results 3, In Their Words 2,
     Select Work 2); touch swipe does the rest */
  .carousel-track {
    --car-gutter: 32px;
    --car-gap: 16px;
    --car-peek: 48px;
  }
  .impact__grid { --car-cols: 3; }
  .quotes__track,
  .sw-grid { --car-cols: 2; }
  /* Contact drops out, so the actions capsule tightens into a 56px glass
     disc around the toggle, pulled right to pair with the hamburger disc.
     The negative margin trims the 32px flex gap between the two. */
  .theme-toggle__icon { width: 26px; height: 26px; }
  .site-nav__actions { margin-left: auto; margin-right: -16px; padding: 5px; }
  .site-nav__inner { padding: 0 32px; }

  /* Homepage header: Contact stays, Menu keeps its text label, and the
     capsule sits flush right (no hamburger disc to pair with) */
  .site-nav--menu .site-nav__actions > .nav-link--contact { display: inline-flex; }
  .site-nav--menu .site-nav__actions { margin-right: 0; }
  .site-nav--menu .site-nav__inner { padding: 0 20px; }

  /* Hero shares the nav's 32px gutters from tablet down, so the name, copy
     and CTA row start on the logo tile's left edge and the scroll hint ends
     on the hamburger's right edge. The actions row spans the full width to
     put the hint there (the <=768 block already does this). */
  .hero__content { padding-left: 32px; padding-right: 32px; }
  .hero__actions { width: 100%; justify-content: space-between; }
  /* Tablet and phone are the live hero: one CTA, and the stat band is
     desktop-only (the Impact section below carries the eight-card
     evidence at these widths, 2 x 4 via the grid rule above) */
  .hero__ctas .btn--ghost { display: none; }
  .stat-cards--hero { display: none; }

  /* Logo drops back to the 56px the toggle and hamburger discs use, so the
     three controls stay a matched set on small screens. The radius scales
     with it (13/56 matches desktop's 16/68) to keep the same corner. */
  .site-nav__logo { width: 56px; height: 56px; border-radius: 13px; }
  .site-nav__logo img { height: 30px; }
  .site-nav.is-over-hero .site-nav__logo img { height: 36px; } /* the bare homepage-hero step-up, scaled to the smaller tile */
}

/* Small tablets (641 to 767): three-up would leave ~170 to 210px Impact
   cards, too narrow for the 25px metric, so two cards plus the peek */
@media (min-width: 641px) and (max-width: 767px) {
  .impact__grid { --car-cols: 2; }
}

/* Tablet hero (Figma tablet frame, <=768): stacked column — single-line
   72px name, copy below, then buttons left / scroll hint right. Above
   this the fluid 1920-768 desktop layout applies. */
@media (max-width: 768px) {
  .hero__content {
    flex-direction: column;
    align-items: stretch;
    padding: calc(var(--nav-height) + 20px + env(safe-area-inset-top, 0px)) 32px 36px;
    gap: 16px;
  }

  .hero__main {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 16px;
    width: 100%;
    min-width: 0;
    flex: 0 0 auto; /* don't stretch vertically in the stacked column */
  }

  .hero__name {
    font-size: 4.5rem;
    letter-spacing: 1.3px;
    white-space: normal;
    width: 100%;
  }

  .hero__name-break { display: none; }

  .hero__copy {
    width: 100%;
    margin-bottom: 0;
  }

  .hero__desc { max-width: 66ch; }

  /* No orphans in the stacked hero copy: the subhead balances into two even
     lines instead of dropping "Strategist" alone, and the body copy pulls its
     last word up. The &nbsp; pairs in the markup cover browsers without
     text-wrap support. */
  .hero__sub { text-wrap: balance; }
  .hero__desc { text-wrap: balance; }

  .hero__actions {
    width: 100%;
    justify-content: space-between;
    margin-top: 8px;
  }

  .hero__gradient {
    background: linear-gradient(180deg, var(--hero-to) 50%, var(--bg-default) 79.5%);
  }
}

/* Tablet hero, 641 to 1024 (Dave 2026-09-06): one-line 64px name, the
   desktop body type up to 800px wide, both buttons, and the still caption
   beside an enlarged scroll arrow. Grid: row 1 = eyebrow / name / copy,
   row 2 = buttons | caption | arrow. .hero__actions dissolves so its two
   children can take their own cells. Between 641 and 767 the row has no
   room for the caption, so it takes its own line between the copy and
   the buttons. */
@media (min-width: 641px) and (max-width: 1024px) {
  .hero__content {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    column-gap: 16px;
    row-gap: 24px;
    align-items: end;
  }

  .hero__main {
    grid-column: 1 / -1;
    grid-row: 1;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    width: 100%;
    min-width: 0;
  }

  .hero__sub { width: 100%; }

  .hero__name {
    width: auto;
    font-size: 4rem;
    line-height: 1;
    letter-spacing: 0.018em;
    white-space: nowrap;
  }

  .hero__name-break { display: none; }

  .hero__copy {
    width: 100%;
    max-width: 800px;
    margin: 8px 0 0;
    gap: 0;
  }

  .hero__desc {
    max-width: 66ch; /* three balanced lines here too (Dave 2026-09-07; was 800px) */
    text-wrap: balance;
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--neutral-200);
  }

  .hero__actions { display: contents; }

  .hero__ctas {
    grid-column: 1;
    grid-row: 2;
    gap: var(--button-gap);
  }

  .hero__ctas .btn--ghost { display: inline-flex; }

  .hero__caption {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    grid-column: 2;
    grid-row: 2;
    justify-self: end;
    align-self: center;
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    text-align: right;
  }

  /* "More" + a 28px arrow (Dave 2026-09-06), 44px tall, with a 24px
     divider line on its left separating it from the caption */
  .hero__scroll {
    grid-column: 3;
    grid-row: 2;
    align-self: center;
    justify-content: center;
    height: 44px;
    gap: 12px;
  }

  .hero__scroll::before {
    content: '';
    width: 1px;
    height: 24px;
    margin-right: 4px; /* + the 12px flex gap = 16px, matching the 16px column gap on the caption side */
    background: var(--divider-strong);
  }

  .hero__scroll span { display: inline-block; }

  .hero__scroll-arrow {
    width: 28px;
    height: 28px;
  }
}

/* 768 to 899: the caption breaks company over product beside the arrow
   (the longest caption is ~300px and only ~250px is free at 768) */
@media (min-width: 768px) and (max-width: 899px) {
  .hero__caption-client,
  .hero__caption-product { display: block; }
  .hero__caption-sep { display: none; }
}

/* 641 to 767: caption on its own line, right-aligned, between the copy
   and the button row */
@media (min-width: 641px) and (max-width: 767px) {
  .hero__content { row-gap: 16px; }
  .hero__caption { grid-column: 1 / -1; grid-row: 2; }
  .hero__ctas { grid-row: 3; }
  .hero__scroll { grid-row: 3; }
  .hero__scroll::before { display: none; } /* nothing to divide from: the caption is on its own row */
}

@media (max-width: 640px) {
  :root { --nav-height: 76px; }

  .site-nav__inner { padding: 0 16px; }

  /* Phones keep the same nav readability scrims as desktop/tablet (the
     2026-08 bare-bar experiment left the nav illegible over the hero's
     brighter stills) */

  /* Menu controls at Apple's 44x44 minimum tap target, with the glyphs
     sized down inside so the controls read lighter than the old 56px
     discs. The X-state translate shrinks with the bar gap (5px gap +
     2px bar = 7px from the outer bars to center). */
  .nav-toggle {
    width: 44px;
    height: 44px;
    gap: 5px;
  }

  .nav-toggle__bar { width: 20px; }

  .nav-toggle.is-open .nav-toggle__bar:nth-child(1) { translate: 0 7px; }
  .nav-toggle.is-open .nav-toggle__bar:nth-child(3) { translate: 0 -7px; }

  /* Theme toggle: the button already is the 44px target; the capsule
     tightens to hug it and the icon steps down to match the hamburger */
  .site-nav__actions { padding: 0; }
  .theme-toggle__icon { width: 20px; height: 20px; }

  /* Homepage header on phones: Contact drops out and the Menu label gives
     way to the bars (the accessible name stays "Menu"), so the violet
     button is the 44px icon-only square the open state uses everywhere;
     gutters go to 12px */
  .site-nav--menu .site-nav__inner { padding: 0 12px; }

  /* The open panel's edge would slice the logo tile (Dave 2026-09-07), so
     the logo fades out whole while the panel is open and returns on close */
  .site-nav--menu .site-nav__logo {
    transition: background 400ms ease, border-color 400ms ease,
      backdrop-filter 400ms ease, -webkit-backdrop-filter 400ms ease,
      opacity 150ms ease, visibility 0s;
  }
  body.menu-open .site-nav--menu .site-nav__logo {
    opacity: 0;
    visibility: hidden;
    transition: background 400ms ease, border-color 400ms ease,
      backdrop-filter 400ms ease, -webkit-backdrop-filter 400ms ease,
      opacity 150ms ease, visibility 0s linear 150ms;
  }
  .site-nav--menu .site-nav__actions > .nav-link--contact { display: none; } /* Contact lives in the panel on phones (outranks the tablet block's inline-flex) */
  /* With Menu alone in it the frame has nothing to group, so it goes
     (Dave 2026-09-07); the margin and padding cancel so the button stays put */
  .site-nav--menu .site-nav__actions,
  .site-nav--menu.on-light .site-nav__actions {
    margin: 0;
    padding: 0;
    border-color: transparent;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .site-nav--menu .nav-toggle {
    gap: 0;
    padding: 0 13px;
  }
  .site-nav--menu .nav-toggle__label {
    position: absolute;
    width: 1px;
    height: 1px;
    max-width: none;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /* Figma mobile hero: single-line name sized in vw (see below), copy
     left-aligned, buttons side-by-side at half width, gradient runs to
     solid plum near the bottom. Gutters match the nav's 20px so the
     full-width CTA lines up with the logo and hamburger. */
  /* iOS Safari: the hero spans the LARGE viewport (100lvh) so its plum
     gradient keeps running behind the collapsed URL-bar zone — otherwise
     the bar's blur samples the next (white, in light mode) section. The
     copy still anchors to the SMALL viewport: (100lvh - 100dvh) equals the
     retractable-bar height while the bars are out and collapses to 0 once
     they slide away, so the block rides just above the URL bar in both
     states. vh first as the fallback for browsers without lvh/dvh. The
     env() insets keep the copy clear of the home indicator. */
  .hero {
    min-height: 100vh;
    min-height: 100lvh;
  }

  /* Compressed text block: tighter gaps all the way down (name -> title ->
     copy -> CTA) and a shorter bottom inset to the URL bar, so the stills
     get more unobstructed room above the copy */
  .hero__content {
    padding: calc(var(--nav-height) + 16px + env(safe-area-inset-top, 0px))
      16px calc(24px + env(safe-area-inset-bottom, 0px));
    padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)
      + (100lvh - 100dvh));
    align-items: center;
    gap: 10px;
  }

  .hero__main { gap: 10px; }
  .hero__copy { gap: 8px; }

  /* Gradient raised 120px from the tablet ramp so the compressed text
     block sits on a solidly dark base against the brighter stills. Colors
     are the SAME tokens as the desktop/tablet ramps (transparent #0e011c
     -> bg-default), not the old bg-surface variant. */
  .hero__gradient {
    background: linear-gradient(180deg,
      var(--hero-to) calc(50% - 120px),
      var(--bg-default) calc(90.6% - 120px));
  }

  /* Single line, left-aligned. vw-sized (capped at 2.75rem) so the name
     always fits the 100vw - 32px column: glyph run is ~7.3x the font size
     plus 10px of tracking, and vw is immune to the ~9.4% root font-size
     inflation phones apply to rem — which is what used to push the 2.5rem
     name past the column and wrap it. */
  .hero__name {
    font-size: min(2.75rem, 11.5vw);
    line-height: 1.08;
    letter-spacing: 0.8px;
    white-space: nowrap;
  }

  /* Shortened one-line title ("Senior UX/UI Designer") — fits the column
     at full size even with phone rem inflation, so no vw cap needed */
  .hero__sub-full,
  .hero__desc-full { display: none; }
  .hero__sub-short,
  .hero__desc-short { display: inline; }
  .hero__desc-lead { display: none; }

  .hero__sub {
    font-size: 1.25rem;
    line-height: 1.3;
    white-space: nowrap;
  }

  /* Regular weight so the copy reads a step lighter than the 600 subhead.
     Ragged right by choice; the box itself spans the full gutter width */
  .hero__desc {
    font-size: 0.9375rem;
    font-weight: 400;
    line-height: 1.4;
    color: var(--neutral-200); /* Mist stand-in, same as desktop and tablet (Dave 2026-09-06; was fg-muted, briefly white) */
    text-wrap: balance; /* four even lines on phones (Dave 2026-09-07; was auto) */
  }

  /* Actions ride one row: the CTA takes the full remaining width with the
     bouncing scroll arrow beside it on the right. The word "Scroll" is
     dropped — the arrow alone is the cue (the link keeps its name via
     aria-label), and it still rides the same #impact anchor scroll. */
  .hero__actions {
    flex-direction: row;
    align-items: center;
    gap: 16px;
    margin-top: 16px; /* was 6 (Dave 2026-09-06: more air between the body and the button) */
  }

  .hero__ctas {
    flex: 1;
    min-width: 0;
    flex-wrap: nowrap;
    gap: var(--button-gap);
  }

  .hero__ctas .btn {
    flex: 1;
    min-width: 0;
    padding: 0 8px;
    white-space: nowrap;
  }

  .hero__scroll { display: flex; }

  .hero__scroll span { display: none; }

  /* 44px square tap target, centered on the button's midline */
  .hero__scroll {
    justify-content: center;
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    gap: 0;
  }

  .mobile-menu__link { font-size: 1.75rem; }

  /* Tighter section rhythm on phones: less air above the eyebrow and
     between Impact -> Case Studies -> Select Work (each seam is this
     section's bottom padding + the next one's top padding) */
  .impact { padding: 56px 16px 40px; gap: 64px; }
  .case-studies { padding: 40px 0; }

  /* Carousels on phones: every track re-keys to the 16px gutters (gap =
     gutter, so a snapped card never shows a sliver of the previous one)
     and one card at 84% of the gutter-to-gutter width, so the next card
     peeks in from the right */
  .carousel-track {
    --car-gutter: 16px;
    --car-gap: 16px;
    --car-cols: 1;
    --car-peek: 16%;
  }

  .carousel-track > * { scroll-snap-stop: always; }

  /* The note drops under the arrows, both on the left content edge */
  .impact__source {
    width: 100%;
    margin-left: 0;
    text-align: left;
  }
  .impact__headline { font-size: 2.125rem; line-height: 1.12; letter-spacing: 0.6px; }
  .impact__sub { color: var(--fg-subtle); }

  .cs-stack { padding: 0 16px; gap: 32px; }
  .cs-header { padding: 0 16px; }
  .cs-header__sub { font-size: 1.0625rem; }
  .cs-card__details { padding: 24px; gap: 20px; }

  /* Mobile reads left-aligned: the centered section headers flush left.
     Only the hero (above) and the contact footer stay centered. */
  .impact__header,
  .quotes__header,
  .cs-header,
  .skills-header,
  .trusted-header {
    align-items: flex-start;
    text-align: left;
    width: 100%;
  }

  /* ---- Mobile type scale -------------------------------------------------
     The root scales UP on phones (17.5px, see the top of this file), so every
     rem size lands ~9% larger here than the desktop value suggests — section
     headlines were rendering at 33-37px and section ledes at 22px on a 375px
     screen. These step the copy back down to the scale the .cs-card block
     below already uses (28px title / 16px body), which is the reference for
     the whole site. Sizes are the rendered px at the 17.5px mobile root:

       section headline  1.75rem   -> 30.6px
       section lede      1.0625rem -> 18.6px
       card / stat title 1.625rem  -> 28.4px
       body copy         0.9375rem -> 16.4px

     Headlines also lose a little tracking, which the display face needs once
     it drops below ~32px. */
  .impact__headline,
  .quotes__headline { font-size: 1.75rem; line-height: 1.14; letter-spacing: 0.4px; }

  .cs-header__headline,
  .sw-header__headline,
  .about-headline,
  .contact__headline {
    font-size: 1.75rem;
    line-height: 1.12;
  }

  /* Section ledes settle on one size — .impact__sub was the last one still
     running at the 1.25rem desktop value (21.9px rendered) while every other
     sub had already been stepped to 1.0625rem */
  .impact__sub,
  .quotes__sub { font-size: 1.0625rem; }

  /* Body copy matches .cs-card__desc so the prose in a section and the prose
     in a card read at the same size (.stat-card__label is sized above with
     the 2x2 stats grid instead) */
  .about-bio p { font-size: 0.9375rem; }

  /* Skill card labels sat at the section-lede size and competed with the
     headings above them */
  .skill-card__name { font-size: 1.125rem; }

  /* ---- Slide-out panel on phones (Dave 2026-09-07) ----------------------
     Full width with 16px gutters; the list is the scroll region and the
     settings group is pinned at the bottom (safe-area aware) so it never
     rides away or covers the last item; type steps
     down one notch while every target stays 44px tall (padding, not
     font size, holds the height). */
  .mobile-menu--panel {
    width: 100%;
    box-shadow: none;
  }

  .mobile-menu--panel:not(.mobile-menu--contact) { overflow: hidden; }

  .mobile-menu__head {
    padding-left: 16px;
    padding-right: 16px;
  }

  .mobile-menu--panel .mobile-menu__nav {
    flex: 1 1 auto;
    min-height: 0;
    --menu-gutter: 16px;
    padding: 4px 16px 24px;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }

  .mobile-menu__settings {
    flex-shrink: 0;
    padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    background: var(--bg-surface);
  }

  .mobile-menu__contact-body { padding: 24px 16px calc(20px + env(safe-area-inset-bottom, 0px)); --menu-gutter: 16px; } /* 20 not 24: the panel then fits an 812px phone viewport without a scroll (Dave 2026-09-07) */


  .mobile-menu--panel .mobile-menu__nav > .mobile-menu__item { padding: 0; }

  .mobile-menu--panel .mobile-menu__link {
    display: flex;
    align-items: center;
    width: calc(100% + 2 * var(--menu-gutter, 16px));
    min-height: 52px; /* the 44px target plus the 4px row padding each side, so plain rows match the accordion rows */
    padding-top: 4px;
    padding-bottom: 4px;
    font-size: 1.125rem;
  }

  .mobile-menu--panel .mobile-menu__expand { margin: 0 -10px 0 0; }

  .mobile-menu--panel .mobile-menu__sublink {
    min-height: 44px;
    font-size: 1rem;
  }

  .mobile-menu--panel .mobile-menu__sub-inner { --menu-indent: 12px; }

  /* Menu button on phones: a ghost stroke only, no fill, at roughly a
     quarter of the icon colour (a touch more over the hero stills); the
     8px radius is the button's own (Dave 2026-09-07) */
  .site-nav--menu .nav-toggle,
  .site-nav--menu .nav-toggle:hover,
  .site-nav--menu .nav-toggle:focus-visible {
    background: transparent;
    border-color: rgba(255, 255, 255, 0.3);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .site-nav--menu.is-over-hero .nav-toggle,
  .site-nav--menu.is-over-hero .nav-toggle:hover,
  .site-nav--menu.is-over-hero .nav-toggle:focus-visible { border-color: rgba(255, 255, 255, 0.4); }

  .site-nav--menu.on-light .nav-toggle,
  .site-nav--menu.on-light .nav-toggle:hover,
  .site-nav--menu.on-light .nav-toggle:focus-visible {
    background: transparent;
    border-color: rgba(30, 12, 50, 0.25);
  }

  /* Dark mode (Dave 2026-09-07): the same 6px frost the light on-light
     state carries, tinted with the surface token at 55%, so the white
     icon separates from the hero stills and dense dark content instead
     of competing with them. The ghost stroke stays on top of it. */
  :root[data-theme='dark'] .site-nav--menu .nav-toggle,
  :root[data-theme='dark'] .site-nav--menu .nav-toggle:hover,
  :root[data-theme='dark'] .site-nav--menu .nav-toggle:focus-visible {
    background: color-mix(in srgb, var(--bg-surface) 55%, transparent);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
  }

  /* Testimonial cards on phones (Dave 2026-09-07): a narrower peek, less
     side padding and a fluid body size (14.9px at 320, 16.5 at 375, 18.6
     at 430, down from 19.7) so the measure lands around 34 to 40
     characters per line on 375 and up (about 32 on a 320 screen); the
     attribution scales with it */
  .quotes__track { --car-peek: 6%; }
  .quote-card { padding: 20px 16px; }
  .quote-card__text p { font-size: clamp(0.85rem, 4.4vw, 1.0625rem); }
  .quote-card__name { font-size: 0.75rem; letter-spacing: 1.2px; }
  .quote-card__role { font-size: 0.75rem; }

}

/* Phone-width cards (~480 down): the 640 sizes read oversized and the
   details panel runs taller than the image, so the whole scale steps down
   proportionally — hierarchy intact, card meaningfully shorter */
@media (max-width: 480px) {
  .cs-card__details { padding: 20px; gap: 14px; }
  .cs-card__info { gap: 10px; }
  .cs-card__client { font-size: 0.9375rem; }
  .cs-card__title { font-size: 1.625rem; }
  .cs-card__desc { font-size: 0.9375rem; }
  .cs-card__stats { gap: 6px; }
  .cs-card__stats li { font-size: 0.875rem; line-height: 1.35; }

  /* Headings take one more notch on small phones; body copy stays at
     16.4px — below ~16px iOS starts zooming on focus and the measure gets
     choppy, so this is the floor for prose everywhere on the site */
  .impact__headline,
  .quotes__headline,
  .cs-header__headline,
  .sw-header__headline,
  .about-headline,
  .contact__headline { font-size: 1.625rem; }

  .impact__sub,
  .quotes__sub,
  .cs-header__sub,
  .sw-header__sub,
  .about-sub,
  .contact__desc { font-size: 1rem; }

  /* .stat-card__value keeps the 640 grid size — the 2x2 stat cards are
     already sized for half-width columns and don't step at 480 (same grid
     as the live site) */
  .skill-card__name { font-size: 1.0625rem; }
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .cascade {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .hero__scroll-arrow { animation: none; }

  /* The stat card hover swap becomes instant */
  .stat-card,
  .stat-card__value,
  .stat-card__label,
  .stat-card__hint,
  .stat-card:hover .stat-card__value,
  .stat-card:hover .stat-card__label,
  .stat-card:hover .stat-card__hint,
  .stat-card:focus-visible .stat-card__value,
  .stat-card:focus-visible .stat-card__label,
  .stat-card:focus-visible .stat-card__hint { transition: none; }

  .hero__caption-inner { transition: none; }
  .hero__caption .hero__pause { display: none; } /* the hero sequence is one static frame here; the card / hero slide controls keep theirs (they start paused) */
  .carousel__arrow,
  .impact-card,
  .impact-card__arrow,
  .quote-card { transition: none; }

  .impact__card,
  .cs-item,
  [data-cascade],
  [data-cascade-fade] {
    transition: none;
    opacity: 1;
    transform: none;
  }

  .cs-tooltip { display: none; }

  /* Marquee stands still; the strip stays browsable via horizontal scroll */
  .logo-marquee__track { animation: none; }
  .logo-marquee { overflow-x: auto; }

  .copyright-tip { transition: opacity 200ms ease; transform: none; }

  .site-nav,
  .nav-link,
  .nav-link--contact,
  .nav-link__text,
  .mobile-menu,
  .mobile-menu__item,
  .mobile-menu__link,
  .mobile-menu__sub,
  .mobile-menu__chevron,
  .nav-toggle__bar,
  .nav-toggle,
  .nav-toggle__label,
  .menu-overlay,
  .theme-toggle,
  .theme-toggle::after,
  .theme-toggle__track,
  .theme-toggle__knob { transition: none; }

  /* Homepage header pair: the panel-open slide, fade and label fold stand
     still (these outrank the plain selectors above on specificity) */
  .site-nav--menu .nav-link--contact,
  .site-nav--menu .nav-toggle,
  body.menu-open .site-nav--menu .nav-link--contact,
  .site-nav--menu .site-nav__logo,
  body.menu-open .site-nav--menu .site-nav__logo { transition: none; }

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