/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  --bg: #0A0A0B;
  --bg-elevated: #121214;
  --bg-card: #141416;
  --border: rgba(255, 255, 255, 0.07);
  --border-strong: rgba(255, 255, 255, 0.13);
  --text: #EDEDEC;
  --text-secondary: #A0A0A0;
  /* Spec value #6E6E73 measures 3.90:1 against --bg and fails WCAG AA for the
     small text it carries (11px eyebrows, card domains, captions). Raised to
     the nearest value that clears 4.5:1 against the *lightest* surface it sits
     on, which is --bg-card, not --bg: 4.82:1 on --bg-card, 5.19:1 on --bg. */
  --text-muted: #82828A;
  --accent: #C9A96E;
  /* Two companions to the bronze, tuned to sit at the same optical weight
     on the ground: 8.84, 8.21 and 8.09 against --bg. Assigned by meaning,
     one per engagement, not sprinkled for variety. */
  --accent-slate: #8FAAC4;
  --accent-jade: #7FB09B;
  --accent-dim: rgba(201, 169, 110, 0.13);

  --font-display: "Bricolage Grotesque", "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

  --container: 1120px;
  --measure: 680px;
  --section-gap: 72px;
  --radius: 16px;
  --radius-sm: 10px;
  --radius-pill: 999px;
  --nav-h: 60px;
  /* Two-row header below 900px, so anchor offsets need the extra row. */
  --nav-h-sm: 104px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (min-width: 900px) {
  :root { --section-gap: 120px; }
}

/* ==========================================================================
   Fonts — self hosted, latin subset
   ========================================================================== */

/* Variable file — one download covers the whole 200 to 800 weight range. */
@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-var-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Variable file — one download covers both 400 and 500. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/inter-var-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   Base
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* Grain overlay. feTurbulence as an inline data URI — this is what stops the
   dark ground from banding. Fixed, non-interactive, 2.5% opacity. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.025;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

h1, h2, h3, p, dl, dd, figure, blockquote, ul { margin: 0; }

ul {
  padding: 0;
  list-style: none;
}

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

/* ==========================================================================
   Typography
   ========================================================================== */

h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(3rem, 7.5vw, 5.5rem);
  line-height: 1.0;
  letter-spacing: -0.035em;
  color: var(--text);
  text-wrap: balance;
}

h2 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.875rem, 3.4vw, 2.75rem);
  line-height: 1.12;
  letter-spacing: -0.028em;
  color: var(--text);
  text-wrap: balance;
}

h3 {
  font-family: var(--font-body);
  font-size: 1.125rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--text);
}

p {
  color: var(--text-secondary);
  text-wrap: pretty;
}

.lede {
  font-size: clamp(1.25rem, 2vw, 1.5rem);
  line-height: 1.45;
  letter-spacing: -0.011em;
  color: var(--text);
}

.eyebrow {
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-muted);
}

/* A section label that carries the h2 for document outline purposes but
   reads as an eyebrow. */
.h2-eyebrow {
  font-family: var(--font-body);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-muted);
}

.measure { max-width: var(--measure); }

/* ==========================================================================
   Links, buttons, focus
   ========================================================================== */

a {
  color: var(--text);
  text-decoration: none;
  transition: color 180ms ease, border-color 180ms ease;
}

a:hover { color: var(--accent); }

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

.link-underline {
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: 2px;
}

.link-underline:hover { border-color: var(--accent); }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 0 1.5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1;
  color: var(--text);
  transition: color 180ms ease, border-color 180ms ease, background-color 180ms ease;
}

.btn:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.btn--primary { background: var(--bg-elevated); }

.btn--secondary { border-color: var(--border); }

.skip-link {
  position: absolute;
  left: 1rem;
  top: -6rem;
  z-index: 10000;
  padding: 0.75rem 1rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 0.9375rem;
  font-weight: 500;
}

.skip-link:focus { top: 1rem; }

/* ==========================================================================
   Floating pill navigation
   Near-opaque rather than blurred: a backdrop-filter here would be
   glassmorphism, and it costs a compositing pass on every scroll frame.
   ========================================================================== */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 1rem;
  padding: 0.875rem 1.25rem;
  pointer-events: none;
}

.nav > * { pointer-events: auto; }

@media (min-width: 900px) {
  .nav {
    grid-template-columns: 1fr auto 1fr;
    padding: 1rem 2rem;
  }
}

.nav__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-pill);
}

.nav__mark svg { display: block; }

.nav__pill {
  display: none;
  justify-self: center;
  background: rgba(18, 18, 20, 0.94);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 0.3125rem;
}

@media (min-width: 900px) {
  .nav__pill { display: block; }
}

.nav__pill ul {
  display: flex;
  align-items: center;
  gap: 0.125rem;
}

.nav__pill a {
  display: block;
  padding: 0.5rem 0.9375rem;
  border-radius: var(--radius-pill);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-secondary);
  transition: color 180ms ease, background-color 180ms ease;
}

.nav__pill a:hover { color: var(--text); }

.nav__pill a[aria-current="true"] {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

.nav__cta {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 1.125rem;
  background: rgba(18, 18, 20, 0.94);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1;
  color: var(--text);
  transition: color 180ms ease, border-color 180ms ease;
}

.nav__cta:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* ==========================================================================
   Layout
   ========================================================================== */

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

@media (min-width: 900px) {
  .container { padding-inline: 2.5rem; }
}

.section {
  padding-block: var(--section-gap);
  border-top: 1px solid var(--border);
  scroll-margin-top: calc(var(--nav-h-sm) + 1rem);
}

.section--flush { border-top: 0; }

.section__head { margin-bottom: 3.5rem; }

.section__head .eyebrow { margin-bottom: 1rem; }

.section__head h2 { max-width: 22ch; }

/* The rationed accent hairline — one per section, 40px, nothing heavier. */
/* Each case study carries its own hue: bronze for the oncology work,
   slate for the institutional B2B engagement, jade for the product role.
   Only the rule, the eyebrow and the disclosure take it, so the page still
   reads as one palette rather than three. */
.case-tint { --accent: var(--case-accent, #C9A96E); }

/* The eyebrow names the engagement, so it is the right place to carry the
   hue. .case__index and the disclosure already read from --accent, so
   rebinding the token above tints them with no further rules. */
.case .eyebrow { color: var(--accent); }
.case-tint--slate { --case-accent: var(--accent-slate); }
.case-tint--jade { --case-accent: var(--accent-jade); }

.rule-accent {
  width: 40px;
  height: 1px;
  border: 0;
  margin: 0 0 1.5rem;
  background: var(--accent);
}

/* ==========================================================================
   1. Hero — full height, cinematic
   ========================================================================== */

.hero {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 100svh;
  padding-block: calc(var(--nav-h) + 3rem) var(--section-gap);
  border-top: 0;
}

/* The only glow on the page. Soft-stopped gradient rather than a blur
   filter — same read, no per-frame paint cost. */
.hero::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  /* Clamped to 100%: at 150% this pushed document scrollWidth past the
     viewport on narrow screens, which nudged the fixed nav off-screen. */
  width: min(1400px, 100%);
  height: min(900px, 110%);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: -1;
  background: radial-gradient(
    ellipse 50% 50% at 50% 50%,
    rgba(201, 169, 110, 0.06) 0%,
    rgba(201, 169, 110, 0.032) 34%,
    rgba(201, 169, 110, 0.01) 60%,
    rgba(201, 169, 110, 0) 78%
  );
}

.hero__lede {
  margin-top: 1.75rem;
  max-width: 26ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 2.5rem;
}

/* ==========================================================================
   2. Proof — oversized numerals on an elevated full-bleed band
   ========================================================================== */

/* Full-bleed elevated band. Sits flush against the foot of its section so
   the next section's border-top supplies the closing hairline — otherwise
   the two rules stack into a 2px line. */
.band {
  background: var(--bg-elevated);
  border-top: 1px solid var(--border);
  padding-block: clamp(3rem, 6vw, 4.5rem);
  margin-top: 2.5rem;
}

.section--band { padding-bottom: 0; }

.proof {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem;
}

.numeral {
  display: flex;
  align-items: baseline;
  gap: 0.0625em;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(3.5rem, 8vw, 5.5rem);
  line-height: 0.95;
  letter-spacing: -0.045em;
  color: var(--accent);
}

.numeral__unit {
  font-size: 0.42em;
  letter-spacing: -0.02em;
  color: var(--accent);
}

.proof__label {
  margin-top: 1.25rem;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--text);
}

.proof__context {
  margin-top: 0.5rem;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-muted);
  max-width: 32ch;
}

@media (min-width: 640px) {
  .proof {
    grid-template-columns: repeat(3, 1fr);
    gap: 0;
  }

  .proof__item { padding-inline: 2.5rem; }

  .proof__item:first-child { padding-inline-start: 0; }

  .proof__item:last-child { padding-inline-end: 0; }

  .proof__item + .proof__item { border-left: 1px solid var(--border); }
}

/* ==========================================================================
   3-5. Case studies
   ========================================================================== */

.case__index {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.5rem, 5vw, 3.5rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
  margin-bottom: 1.25rem;
}

.case__title {
  margin-top: 1rem;
  max-width: 24ch;
}

.case__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2.5rem;
  margin-top: 3.5rem;
}

@media (min-width: 900px) {
  .case__grid {
    grid-template-columns: 240px minmax(0, 1fr);
    gap: 4.5rem;
  }
}

.case__rail { font-size: 0.9375rem; }

.case__rail div { padding-block: 0.9375rem; }

.case__rail div + div { border-top: 1px solid var(--border); }

/* Pinned narrative.
   The rail holds still while the beats scroll past it and a marker tracks
   position. Nothing is behind a click: every word is in the document and
   visible to a skimmer and a crawler alike, which matters more than usual
   on a page whose author is selling SEO. */

@media (min-width: 900px) {
  .case__rail {
    position: sticky;
    top: calc(var(--nav-h) + 2rem);
    align-self: start;
  }
}

.case__marks {
  display: none;
  margin: 1.75rem 0 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 900px) {
  .case__marks { display: grid; gap: 0.625rem; }
}

.case__mark {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
  transition: color 300ms ease;
}

.case__mark i {
  display: block;
  width: 16px;
  height: 1px;
  background: var(--border-strong);
  transition: width 300ms var(--ease), background-color 300ms ease;
}

.case__mark.is-on { color: var(--accent); }
.case__mark.is-on i { width: 30px; background: var(--accent); }

.case__beat {
  padding: 1.75rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: opacity 320ms ease, border-color 240ms ease, background-color 240ms ease;
}

/* Hover lifts a beat wherever the scroll marker happens to be, so a reader
   can pull any one of them forward.

   The gradient runs warm at the top left and cools into the page ground at
   the bottom right, which is what makes the card read as lit from somewhere
   rather than just as a lighter rectangle. It leans warm on purpose: the
   accent is champagne bronze, and a neutral grey lift beside it looks like
   a mistake. Contrast is measured against the lightest stop, where body
   copy still clears 5.98:1. */
.case__beat:hover {
  background:
    linear-gradient(152deg, #262320 0%, #1B1A1B 55%, #151517 100%);
  border-color: var(--border-strong);
}

.js-beats .case__beat:hover { opacity: 1; }

.case__beat + .case__beat { margin-top: 0.875rem; }

.case__beat h3 { margin: 0 0 0.625rem; }
.case__beat p { margin: 0; }

/* Only dimmed once the script confirms it can undim them again. */
.js-beats .case__beat { opacity: 0.42; }
.js-beats .case__beat.is-on { opacity: 1; border-color: var(--border-strong); }

@media (prefers-reduced-motion: reduce) {
  .js-beats .case__beat { opacity: 1; }
}

.case__rail dt {
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-muted);
  margin-bottom: 0.375rem;
}

.case__rail dd {
  color: var(--text-secondary);
  line-height: 1.5;
}

.case__body > h3 { margin-top: 2.5rem; }

.case__body > h3:first-child { margin-top: 0; }

.case__body > p { margin-top: 0.875rem; }

/* <details> — CSS only, no JS. Default marker replaced by a rotating chevron. */
.case__more {
  margin-top: 2.75rem;
  border-top: 1px solid var(--border);
  padding-top: 1.5rem;
}

.case__more summary {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  list-style: none;
  padding-block: 0.25rem;
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--accent);
  transition: color 180ms ease;
}

.case__more summary::-webkit-details-marker { display: none; }

.case__more summary::marker { content: ""; }

.case__more summary:hover { color: var(--text); }

.chev {
  width: 8px;
  height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 180ms ease;
}

.case__more[open] .chev { transform: translateY(1px) rotate(225deg); }

.case__more .details-body { padding-top: 1.25rem; }

.case__more .details-body > * + * { margin-top: 1rem; }

/* ==========================================================================
   6. Selected work
   ========================================================================== */

/* Carousel — centre-stage.
   The middle card is the subject; its neighbours sit back, scaled and
   dimmed, and clicking one brings it forward. There are no arrows or dots,
   because the scaled cards are the affordance: something smaller and dimmer
   beside something sharp reads as "click me" without a caption.

   Underneath it is still a scroll-snap row, so with no JavaScript at all
   every card is full size and the whole thing is an ordinary scroller. */

.carousel { position: relative; }

.work {
  --card-w: min(84vw, 420px);
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  list-style: none;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  /* Half the track minus half a card, so the first and last can reach the
     middle. Without this the ends never centre and the effect breaks. */
  padding-inline: max(0px, calc(50% - var(--card-w) / 2));
  padding-block: 1.5rem;
  margin-block: -1.5rem;
}

.work::-webkit-scrollbar { display: none; }

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

.work > li {
  flex: 0 0 auto;
  width: var(--card-w);
  scroll-snap-align: center;
  display: flex;
}

@media (prefers-reduced-motion: reduce) {
  .work { scroll-behavior: auto; }
}

/* Centred under the track rather than pushed to one side: the whole section
   is built around a middle, so the controls belong on that axis too. */
.carousel__controls {
  display: flex;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 2.5rem;
}

.carousel__btn {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary);
  font-size: 1.05rem;
  cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease, background-color 180ms ease;
}

.carousel__btn:hover:not([aria-disabled="true"]) {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--bg-card);
}

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

/* Announced rather than removed, so a control never disappears from under a
   keyboard user at the ends of the track. */
.carousel__btn[aria-disabled="true"] { opacity: 0.3; cursor: default; }

/* No JS, no controls: the track on its own is the whole feature. */
.carousel:not(.is-ready) .carousel__controls { display: none; }

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 180ms ease, background-color 180ms ease;
}

/* Screenshots are bright and this page is not, so they sit knocked back at
   rest and come up on hover. Without that, five white thumbnails punch
   holes in the section and the eye stops reading the copy. */
.card__shot {
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
}

.card__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
  filter: saturate(0.72) brightness(0.68) contrast(1.04);
  transform: scale(1.005);
  transition: filter 260ms ease, transform 420ms var(--ease);
}

.card:hover .card__shot img,
.card:focus-visible .card__shot img {
  filter: saturate(1) brightness(0.94) contrast(1);
  transform: scale(1.03);
}

.card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.5rem;
}

.card:hover {
  border-color: var(--border-strong);
  background: var(--bg-elevated);
  color: inherit;
}

.card:hover .card__title { color: var(--accent); }

.card__index {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--text-muted);
  margin-bottom: 0.75rem;
}

.card__title {
  padding-right: 1.5rem;
  transition: color 180ms ease;
}

.card__desc {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-secondary);
}

.card__domain {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin-top: auto;
  padding-top: 1.5rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.card__arrow {
  flex: none;
  transition: transform 180ms ease, color 180ms ease;
}

.card:hover .card__arrow {
  color: var(--accent);
  transform: translate(2px, -2px);
}

/* Only once the script is driving it do the neighbours recede. Without JS
   every card stays at full size and full opacity. */
.carousel.is-ready .card {
  transform: scale(0.87);
  opacity: 0.45;
  transition: transform 460ms var(--ease), opacity 460ms var(--ease),
              border-color 180ms ease, background-color 180ms ease;
}

.carousel.is-ready li.is-active .card {
  transform: scale(1);
  opacity: 1;
  border-color: var(--border-strong);
}

/* A strict three-colour page cannot carry five full-colour screenshots.
   Off-centre they go monochrome; only the centred card is in colour, which
   also makes the carousel's focus state do double duty. */
.carousel.is-ready li:not(.is-active) .card__shot img {
  filter: saturate(0.4) brightness(0.42) contrast(1.04);
}

.carousel.is-ready li.is-active .card__shot img {
  filter: saturate(0.86) brightness(0.86) contrast(1.02);
}

@media (prefers-reduced-motion: reduce) {
  .carousel.is-ready .card { transition: opacity 200ms ease, border-color 180ms ease; }
  .carousel.is-ready .card,
  .carousel.is-ready li.is-active .card { transform: none; }
}

/* ==========================================================================
   7. Testimonials — as cards
   ========================================================================== */

.quotes {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

@media (min-width: 900px) {
  .quotes { grid-template-columns: repeat(3, 1fr); }
}

/* A step above the case cards on purpose: these are the only blocks on the
   page written in someone else's voice, so they should sit off it. #1C1C20
   with the role line raised to #8C8C94 keeps the smallest text at 5.09:1. */
.quote {
  display: flex;
  flex-direction: column;
  padding: 2rem;
  background:
    linear-gradient(152deg, #2A2621 0%, #1D1B1C 52%, #16161A 100%);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  position: relative;
}

/* A single bronze hairline along the top edge. It is what sells the card as
   lit rather than merely pale, and it costs one pseudo-element. */
.quote::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  border-radius: var(--radius) var(--radius) 0 0;
  background: linear-gradient(to right,
    rgba(201, 169, 110, 0) 0%,
    rgba(201, 169, 110, 0.45) 22%,
    rgba(201, 169, 110, 0.12) 70%,
    rgba(201, 169, 110, 0) 100%);
}

.quote blockquote {
  flex: 1;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.1875rem;
  line-height: 1.45;
  letter-spacing: -0.015em;
  color: var(--text);
}

.quote figcaption {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}

.quote picture {
  display: flex;
  flex: none;
}

.quote img {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border);
  flex: none;
}

.quote__name {
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--text);
}

.quote__role {
  /* Two lines reserved whether the role needs them or not. One caption
     wrapping to a second line was pushing its rule up out of line with
     the other two. */
  display: block;
  min-height: 2.9em;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: #94949C;
}

/* ==========================================================================
   8. Capabilities — grouped lists on cards, hairlines kept
   ========================================================================== */

/* Capabilities now uses the tech stack's shape: a labelled column per group
   with a rule under the label, plain items beneath. No icons, because these
   are disciplines rather than products. */
.caps {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2.5rem 2rem;
}

@media (min-width: 640px) {
  .caps { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 900px) {
  .caps { grid-template-columns: repeat(3, 1fr); }
}

.caps > div > h3 {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 1.125rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--border);
}

.caps ul { display: grid; gap: 0.75rem; margin: 0; padding: 0; list-style: none; }

.caps li {
  padding: 0;
  border: 0;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* ==========================================================================
   9. Contact — centred closing statement
   ========================================================================== */

.contact { text-align: center; }

.contact .rule-accent { margin-inline: auto; }

.contact h2 {
  margin-inline: auto;
  max-width: 20ch;
}

.contact__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem;
  margin-top: 2.25rem;
  font-size: 0.9375rem;
}

.footer {
  border-top: 1px solid var(--border);
  padding-block: 2rem;
}

.footer p {
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* ==========================================================================
   Motion
   Elements are visible by default. script.js adds .js to <html> only after
   it confirms IntersectionObserver support and that reduced motion is not
   requested, so the page renders in full with JS off.
   ========================================================================== */

.js .reveal {
  opacity: 0;
  transform: translateY(8px);
}

.js .reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: opacity 420ms var(--ease), transform 420ms var(--ease);
}

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

  .js .reveal,
  .js .reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   Motion graphics
   Added as one block so it can be tuned or deleted whole. Everything here
   animates transform or opacity only, so it runs on the compositor and
   never triggers layout or paint. No JavaScript is involved.
   The global prefers-reduced-motion rule further down neutralises every
   animation in this section; the two rules that need more than that say so.
   ========================================================================== */

/* --- Hero ambient loop --------------------------------------------------- */

.hero { isolation: isolate; overflow: hidden; }
.hero .container { position: relative; z-index: 1; }

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 60% 50%;
  opacity: 0;
  transition: opacity 900ms var(--ease);
}

/* Faded in by script.js once the file can actually paint, so the poster
   never flashes in and out under it. */
/* Full strength. The footage is the artwork, not a texture. */
.hero__video.is-live { opacity: 1; }

/* Two scrims in one layer. The vertical pass seats the footage into the
   page top and bottom; the horizontal pass keeps the text column dark
   enough that body copy still clears 4.5:1 over the brightest frame. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to right,
      rgba(10, 10, 11, 0.92) 0%,
      rgba(10, 10, 11, 0.88) 55%,
      rgba(10, 10, 11, 0.18) 76%,
      rgba(10, 10, 11, 0) 100%),
    linear-gradient(to bottom,
      rgba(10, 10, 11, 0.85) 0%,
      rgba(10, 10, 11, 0.06) 16%,
      rgba(10, 10, 11, 0.10) 78%,
      rgba(10, 10, 11, 0.88) 100%);
}

/* Portrait and small screens: the text runs full width, so the horizontal
   pass would sit under the copy instead of beside it. Vertical only, and
   the footage drops back further. */
@media (max-width: 899px) {
  .hero__video.is-live { opacity: 0.7; }
  .hero__scrim {
    background: linear-gradient(to bottom,
      var(--bg) 0%,
      rgba(10, 10, 11, 0.58) 26%,
      rgba(10, 10, 11, 0.68) 70%,
      var(--bg) 100%);
  }
}

/* --- Ambient glow drift -------------------------------------------------- */
/* The hero glow breathes: long, slow, low contrast, so it reads as depth
   rather than as something moving.

   Measured cost, via an interleaved A/B in a single Lighthouse batch:
   ~1ms of LCP (1806/1805 with, 1805/1804 without). An earlier back-to-back
   comparison appeared to show 75ms, but that was drift between measurement
   batches, not this animation. If you re-test it, interleave the runs —
   this page's LCP on a local server is bimodal at roughly 1730ms or 1805ms
   and consecutive batches are not comparable. */

.hero::before {
  animation: glow-drift 16s ease-in-out infinite;
}

@keyframes glow-drift {
  0%, 100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
  50% {
    transform: translate(-50%, -53%) scale(1.1);
    opacity: 0.78;
  }
}

/* --- Accent hairline draw-in --------------------------------------------- */
/* The bronze rule draws itself left to right as its section arrives. Uses
   the existing .reveal hook, so it needs no new JavaScript. Gated on .js
   so the rules are simply present when scripting is off. */

.js .reveal .rule-accent {
  transform: scaleX(0);
  transform-origin: left center;
}

.js .reveal.is-visible .rule-accent {
  animation: rule-draw 620ms var(--ease) 120ms forwards;
}

.js .hero .rule-accent {
  transform-origin: left center;
  animation: rule-draw 720ms var(--ease) 240ms both;
}

@keyframes rule-draw {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* --- Scroll progress hairline -------------------------------------------- */
/* A CSS scroll-driven animation: no scroll listener, no JS, no main-thread
   work. Where animation-timeline is unsupported the bar is removed rather
   than left sitting at zero width. */

.progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  z-index: 101;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  pointer-events: none;
  animation: progress-grow linear;
  animation-timeline: scroll(root block);
}

@keyframes progress-grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

@supports not (animation-timeline: scroll()) {
  .progress { display: none; }
}

/* The blanket reduced-motion rule collapses animation duration, which would
   strand these two mid-state rather than at rest. Handle them explicitly. */
@media (prefers-reduced-motion: reduce) {
  .progress { display: none; }

  .js .reveal .rule-accent,
  .js .reveal.is-visible .rule-accent,
  .js .hero .rule-accent {
    transform: none;
    animation: none;
  }
}

/* ==========================================================================
   Location and contact icons
   ========================================================================== */

/* Location. A small map earns its place only if it is instantly readable
   as one fact: she works from here. One pin, no arcs, no legend. */
.locate {
  display: inline-flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 2rem;
  padding: 0.875rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-card);
}

.locate__map { display: block; width: 148px; height: auto; flex: none; }

.locate__where { margin: 0; text-align: left; }
.locate__where b { display: block; font-weight: 500; color: var(--text); font-size: 0.9375rem; }
.locate__where span { display: block; font-size: 0.8125rem; color: var(--text-muted); margin-top: 0.125rem; }

/* Icon links. Labelled for assistive tech and given a visible label on the
   contact block, because an unlabelled glyph row is a guessing game. */
.iconrow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.625rem;
  margin-top: 2rem;
}

.iconlink {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 1.125rem 0.75rem 0.9375rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font-size: 0.9375rem;
  text-decoration: none;
  transition: border-color 180ms ease, color 180ms ease, background-color 180ms ease;
}

.iconlink svg { flex: none; width: 17px; height: 17px; }

.iconlink:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--bg-card);
}

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

/* ==========================================================================
   Tech stack
   ========================================================================== */

.stack {
  display: grid;
  gap: 2.5rem 2rem;
  margin-top: 2.5rem;
  grid-template-columns: 1fr;
}

@media (min-width: 640px) { .stack { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .stack { grid-template-columns: repeat(4, 1fr); } }

.stack__group h3 {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 1.125rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--border);
}

.stack__list { display: grid; gap: 0.75rem; margin: 0; padding: 0; list-style: none; }

.tool {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.9375rem;
  color: var(--text-secondary);
}

.tool svg { flex: none; width: 18px; height: 18px; color: var(--text-muted); }

/* Not every tool she uses has an openly licensed mark, and inventing a
   logo for the ones that do not is worse than not drawing one. Those get a
   monogram in the same footprint so the column still lines up. */
.tool__mono {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  font-size: 0.5625rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}

/* ==========================================================================
   Rendering budget
   ========================================================================== */

/* content-visibility: auto was here and has been removed on purpose.
   It skips layout for off-screen sections, but to do that it has to guess
   their height, and a wrong guess moves the page under a jump-link: the
   browser scrolls to the estimated position, the section then realises at
   its real height, and the target ends up somewhere else. Measured on this
   page it put "Work" 2,018px off. On a one-page site with in-page nav that
   is a worse bug than the render time it saved, and the page already paints
   in ~210ms without it. */

/* Print: no motion, no chrome, just the words. */
@media print {
  .nav, .nav__pill, .nav__cta, .hero__media, .grain, .carousel__controls { display: none !important; }
  body { background: #fff; color: #000; }
  .case__beat, .card, .quote { break-inside: avoid; }
}

/* ==========================================================================
   Content rev — additions only
   ========================================================================== */

/* Hero now carries a kicker above the claim and a supporting line below. */
.hero__kicker { margin-top: 1.25rem; }
.hero__lede { margin-top: 0.875rem; max-width: 22ch; }
.hero__sub { margin-top: 1.25rem; font-size: 1rem; max-width: 46ch; color: var(--text-secondary); }

/* A standfirst under a section heading. */
.section__sub { margin-top: 1rem; font-size: 1rem; }

/* Each case study opens with a summary line before the pinned beats. */
.case__intro { margin-top: 1.25rem; font-size: 1.0625rem; }

/* Four metrics instead of three, and four capability columns instead of three. */
@media (min-width: 640px) { .proof--four { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .proof--four { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 640px) { .caps--four { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .caps--four { grid-template-columns: repeat(4, 1fr); } }

/* "What I did" is now four labelled moves rather than one paragraph. */
.case__beat p + p { margin-top: 0.875rem; }
.case__beat p strong { color: var(--text); font-weight: 500; }

/* The long-form case studies are sectioned, so the disclosure needs headings. */
.details-title {
  font-family: var(--font-display);
  font-weight: 520;
  font-size: 1.25rem;
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0 0 1.25rem;
}

.details-body h4 {
  margin: 1.75rem 0 0.5rem;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.details-body h4:first-of-type { margin-top: 1.5rem; }

/* Hero copy sits over lit footage now that the scrim has been pulled back.
   Lifted here only, and measured across seven frames of the loop at three
   viewport widths: kicker 5.50:1, supporting line 6.70:1, h1 12.40:1. */
.hero__kicker { color: #9AA0A6; }
.hero__sub { color: #B0B0B0; }

/* ==========================================================================
   Mobile navigation
   ========================================================================== */

/* Desktop keeps the pill. Below 900px the links move into a menu behind a
   button, because a scrolling row cut the last item off and spent a whole
   row of a small screen doing it. */

.nav__toggle { display: none; }

@media (min-width: 900px) {
  :root { --nav-h-sm: var(--nav-h); }
}

@media (max-width: 899px) {
  .nav {
    grid-template-columns: auto 1fr auto auto;
    align-items: center;
    gap: 0.625rem;
  }

  .nav__mark  { grid-column: 1; }
  .nav__cta   { grid-column: 3; }
  .nav__toggle {
    grid-column: 4;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    background: rgba(18, 18, 20, 0.94);
    color: var(--text);
    cursor: pointer;
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }

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

  /* Three bars drawn from one element: the middle is the box's own border,
     the outer two are pseudo-elements. On open they cross into an X. */
  .nav__bars,
  .nav__bars::before,
  .nav__bars::after {
    display: block;
    width: 17px;
    height: 1.5px;
    border-radius: 1px;
    background: var(--accent);
    transition: transform 260ms var(--ease), opacity 160ms ease;
  }

  .nav__bars::before,
  .nav__bars::after { content: ""; position: absolute; }
  .nav__bars::before { transform: translateY(-6px); }
  .nav__bars::after  { transform: translateY(6px); }

  [aria-expanded="true"] .nav__bars { background: transparent; }
  [aria-expanded="true"] .nav__bars::before { transform: rotate(45deg); }
  [aria-expanded="true"] .nav__bars::after  { transform: rotate(-45deg); }

  /* The panel. visibility:hidden rather than opacity alone, so the links
     leave the tab order when the menu is shut. */
  .nav__pill {
    display: block;
    position: absolute;
    /* justify-self from the desktop rule still applies to an absolutely
       positioned grid child and wins over left/right, which pinned the
       panel to a 121px column. Reset it. */
    justify-self: stretch;
    top: calc(100% - 0.25rem);
    right: 1.25rem;
    left: 1.25rem;
    width: auto;
    padding: 0.5rem;
    background: rgba(16, 16, 18, 0.97);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity 200ms ease, transform 240ms var(--ease), visibility 200ms;
  }

  .nav-open .nav__pill {
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  .nav__pill ul {
    display: grid;
    gap: 0.125rem;
    width: auto;
  }

  .nav__pill a {
    display: block;
    padding: 0.8125rem 0.875rem;
    font-size: 0.9375rem;
    border-radius: calc(var(--radius) - 2px);
  }

  @media (prefers-reduced-motion: reduce) {
    .nav__pill,
    .nav__bars,
    .nav__bars::before,
    .nav__bars::after { transition: none; }
  }
}
