/* ==========================================================================
   Erik Levitch — portfolio
   Design system lifted from work-next.pdf. Every colour below was sampled
   from the rendered slides rather than estimated.
   ========================================================================== */

/* --- Fonts ---------------------------------------------------------------- */

@font-face {
  font-family: "Apercu Pro";
  src: url("../fonts/ApercuPro-Regular.otf") format("opentype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Apercu Pro";
  src: url("../fonts/ApercuPro-Medium.otf") format("opentype");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Apercu Mono";
  src: url("../fonts/ApercuMono-Regular.otf") format("opentype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Apercu Mono";
  src: url("../fonts/ApercuMono-Medium.ttf") format("truetype");
  font-weight: 500; font-style: normal; font-display: swap;
}

/* --- Tokens --------------------------------------------------------------- */

:root {
  --bg:        #FAFAFA;  /* the slides' own paper */
  --surface:   #FFFFFF;

  /* Three levels, and the gaps between them are the hierarchy — structure, support,
     aside. Ratios are against --bg; change --bg and all three need re-checking,
     since AA has the least margin to spare at the faint end. */
  --ink:       #37383C;  /* 11.2:1 — body copy, labels, anything structural */
  --ink-soft:  #5F6167;  /*  5.9:1 — secondary copy and rest-state nav */
  --ink-faint: #6A6C72;  /*  5.0:1 — mono eyebrows and meta. Real text, not
                                decoration, so it stays above the AA floor even
                                though it's the quietest thing on the page. */
  --rule:      #D6D7DA;

  /* Tags are the one label that never takes the accent — they're taxonomy, not
     voice, so they read the same on every card and every case study. One step
     off --surface, so the pill has an edge without needing a border. */
  --tag-bg:  #EAEBED;
  --tag-ink: var(--ink);   /* 9.9:1 on --tag-bg */

  /* Colour on this site says whose work you're looking at, and nothing else. The
     homepage carries no accent of its own — it runs on ink — and each case study
     borrows its accent from the client brand, so the moment anything turns
     coloured you're inside someone's work.

     Every accent owes the same four jobs, and each is a contrast floor, not a
     preference: --accent carries strokes and focus rings (≥3.5:1 on --bg);
     --accent-halo is a tint you can set --ink on (≥7:1); --accent-ink carries type
     (≥5.5:1 on --bg); --accent-solid + --accent-solid-ink are a fill and the one
     text colour legible on it. A brand hex rarely does all four, so each ramp
     below is the brand colour re-cut per job. */
  --blue:   #345AF6;  --blue-halo:   #B9C6F8;  --blue-ink:   #2A4BE0;

  /* Orange is Pulley's, violet is Mixpanel's, and both are sampled from their own
     card gradient so the stroke you see on hover is a colour already in the wash
     behind it. Violet is the gradient's 0% stop used unmodified — it clears both
     the stroke floor (4.5:1) and white-on-fill (4.7:1) as-is, which is rare.
     Pulley's #FF5E0B does not: 2.9:1 as a stroke and illegible under either ink,
     so it stays a gradient stop only and --orange is the same hue cut darker. */
  --orange: #CF4600;  --orange-halo: #FFD3BC;  --orange-ink: #B33D00;
  --violet: #6C5FEF;  --violet-halo: #DCD8FB;  --violet-ink: #5849ED;

  /* The one colour that isn't a client's. It exists for .hero__dot and nothing
     else — see that rule for why the availability signal earns it. */
  --gold: #A87F00;

  /* Default accent — neutral, so the homepage reads in ink. Overridden per case
     study via [data-accent]. Each value is the ink token that does that accent's
     job, rather than a fourth ramp: the point of the default is that it is not a
     colour, and inventing greys here would only be a colour in disguise. */
  --accent: var(--ink);
  --accent-halo: var(--rule);   /* --ink on it is 8.4:1 */
  --accent-ink: var(--ink);
  /* Solid-fill pair: the fill, and the only text colour legible on it. Split out
     because the fill differs per accent and the legible ink follows it. */
  --accent-solid: var(--ink);
  --accent-solid-ink: var(--surface);

  /* The card wash. --accent-wash is the client's full-strength gradient; --card-wash
     is how much of it survives, as a single number for the whole site. The gradients
     are drawn at full saturation, and card copy is tuned for white — at 1 the body
     text lands at 2.2–2.9:1. 0.15 keeps the hue legible as the client's while
     leaving the type where it was. See .card for how the two combine. */
  --card-wash: 0.15;
  --accent-wash: none;

  --sans: "Apercu Pro", ui-sans-serif, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --mono: "Apercu Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --gutter: clamp(1.25rem, 5vw, 5rem);
  --measure: 62ch;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* One block per case study, named for the client rather than the colour — the
   attribute then says which work it is, and retuning a brand doesn't leave the
   markup calling orange "pink". Adding a fourth case study is one block here plus
   data-accent on the card and on that page's <body>; nothing else changes.

   --accent-wash is the only value that isn't a flat colour. It's the client's
   gradient at full strength; .card decides how much of it shows. */

[data-accent="watershed"] {
  --accent: var(--blue); --accent-halo: var(--blue-halo); --accent-ink: var(--blue-ink);
  --accent-solid: var(--blue); --accent-solid-ink: #fff;
  --accent-wash: linear-gradient(160deg, #CBD7FE 0%, #94ADFD 50%, #ACEFD1 100%);
}
[data-accent="pulley"] {
  --accent: var(--orange); --accent-halo: var(--orange-halo); --accent-ink: var(--orange-ink);
  --accent-solid: #FF8E3C; --accent-solid-ink: #0A0A0A;
  --accent-wash: linear-gradient(160deg, #FFBF6E 0%, #FF5E0B 25%, #F686D8 75%, #CEADF7 100%);
}
[data-accent="mixpanel"] {
  --accent: var(--violet); --accent-halo: var(--violet-halo); --accent-ink: var(--violet-ink);
  --accent-solid: var(--violet); --accent-solid-ink: #fff;
  --accent-wash: linear-gradient(160deg, #6C5FEF 0%, #958CF5 33%, #F686D8 64%, #FE58D1 94%);
}

/* --- Base ----------------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: 6rem; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

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

a { color: inherit; }

::selection { background: var(--accent-halo); color: var(--ink); }

.wrap { max-width: 1320px; margin-inline: auto; padding-inline: var(--gutter); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: 50%; translate: -50% -120%;
  z-index: 100; padding: 0.75rem 1.25rem;
  background: var(--ink); color: var(--bg);
  font-family: var(--mono); font-size: 0.8rem; letter-spacing: 0.08em;
  text-transform: uppercase; text-decoration: none; border-radius: 0 0 10px 10px;
  transition: translate 0.2s var(--ease);
}
.skip-link:focus { translate: -50% 0; }

/* --- Typography ----------------------------------------------------------- */

h1, h2, h3 { font-weight: 400; margin: 0; text-wrap: balance; }

.display {
  font-size: clamp(2.25rem, 1.1rem + 5.1vw, 5.25rem);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.headline {
  font-size: clamp(1.75rem, 1.1rem + 2.9vw, 3.5rem);
  line-height: 1.1;
  letter-spacing: -0.018em;
}

.subhead {
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.75rem);
  line-height: 1.22;
  letter-spacing: -0.012em;
  font-weight: 500;
}

.lede { font-size: clamp(1.05rem, 1rem + 0.35vw, 1.3rem); line-height: 1.5; max-width: var(--measure); }

.prose { max-width: var(--measure); color: var(--ink); }
.prose p + p { margin-top: 1.1em; }

/* The deck's most recognisable texture: mono, uppercase, widely tracked. */
.mono {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

/* --- Dotted section rule (the deck's slide header) ------------------------ */

.rule {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding-bottom: 0.85rem;
  border-bottom: 1px dotted var(--rule);
  margin-bottom: clamp(2.5rem, 5vw, 4.5rem);
}

.rule__label { color: var(--ink); }
.rule__label::before { content: "●"; margin-right: 0.7em; font-size: 0.85em; color: var(--ink); }
.rule__meta { color: var(--ink-faint); font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.12em; }

/* Secondary label wraps awkwardly on narrow screens — drop it and keep the
   dotted rule to a single clean line. */
@media (max-width: 560px) { .rule__meta { display: none; } }

/* --- The link button -------------------------------------------------------
   A plain single-stroke pill: outline, mono label, trailing or leading arrow
   that names the action (leave the site, download a file, go back/forward).
   Fills solid on hover/focus rather than lifting — a flatter, quieter move
   than the old chip's, matched to a control with a direction to communicate. */

.link-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.62em 1.15em;
  background: var(--surface);
  color: var(--ink-soft);
  border: 1.5px solid var(--ink-soft);
  border-radius: 999px;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}

.link-btn:hover,
.link-btn:focus-visible {
  background: var(--ink-soft);
  color: var(--surface);
  outline: none;
}

.link-btn__icon {
  width: 0.85em;
  height: 0.85em;
  flex: none;
  stroke: currentColor;
}

/* The "going back" reading needs the arrow both leading the label and
   pointing left — the icon markup itself always points right. */
.link-btn--prev { flex-direction: row-reverse; }
.link-btn--prev .link-btn__icon { transform: scaleX(-1); }

/* --- Inline logotype -----------------------------------------------------
   The client's wordmark set into the headline sentence. Replaces the chip in
   case study <h1>s: the chip's rings read as a button there, and nothing in a
   headline is clickable. Solid brand fill, white knockout mark, no border, no
   rings, no hover — it's a word, not a control.

   Every dimension is in em of the headline, so it tracks .headline's clamp()
   from 1.75rem to 3.5rem with no second breakpoint.

   --logo-h is per brand because the supplied SVGs crop to different bands, so a
   shared height would render them at wildly different sizes. Measured off the
   rendered artwork:

     Watershed  box = cap-top to baseline, no descenders.  x-height = 79% of box
     Mixpanel   box = 'l' ascender to 'p' descender.       x-height = 52% of box

   x-height is what the eye compares between a logotype and the words either
   side of it — box height is not. So --logo-h targets x-height, against
   Apercu's measured 0.504em.

   The artwork is centred in the pill and left there. An earlier pass shifted
   each mark to line its baseline up with the sentence; that is typographically
   tidier but looks wrong, because neither box is symmetric about its own
   baseline — Watershed ended up with a band of empty fill beneath it and
   Mixpanel sat visibly low. Centring wins: Mixpanel's x-band happens to span
   0.24–0.76 of its box, so box-centred *is* x-band-centred, and Watershed's
   near enough. Don't reintroduce the shift without looking at the result.
   -------------------------------------------------------------------------- */

.logotype {
  --logo-h: 0.64em;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1.05em;
  padding-inline: 0.42em;
  background: var(--accent-solid);
  color: var(--accent-solid-ink);  /* the knockout: alt text here, real text in --stat */
  border-radius: 999px;
  letter-spacing: normal;    /* don't inherit the headline's -0.018em */
  vertical-align: -0.165em;  /* centres the pill on the cap band */
  white-space: nowrap;
}

/* No margin-inline: the real space characters in the markup either side of the
   span already do the word spacing. .chip--inline added 0.2em on top of them,
   which is what pushed the old pill out of the sentence. */
.logotype img {
  display: block;
  height: var(--logo-h);
  width: auto;
}

/* Apercu measures cap 0.700em, x-height 0.504em, space 0.268em.

     Watershed  0.64em box → x-height 0.506em, an exact match to the sentence.
     Mixpanel   0.82em box → x-height 0.426em, deliberately short of it.

   Mixpanel can't reach parity: its box carries both an 'l' ascender and a 'p'
   descender, so matching x-heights would need a 0.97em box inside a 1.05em
   pill — 0.04em of clearance, which is sub-pixel at the mobile end of the
   clamp and reads as the 'p' touching the edge. It stops at 0.82em, and the
   shortfall is doing real work anyway: the wordmark is a high-contrast serif
   against Apercu's sans, so it holds its weight at a smaller x-height than
   Watershed's clean sans needs. */
[data-accent="watershed"] .logotype { --logo-h: 0.64em; }
[data-accent="mixpanel"]  .logotype { --logo-h: 0.82em; }
[data-accent="pulley"]    .logotype { --logo-h: 0.85em; }

/* Vertical seating in the case-study <h1> only.

   -0.165em is the general-purpose value: it centres the pill on the cap band of
   a line of running text. In the case headers the pill is set at the top of the
   clamp against a two-line headline, and at that size the residual asymmetry in
   each brand's artwork becomes visible — Watershed (cap-to-baseline box, no
   descenders) sits low, Pulley (ascender-to-descender box) sits high. Measured
   on the rendered pages rather than derived, so they're per brand and scoped to
   .case-head; every other .logotype keeps the -0.165em baseline. */
[data-accent="watershed"] .case-head .logotype { vertical-align: -0.05em; }
[data-accent="pulley"]    .case-head .logotype { vertical-align: -0.20em; }

/* Pulley lands with Mixpanel rather than Watershed, for the same reason: its box
   runs 'l' ascender to 'y' descender, x-height 0.50 of the box, so 0.85em puts
   it at 0.425em — within a thousandth of Mixpanel's 0.426em. Two bold serifs
   sitting at the same optical size, which is what you want when the only thing
   distinguishing the three headers should be the brand.

   Its artwork is outlined from Lora Bold. The wordmark Pulley supplied was live
   <text font-family="Lora">, which only renders as Lora on a machine that has
   Lora installed — inside an <img> the SVG is an isolated document and cannot
   reach a webfont, so every other visitor would have got Georgia at a different
   width. Outlines removed the dependency; verified against the text rendering
   at 1280px wide, ink bounds matching within 1px. */

/* Text set in the logotype pill instead of a wordmark — the "90% ↑" stat in
   Mixpanel's Results <h2>. It kept the chip through the logotype conversion on
   the grounds that a stat isn't a brand mark, but that's the wrong axis: the
   rings aren't saying "this is a logo", they're saying "this is a control", and
   an <h2> is no more clickable than the <h1> the rings were removed from. Two
   ringed pills a scroll apart on the same page, one ringed and one not, read as
   an oversight rather than a distinction. Same pill, different filling.

   Everything in .logotype is in em of the headline, and this variant resets
   font-size, so each inherited length has to be restated in its own em to come
   out the same physical size. The divisor is 0.6 throughout:

     height          1.05em  / 0.6 = 1.75em
     padding-inline  0.42em  / 0.6 = 0.7em
     vertical-align -0.165em / 0.6 = -0.275em

   0.6em is where Apercu Mono's figures (0.708em tall) land at 0.425em — the
   same band .logotype puts the wordmarks' x-height at, so the two pills hold
   their contents at matching optical size and not merely at matching height.
   That is a good deal larger than the 0.44em the chip used, because the chip
   was sized against its own smaller pill.

   The end padding drops by the 0.08em of tracking that hangs off the last
   glyph; without it the figures sit visibly left of centre. */
.logotype--stat {
  font-family: var(--mono);
  font-size: 0.6em;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.08em;  /* overrides .logotype's `normal`, which is only there
                              to block the headline's -0.018em */
  text-transform: uppercase;
  height: 1.75em;
  padding-inline: 0.7em 0.62em;
  vertical-align: -0.275em;
}

/* Taxonomy labels — the "0 → 1" row that sits under each chip in the deck.
   Deliberately neutral and deliberately not a .chip: squarer radius, no border,
   no rings. The chip is the accent's signature and appears once per card; tags
   come in threes and have to stay quiet underneath it. */
.tag {
  display: inline-flex;
  align-items: center;
  padding: 0.34em 0.7em;
  background: var(--tag-bg);
  color: var(--tag-ink);
  border-radius: 7px;
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* The pill supplies its own separation, so the gap is a real gap now — the old
   1.25rem was manufacturing edges for text that had none. */
.tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; padding: 0; list-style: none; }

/* --- Nav ------------------------------------------------------------------ */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px dotted var(--rule);
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 4.5rem;
}

.nav__brand {
  font-size: 1.02rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  white-space: nowrap;
}
.nav__brand img { width: 30px; height: 30px; border-radius: 50%; flex: none; }

.nav__links { display: flex; align-items: center; gap: clamp(1rem, 2.5vw, 2rem); }

.nav__link {
  font-family: var(--mono);
  font-size: 0.76rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  padding: 0.4rem 0;
  position: relative;
  transition: color 0.2s var(--ease);
}
.nav__link::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0;
  /* --ink, not --accent: which item you're on is navigation state, not the
     client's voice, so the marker reads the same on every page. */
  height: 1.5px; background: var(--ink);
  scale: 0 1; transform-origin: left;
  transition: scale 0.3s var(--ease);
}
.nav__link:hover { color: var(--ink); }
.nav__link:hover::after,
.nav__link[aria-current="true"]::after { scale: 1 1; }
.nav__link[aria-current="true"] { color: var(--ink); }

/* Keep the bar to a single row all the way down: tighten the links, then fall
   back to the mark alone. */
@media (max-width: 720px) {
  .nav__inner { min-height: 3.9rem; }
  .nav__links { gap: 1.15rem; }
  .nav__link { font-size: 0.7rem; letter-spacing: 0.1em; }
  .nav__brand { font-size: 0.95rem; }
  html { scroll-padding-top: 4.5rem; }
}

@media (max-width: 420px) {
  .nav__brand span { display: none; }
  .nav__links { gap: 0.9rem; }
}

/* --- Sections ------------------------------------------------------------- */

.section { padding-block: clamp(4rem, 9vw, 8rem); }
.section--tight { padding-block: clamp(3rem, 6vw, 5rem); }

/* --- Hero ----------------------------------------------------------------- */

/* Bottom padding is deliberately light — the Work section brings its own
   clamp(4rem, 9vw, 8rem) on top, and doubling both left the status bar
   marooned in the middle of the fold. */
.hero { padding-top: clamp(3rem, 7vw, 6rem); padding-bottom: 0; }

/* Full measure, no cap: the sentence is the page's strongest asset and nothing
   shares its row. It sets no colour of its own and inherits --ink at full strength,
   which is a deliberate call rather than an oversight: at this size some eyes read
   it as loud. --ink-soft here is the one-line change if it ever needs to sit back. */
.hero__headline {
  letter-spacing: -0.028em;
  /* Caps a touch below .display's 5.25rem. At the full size the sentence needs
     1163px for two lines but .wrap only ever offers 1160 — a 3px miss that
     orphaned "teams" onto a third line. Backing off to 4.85rem lets it set as
     two full-measure lines from 1100px up. */
  font-size: clamp(2.25rem, 1.1rem + 4.7vw, 4.85rem);
  /* h1 inherits text-wrap: balance, which equalises line lengths — right for a
     short heading, wrong here. At 1600px it pulled the headline into three short
     centred-looking lines instead of letting it fill the measure. `pretty` still
     avoids a one-word last line without shrinking the block. */
  text-wrap: pretty;
}

/* Where I am · whether I'm free · how to reach me. One line, three facts. */
.hero__status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.9rem 2rem;
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: 1rem;
  border-top: 1px dotted var(--rule);
  color: var(--ink-soft);
}

.hero__status > * { display: inline-flex; align-items: center; gap: 0.55em; }
.hero__status strong { font-weight: inherit; color: var(--ink); }

/* Availability as a signal rather than a sentence — a live dot reads as current
   state in a way prose written once never does. Deliberately --gold, not
   --accent: it is the site speaking, not the client, so it holds on a case study
   carrying a brand colour — and it is the only place gold survives, which is why
   the token still exists. #A87F00 rather than the saturated gold you'd reach for
   first: at 0.5rem the dot is a shape that has to be seen against --bg, and a
   bright gold is ~1.5:1 there. The slow dim-and-contract is what makes it read as live
   rather than printed — it stays inside the dot's own footprint, so it never
   pulls the eye off the sentence it belongs to. */
.hero__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--gold);
  flex: none;
  animation: dot-breathe 3.2s ease-in-out infinite;
}
@keyframes dot-breathe { 50% { opacity: 0.4; scale: 0.85; } }

/* Inherits .mono wholesale — same colour, case, and tracking as the two facts
   beside it. Lowercase at tighter tracking packs more ink into the same width,
   which reads as a heavier weight even at identical weight and colour, so the
   address was pulling focus from a row meant to sit evenly. */
.hero__email {
  color: inherit;
  text-decoration: none;
}
.hero__email:hover { text-decoration: underline; text-underline-offset: 0.3em; }

/* 0.4rem padding around a 0.95rem icon keeps the hit target above 24px. */
.hero__copy {
  display: inline-flex;
  padding: 0.4rem;
  margin: -0.2rem 0 -0.2rem -0.25rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: background 0.2s var(--ease);
}
.hero__copy:hover { background: var(--tag-bg); }
.hero__copy:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.hero__copy svg { width: 0.95rem; height: 0.95rem; }

/* Icon swaps to a tick for a beat after copying. The <span role="status"> beside
   it carries the same confirmation for screen readers. */
.hero__copy-done { display: none; }
.hero__copy.is-copied .hero__copy-idle { display: none; }
.hero__copy.is-copied .hero__copy-done { display: block; }

/* The centre item can't hold centre once the row wraps, so stack and left-align
   rather than letting it strand. */
@media (max-width: 860px) {
  .hero__status { flex-direction: column; align-items: flex-start; gap: 0.7rem; }
}

/* --- Work cards ----------------------------------------------------------- */

.cards { display: grid; gap: clamp(2.5rem, 5vw, 4rem); }

.card {
  /* The group break tracks the card's own padding clamp so it never exceeds
     it — at mobile the padding floors at 1.5rem, and a fixed 2rem of internal
     air would leave the card reading cramped at the sides. */
  --card-break: clamp(1.5rem, 3vw, 2rem);
  position: relative;
  display: grid;
  gap: clamp(1.5rem, 3vw, 2.5rem);
  /* Copy starts at the top so all three eyebrows land on one scan line down
     the page; the media overrides back to centre so the shorter column doesn't
     hang with dead space beneath it. */
  align-items: start;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  /* Two layers: the client's gradient, and a white veil over it at the inverse of
     --card-wash. Veiling rather than restating the gradient at lower saturation
     keeps one definition of each brand's ramp — retune --card-wash and all three
     cards move together, and a new case study inherits the strength for free.
     A card with no [data-accent] gets --accent-wash: none and stays flat white. */
  background-color: var(--surface);
  background-image:
    linear-gradient(rgb(255 255 255 / calc(1 - var(--card-wash))) 0 0),
    var(--accent-wash);
  border: 1px solid var(--rule);
  border-radius: 20px;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.35s var(--ease), translate 0.35s var(--ease),
              box-shadow 0.35s var(--ease);
}

/* Every card runs copy-left / media-right. The alternating flip that used to
   sit here made the middle card read as a different component rather than the
   same one repeated, which cost more than the rhythm it bought. */
@media (min-width: 900px) {
  .card { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); }
}

.card:hover {
  translate: 0 -3px;
  border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
  box-shadow: 0 18px 40px -28px color-mix(in srgb, var(--accent) 60%, transparent);
}
.card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

/* Vertical rhythm inside the card runs on two values: 1rem holds a group
   together, 2rem breaks between groups. So the index metadata separates from
   the logo+title pair, and the tags separate from the copy — the tight core
   between them is what makes the card read as grouped rather than as five
   evenly-spaced blocks. Nudged off --ink-faint: one step lighter than the
   description so hierarchy is carried by value as well as size.

   This is the one text token on the site under the AA floor, and it is deliberate.
   It's 4.6:1 on white with no headroom, so the card wash pushes it to 3.9–4.3:1
   depending on the accent. Accepted for the quietest label in the hierarchy; if
   --card-wash ever goes above 0.15 this has to move to --ink-faint or darker,
   because everything else on the card fails soon after. */
.card__eyebrow { color: #737579; margin-bottom: var(--card-break); display: block; }

/* Homepage work-card "Case Study 0X/03" eyebrows are hidden — the counter read as
   noise above the card title. Markup is intact; delete this rule to bring them back. */
.card__eyebrow { display: none; }

/* The brand mark sits bare on the card — the chip's rings are the deck's
   "someone circled this" motif and read as a button here, which the whole
   card already is. Multiply drops any white crop to the card colour. */
/* A shared height would be the obvious thing and it's wrong, because the three
   files crop to different bands: Watershed's PNG carries a globe that is taller
   than its own letters, Mixpanel's PNG has ~25% internal whitespace, and
   Pulley's SVG is cropped tight to the ink. Setting all three to one box height
   renders their wordmarks at visibly different sizes — Watershed reading small
   and Pulley about double.

   So the box is scaled per brand to land every wordmark on one x-height. The
   fractions are measured off the rendered artwork, not guessed:

     Watershed  x-height 0.315 of its box  → 1.32
     Mixpanel   x-height 0.4175            → 1     (the reference)
     Pulley     x-height 0.485             → 0.86

   All three come out at ~10px of x-height at the desktop end of the clamp.
   Scaling the same clamp keeps one source of truth for the size itself. The
   cards are full-width and stacked, so Watershed's taller box shifts only its
   own title, with nothing beside it to misalign against. Retire a brand's line
   here the day its art is recropped. */
.card__logo {
  --card-logo-scale: 1;
  display: block;
  height: calc(clamp(1.5rem, 2.6vw, 1.9rem) * var(--card-logo-scale));
  width: auto;
  margin-bottom: 1rem;
  mix-blend-mode: multiply;
}

[data-accent="watershed"] .card__logo { --card-logo-scale: 1.32; }
[data-accent="pulley"]    .card__logo { --card-logo-scale: 0.86; }

.card__title {
  font-size: clamp(1.4rem, 1.1rem + 1.1vw, 2rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
  /* Floors above the logo's 1rem so the three steps — tight (logo+title),
     medium (title/copy), break (groups) — stay distinct at every width. */
  margin-bottom: clamp(1.125rem, 2vw, 1.25rem);
}

/* Held at 16px rather than letting it ride the body clamp up to 18px: against
   a 32px title that widens the size contrast from 1.78x to 2x, and stops the
   paragraph out-massing the headline it belongs to. */
.card__desc {
  color: var(--ink-soft);
  max-width: 46ch;
  margin: 0 0 var(--card-break);
  font-size: 1rem;
  line-height: 1.5;
  text-wrap: pretty;
}

.card__media {
  align-self: center;
  border-radius: 12px;
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  padding: clamp(0.5rem, 1.2vw, 1rem);
}
.card__media img { border-radius: 6px; width: 100%; }

/* --- About ---------------------------------------------------------------- */

.about {
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: center;
}
@media (min-width: 920px) { .about { grid-template-columns: 1.15fr 0.85fr; } }

.stats {
  display: grid;
  gap: 1.5rem;
  margin: clamp(2.5rem, 4vw, 3.5rem) 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 560px) { .stats { grid-template-columns: repeat(3, 1fr); } }

.stat { border-top: 1px dotted var(--rule); padding-top: 1rem; }
.stat__num {
  display: block;
  font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.4rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  /* --ink, not --accent-ink: metrics carry no accent for now. When they do, it
     will be the accent of the case study they sit in. */
  color: var(--ink);
}
.stat__label { display: block; margin-top: 0.5rem; color: var(--ink-soft); font-size: 0.92rem; line-height: 1.35; }

.venn { width: 100%; height: auto; }

/* --- Contact -------------------------------------------------------------- */

.contact__links {
  display: flex; flex-wrap: wrap; gap: 20px;
  margin: clamp(2.5rem, 5vw, 3.5rem) 0 0; padding: 0; list-style: none;
}

/* --- Footer --------------------------------------------------------------- */

.footer {
  border-top: 1px dotted var(--rule);
  padding-block: 2.5rem;
  color: var(--ink-faint);
}
.footer__inner { display: flex; flex-wrap: wrap; gap: 0.75rem 2rem; justify-content: space-between; }

/* --- Case study pages ----------------------------------------------------- */

.case-head { padding-top: clamp(3rem, 7vw, 6rem); }
/* The "Case Study 01/03" counter. Same role as .card__eyebrow on the homepage —
   kept separate because it isn't inside a card. */
.case-eyebrow { margin: 0 0 1.5rem; color: var(--ink-faint); }

/* The metadata block reads as a ledger: label set left, value right, a dotted
   rule between rows. It was four auto-fit columns with the label stacked over
   its value, which broke down on the long values — Watershed's team ran four
   deep in a 160px column while Role sat on one line beside it.

   The row <div> is the grid, rather than the <dl> with display:contents on the
   wrappers. That matters for alignment: baseline-aligning the label to the
   value moves those boxes vertically, and a rule painted on the cells would
   move with them, so the two halves of each line would stop meeting. Painting
   it on the row decouples them — the rule holds while the contents align. */
.case-meta {
  margin: clamp(3rem, 6vw, 4.5rem) 0 0;
  padding: 0;
}
.case-meta > div {
  display: grid;
  grid-template-columns: 1fr;
  padding: 0.9rem 0;
  border-top: 1px dotted var(--rule);
}
/* No rule above the first row — the whitespace under the headline already
   establishes the block's top edge, and a line there fenced it off. */
.case-meta > div:first-child { border-top: 0; padding-top: 0; }
.case-meta dd { margin: 0.5rem 0 0; }

@media (min-width: 640px) {
  .case-meta > div {
    grid-template-columns: 8.5rem 1fr;
    /* Real baselines, not an optical nudge: exact, and it survives a change of
       type size in either column. */
    align-items: baseline;
  }
  .case-meta dd { margin: 0; }
}
/* Avatars and pills have no meaningful text baseline — their flex box reports
   one, but it lands wherever the first glyph inside happens to sit. Centre
   those rows against the label instead. */
.case-meta > div:has(.team),
.case-meta > div:has(.tags) { align-items: center; }

/* Same values as .mono. This used to be 0.72rem/0.13em, one of several
   near-misses of the mono label idiom scattered through the file; the labels
   now sit on the shared values rather than a fourth variant of them. */
.case-meta dt { font-family: var(--mono); font-size: 0.78rem; font-weight: 500; letter-spacing: 0.15em; text-transform: uppercase; color: var(--ink-faint); }

/* --- The team stack ------------------------------------------------------
   The Team row as overlapping circles: my headshot, then teammates as initials.
   Colour stays on the ink ramp for the same reason .tag does — on this site
   colour says whose work you're looking at, so the only hue in the stack is the
   accent halo already baked into the headshot, which is cut per case study. */
.team {
  display: flex;
  align-items: center;
  margin: 0; padding: 0; list-style: none;
  /* New stacking context, so a raised avatar can't punch up through the nav. */
  isolation: isolate;
}

/* Overlap and circle size are set against each other so that centred initials
   clear the neighbour's ring on their own, with no compensating offset.

   Worth stating, because the obvious fix is wrong: padding the initials
   rightwards to dodge that ring fixes the resting state and breaks the hovered
   one. A raised avatar is fully visible and on top, so padding that had been
   compensating for a covered crescent just reads as text shoved off-centre.
   The amount covered changes with state, so the geometry has to work unaided.

   The sums, at 0.6rem: two mono characters measure ~13.1px, so in a 32px circle
   they start 9.5px in. The neighbour covers 0.3rem of overlap plus its 2px
   ring — 6.8px. ~2.7px of clearance, and the initials stay honestly centred
   whether the avatar is resting, hovered or focused.

   Overlap and ring are absolute, so they do not scale with the circle on their
   own: dropping the circle from 48px to 32px without also bringing these down
   leaves the neighbour covering over a third of it, and the initials get clipped.
   Change one of the three and the other two have to follow. */
.team li + li { margin-left: -0.3rem; }

/* Leftmost sits highest, so the photo reads as the front of the stack and the
   overlap runs away from it. Without this, source order would invert it. */
.team li:nth-child(1) { z-index: 6; }
.team li:nth-child(2) { z-index: 5; }
.team li:nth-child(3) { z-index: 4; }
.team li:nth-child(4) { z-index: 3; }
.team li:nth-child(5) { z-index: 2; }
.team li:nth-child(6) { z-index: 1; }

.avatar {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem;
  border-radius: 50%;
  /* The ring is --bg, not white: a cut-out of the page, so the circles read as
     separated without introducing a border colour. */
  box-shadow: 0 0 0 2px var(--bg);
  font-family: var(--mono); font-size: 0.6rem; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase;
  cursor: default;
  transition: translate 0.18s var(--ease), box-shadow 0.18s var(--ease);
}
.avatar img { width: 100%; height: 100%; border-radius: 50%; display: block; }

/* Two steps down the ink ramp, alternating, so the stack has depth without
   colour. --ink and --ink-soft both take --bg as their text; --tag-bg takes
   --ink, and is reused here so the rollup chip matches the tags below it. */
.avatar--photo { background: var(--bg); }
.avatar--a { background: var(--ink);      color: var(--bg); }
.avatar--b { background: var(--ink-soft); color: var(--bg); }
.avatar--more { background: var(--tag-bg); color: var(--ink); letter-spacing: 0.02em; }

/* Hover and focus lift the avatar out of the overlap and bring it to the front.
   :focus-visible carries the same state so this isn't mouse-only. */
.avatar:hover,
.avatar:focus-visible {
  translate: 0 -2px;
  box-shadow: 0 0 0 2px var(--bg), 0 5px 11px rgb(0 0 0 / 0.13);
  outline: none;
}
.team li:has(.avatar:hover),
.team li:has(.avatar:focus-visible) { z-index: 10; }
/* Keyboard focus still needs a visible ring — the lift alone isn't an
   indicator, and it's the one place the accent enters the stack. */
.avatar:focus-visible { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }

.avatar__tip {
  position: absolute;
  bottom: calc(100% + 0.6rem);
  left: 50%;
  translate: -50% 0;
  z-index: 20;
  padding: 0.4rem 0.6rem;
  background: var(--ink); color: var(--bg);
  border-radius: 7px;
  font-family: var(--mono); font-size: 0.66rem; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity 0.16s var(--ease), translate 0.16s var(--ease);
}
.avatar__tip::after {
  content: ""; position: absolute; top: 100%; left: 50%;
  translate: -50% -1px;
  border: 5px solid transparent; border-top-color: var(--ink);
}
.avatar:hover .avatar__tip,
.avatar:focus-visible .avatar__tip { opacity: 1; translate: -50% -2px; }

/* The first avatar's tooltip would hang off the left edge of the wrap, since
   the stack starts at the value column — pin it left instead of centring. */
.team li:first-child .avatar__tip { left: 0; translate: 0 0; }
.team li:first-child .avatar:hover .avatar__tip,
.team li:first-child .avatar:focus-visible .avatar__tip { translate: 0 -2px; }
/* Kept at the same fraction of the circle's width as it was at 48px, so the
   arrow still points into the first avatar rather than past its edge. */
.team li:first-child .avatar__tip::after { left: 0.83rem; }

/* The tooltip is a mouse and keyboard affordance only, so every avatar also
   carries its role as .visually-hidden text — that, not the tooltip, is what
   makes the row readable to assistive tech and on touch. */
@media (prefers-reduced-motion: reduce) {
  .avatar:hover, .avatar:focus-visible { translate: none; }
  .avatar:hover .avatar__tip, .avatar:focus-visible .avatar__tip { translate: -50% 0; }
}

.quote {
  margin: 0;
  padding: clamp(1.75rem, 4vw, 3rem) 0;
  border-block: 1px dotted var(--rule);
}
.quote p {
  margin: 0;
  font-size: clamp(1.3rem, 1rem + 1.5vw, 2.1rem);
  line-height: 1.24;
  letter-spacing: -0.015em;
  max-width: 26ch;
  text-indent: -0.42em;
}
.quote p::before { content: "“"; color: var(--accent); }
.quote p::after  { content: "”"; color: var(--accent); }
.quote footer { margin-top: 1.5rem; color: var(--ink-faint); font-family: var(--mono); font-size: 0.76rem; letter-spacing: 0.13em; text-transform: uppercase; }

/* Full-bleed screenshot with the caption set bottom-right, as in the deck. */
.shot { margin: 0 0 clamp(3rem, 6vw, 5rem); }
.shot__frame {
  border-radius: 14px;
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
  padding: clamp(0.5rem, 1.4vw, 1.15rem);
}
/* Video sits in the same mat as a still on purpose — a recording of the product is
   a screenshot that moves, not a separate "watch this" moment, so it inherits the
   inner radius and fluid width rather than getting a treatment of its own. */
.shot__frame img,
.shot__frame video { border-radius: 7px; width: 100%; display: block; }
/* The base reset's height:auto is scoped to img, so without this the height
   attribute — which is there to reserve the right box before the file arrives —
   would keep applying after it has and squash the frame. */
.shot__frame video { height: auto; }
.shot figcaption {
  margin-top: 1rem;
  text-align: right;
  font-family: var(--mono);
  font-size: clamp(0.72rem, 0.66rem + 0.3vw, 0.9rem);
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}

/* --- Video shots ----------------------------------------------------------
   A .shot--video plays silently on loop while it's on screen, which means the
   one thing it owes the reader is a way to stop it. That's .shot__toggle: it
   ships [hidden] and site.js reveals it, so a page without JS shows the poster
   frame and never a button that does nothing — the same contract the copy
   buttons keep. */

.shot--video .shot__frame { position: relative; }

.shot__toggle {
  position: absolute;
  /* Matched to .shot__frame's own padding so the button sits on the video's
     corner rather than floating in the mat. */
  right: calc(clamp(0.5rem, 1.4vw, 1.15rem) + 0.6rem);
  bottom: calc(clamp(0.5rem, 1.4vw, 1.15rem) + 0.6rem);
  /* 44px of target under a 34px face — the glyph can stay small without the
     button becoming a thing you have to aim at. */
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}

/* The face is drawn on ::before so the 44px hit area stays invisible. Solid
   accent rather than a tint: this sits over arbitrary video pixels, and a
   translucent chip would be legible over some frames and not others. */
.shot__toggle::before {
  content: "";
  grid-area: 1 / 1;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--accent-solid);
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.28);
}

/* Glyph, centred over the face. Pause is two bars via a border pair; play is a
   right-pointing triangle nudged off-centre to sit optically centred in the
   circle. Both are pure borders, so there's no icon file to load.

   Both pseudo-elements are grid items in the same cell rather than the glyph
   being absolutely positioned over the face: place-items centres them together,
   and stacking them this way avoids leaning on how a browser aligns an
   absolutely-positioned child inside a grid container — an area where Safari
   and Chrome have disagreed. */
.shot__toggle::after {
  content: "";
  grid-area: 1 / 1;
  width: 0; height: 10px;
  border-left: 3px solid var(--accent-solid-ink);
  border-right: 3px solid var(--accent-solid-ink);
  box-sizing: content-box;
  padding-inline: 1px;
}
.shot__toggle[aria-pressed="false"]::after {
  height: 0;
  padding: 0;
  border: 5px solid transparent;
  border-right: 0;
  border-left: 9px solid var(--accent-solid-ink);
  translate: 1.5px 0;
}

/* Revealed on approach, not permanently: the control is for when you want it,
   and a persistent button on a looping screenshot reads as chrome. Focus counts
   as approach so this is never mouse-only. */
.shot--video .shot__frame:hover .shot__toggle,
.shot__toggle:focus-visible { opacity: 1; }

.shot__toggle:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
  border-radius: 50%;
}

/* Coarse pointers have no hover to reveal it with. */
@media (pointer: coarse) {
  .shot__toggle { opacity: 1; }
}

.result {
  display: grid;
  gap: 1rem;
  max-width: 38rem;
  padding: clamp(1.75rem, 3.5vw, 2.5rem);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  border-radius: 16px;
}
.result__num {
  font-size: clamp(2rem, 1.4rem + 2.4vw, 3.25rem);
  /* --ink for the same reason as .stat__num; the box around it keeps the accent. */
  line-height: 1; letter-spacing: -0.025em; color: var(--ink);
}
/* Mirrors .stat__label — same job, same voice, one level down from the number. */
.result__label { margin: 0; color: var(--ink-soft); }

/* Three .result cards across, collapsing to one column below 920px — the same
   breakpoint .about uses, so the section changes shape at the point the rest of
   the page already does. The gap matches .stats' 1.5rem: these are the same kind
   of metric in a different container, and they should sit on the same rhythm.

   align-items is left at the grid default (stretch) on purpose. The labels are
   very different lengths, and equal-height cards are what make the row read as
   one set of results rather than three unrelated boxes. */
.impact-grid {
  display: grid;
  gap: 1.5rem;
  margin-top: clamp(2rem, 4vw, 3rem);
}
@media (min-width: 920px) { .impact-grid { grid-template-columns: repeat(3, 1fr); } }

/* The 38rem cap above is sized for a lone card holding one short line. In a
   one-third track it would strand the card short of its column, so the grid
   takes over the width and the cap comes off. */
.impact-grid .result { max-width: none; }

/* .result is itself a grid, so when the cards stretch to match the tallest one
   the leftover height goes into its rows and its 1rem gap — which throws the
   labels out of line: the card with the longest label needs all its height, the
   shorter ones have slack to distribute, and their labels get pushed down by it.
   Packing the content to the top sends the slack to the bottom of the card
   instead, so every number and every label starts on the same line. */
.impact-grid .result { align-content: start; }

/* Number and padding both come down a step from the lone card. At full width
   3.25rem over 2.5rem of padding is a poster; in a column a third that wide the
   same values leave a long label almost no room. */
.impact-grid .result { padding: clamp(1.5rem, 2.5vw, 1.9rem); }
.impact-grid .result__num { font-size: clamp(1.8rem, 1.3rem + 1.6vw, 2.6rem); }

/* The direction arrow qualifies the number, it isn't part of it. At full size and
   full --ink it reads as another glyph in "95%" and the eye has to work out that
   it isn't. Two steps of separation fix that: em-relative size so it stays in
   proportion at every breakpoint, and --ink-faint — the same step down the ramp
   the metadata labels take — so it sits behind the number rather than beside it. */
.delta {
  font-size: 0.58em;
  color: var(--ink-faint);
  /* Nudged up off the baseline, but only partway. Sitting on the baseline the
     arrow reads as another character in the number; lifted to the cap height it
     reads as a footnote marker. Around a fifth of an em is the band where it
     reads as an annotation to the figure. */
  vertical-align: 0.18em;
  /* Measured from the arrow's own em, which is small — it needs proportionally
     more than a word space to clear the % sign. */
  margin-left: 0.22em;
}

/* Same outline-stroke construction as .link-btn__icon — a thin line at the same
   proportional weight — so the up/down cue reads as the same mark as the button
   arrows, just rescaled to sit against a number instead of a mono label. */
.delta__icon {
  width: 0.85em;
  height: 0.85em;
  vertical-align: -0.05em;
  stroke: currentColor;
}

.case-nav {
  display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: space-between;
  border-top: 1px dotted var(--rule); padding-top: 2.5rem;
}

/* --- Motion --------------------------------------------------------------- */

/* Only hide content when JS is present to bring it back — the `js` class is set
   by an inline script in <head>. Without it the page renders fully visible. */
.js .reveal { opacity: 0; translate: 0 18px; }
.reveal.is-in {
  opacity: 1; translate: 0 0;
  transition: opacity 0.7s var(--ease), translate 0.7s var(--ease);
  transition-delay: var(--delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
  .js .reveal, .reveal { opacity: 1; translate: none; }
  .card:hover { translate: none; }
  /* Nothing autoplays here, so the poster is all you get until you ask for
     motion — and the button that asks can't be hidden behind a hover. */
  .shot__toggle { opacity: 1; }
}
